异常处理

当我们在调用服务提供者时有可能会抛异常(注意HystrixBadRequestException异常是不会触发服务降级,原因会在后面进行介绍),默认情况下方法抛了异常会自动触发服务降级,并交给服务降级中的方法去处理。同样由于Hystrix命令存在两种实现方法来调用服务,因此异常处理也需要分为两种情况。

继承类方式

如果开发者使用继承类的方式来实现Hystrix命令,那么我们可以在getFallback()方法内通过Throwable executionException = getExecutionException();方法来获取具体的异常信息,然后通过判断以进入不同的处理逻辑。

修改MovieCommand类中的getFallback方法的代码为如下所示:

1
2
3
4
5
6
@Override
protected Movie getFallback() {
Throwable executionException = getExecutionException();
System.out.println(executionException.getMessage());
return new Movie("狂人日记",128,"鲁迅");
}

为了更加清楚认识到这一点,笔者修改了MovieCommand类中run方法的逻辑,使其执行过程中抛出异常:

1
2
3
4
5
@Override
public Movie run() throws Exception{
int a = 1/0;
return restTemplate.getForObject("http://HELLO-SERVICE/getOneMovie",Movie.class);
}

然后需要启动以下信息,,注意启动的顺序不能颠倒:
(1)服务名为eureka-server的工程,这是服务注册中心,注意端口为1111,即启动实例名为peer1的服务实例。
(2)服务名为hello-service的工程,这是服务提供者,注意需要启动两个实例,启动的端口分别为8081和8082,即启动实例名为p1和p2的服务实例(注意两个服务提供者均需往服务注册中心注册)。
(3)服务名为ribbon-consumer的工程,这是服务消费者,注意需要启动一个实例,启动的端口为9000,即启动实例名为ribbon-consumer的服务实例。

接着去浏览器地址栏中访问http://localhost:9000/testOne链接,然后页面显示如下信息:

顺便可以在ribbon-consumer项目的控制台中可以看到有输出/ by zero等字样,这就说明由于出现异常,自动触发了服务降级。

注解方式

除了前面提到的继承类方式,还可以使用注解方式。注解方式的实现非常简单,只需要在fallback实现方法的参数中增加Throwable e对象的定义,这样在方法内部就可以获取触发服务降级的具体异常内容。

往MovieService类中新增testFall和testFallError方法,其中的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
//注解方式实现异常
@HystrixCommand(fallbackMethod = "testFallError")
public Movie testFall (){
int a = 1/0;
return restTemplate.getForObject("http://HELLO-SERVICE/getOneMovie",Movie.class);
}

public Movie testFallError(Throwable throwable){
System.out.println(throwable.getMessage());
return new Movie("狂人日记",128,"鲁迅");
}

既然是新增了两个方法,那么就需要在ConsumerController类中新增一个方法用于调用testFall方法,这里定义一个同名的testFall方法,相应的代码为:

1
2
3
4
5
//采用注解实现的异常处理
@GetMapping(value = "/testFall")
public Movie testFall(){
return movieService.testFall();
}

然后需要启动以下信息,,注意启动的顺序不能颠倒:
(1)服务名为eureka-server的工程,这是服务注册中心,注意端口为1111,即启动实例名为peer1的服务实例。
(2)服务名为hello-service的工程,这是服务提供者,注意需要启动两个实例,启动的端口分别为8081和8082,即启动实例名为p1和p2的服务实例(注意两个服务提供者均需往服务注册中心注册)。
(3)服务名为ribbon-consumer的工程,这是服务消费者,注意需要启动一个实例,启动的端口为9000,即启动实例名为ribbon-consumer的服务实例。

接着去浏览器地址栏中访问http://localhost:9000/testFall链接,然后页面显示如下信息:

顺便可以在ribbon-consumer项目的控制台中可以看到有输出/ by zero等字样,这就说明由于出现异常,自动触发了服务降级。


其实看到这里,大家也就明白了继承类和注解方式的区别,两种是存在非常相似的逻辑,只是逻辑执行方式不同。

在继承类方式中,run方法用来调用服务并抛出异常,而getFallback方法则用来输出异常和执行服务降级逻辑。

在注解方式中,添加有@HystrixCommand注解的方法(此处是testFall方法)用来调用服务并抛出异常,而fallbackMethod属性指定的方法(此处为testFallError方法)则用来输出异常和执行服务降级逻辑。

异常忽略

在实际开发过程中可能存在这样一种需求,就是假设某个异常被抛出后,我不希望该异常进入到服务降级方法中处理,而是直接将异常抛给用户。在前面我们抛出算数除法为0的异常,它们都进入了服务降级方法,并打印输出了详细的异常,如/ by zero。这里以注解方式为例进行说,开发者只需要在@HystrixCommand注解上添加ignoreExceptions属性,注意它的值是异常对象的字节码类型:

1
2
3
4
5
6
//注解方式实现异常
@HystrixCommand(fallbackMethod = "testFallError",ignoreExceptions = ArithmeticException.class)
public Movie testFall (){
int a = 1/0;
return restTemplate.getForObject("http://HELLO-SERVICE/getOneMovie",Movie.class);
}

然后按照前面的启动顺序启动各个服务后,访问http://localhost:9000/testFall链接,可以看到页面信息为:

确实实现了我们所要的目的,那么现在问题来了为什么这个异常不会进入到服务降级的逻辑中呢?那是因为在Hystrix中存在一个名为HystrixBadRequestException的异常,它是直接继承至RuntimeException异常,注意该异常不会进入到服务降级方法中。当我们在@HystrixCommand注解内配置了ignoreExceptions属性,且值为ArithmeticException.class时,如果抛出了ArithmeticException异常,那么Hystrix会将异常信息封装到HystrixBadRequestException中,然后将该异常抛出,由于这个异常不会进入到服务降级的逻辑中,因此就不会触发服务降级方法。

命令名称、分组以及线程池划分

不知道你是否发现这么一个问题,就是在使用继承方式实现Hystrix命令的时候,我们都在controller的方法中都有定义下面这行代码:

1
2
MovieCommand movieCommand = new MovieCommand(HystrixCommand.Setter.withGroupKey(
HystrixCommandGroupKey.Factory.asKey("")),restTemplate);

我们在MovieCommand类中定义了一个有参的构造方法,其中第一个参数Sette用于保存一些分组信息,第二个参数RestTemplate则用于调用相关服务:

1
2
3
4
protected MovieCommand(Setter setter,RestTemplate restTemplate) {
super(setter);
this.restTemplate = restTemplate;
}

也就说其实下面这行代码就表示分组信息:

1
HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(""))

但是具体什么含义前面没有介绍,因此这里来对其进行学习。

继承类的方式实现

如果开发者选择采用继承类的方式来实现Hystrix命令,那么它将默认使用类名作为默认的命令名称,但是我们可以在构造方法中通过Setetr静态类来设置。通过查看源码可以知道,当一个使用继承类的方式实现Hystrix命令,那么这个实现类其实就存在5个重载方法:

打开MovieCommand类,在里面新增包含HystrixCommandGroupKey参数的构造方法,如下所示:

1
2
3
4
protected MovieCommand() {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("GroupName"))
.andCommandKey(HystrixCommandKey.Factory.asKey("CommandName")));
}

从上面可以看出,我们并没有直接设置命令名称,而是先调用了Setter.withGroupKey()方法来设置命令组名,然后通过调用andCommandKey()方法来设置命令名称。查看一下Setter类的源码,可以知道它其实是一个内部类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public abstract class HystrixCommand<R> extends AbstractCommand<R> implements HystrixExecutable<R>, HystrixInvokableInfo<R>, HystrixObservable<R> {
//其他代码
......

public static final class Setter {
protected final HystrixCommandGroupKey groupKey;
protected HystrixCommandKey commandKey;
protected HystrixThreadPoolKey threadPoolKey;
protected com.netflix.hystrix.HystrixCommandProperties.Setter commandPropertiesDefaults;
protected com.netflix.hystrix.HystrixThreadPoolProperties.Setter threadPoolPropertiesDefaults;

protected Setter(HystrixCommandGroupKey groupKey) {
this.groupKey = groupKey;
}

public static HystrixCommand.Setter withGroupKey(HystrixCommandGroupKey groupKey) {
return new HystrixCommand.Setter(groupKey);
}

public HystrixCommand.Setter andCommandKey(HystrixCommandKey commandKey) {
this.commandKey = commandKey;
return this;
}

public HystrixCommand.Setter andThreadPoolKey(HystrixThreadPoolKey threadPoolKey) {
this.threadPoolKey = threadPoolKey;
return this;
}

public HystrixCommand.Setter andCommandPropertiesDefaults(com.netflix.hystrix.HystrixCommandProperties.Setter commandPropertiesDefaults) {
this.commandPropertiesDefaults = commandPropertiesDefaults;
return this;
}

public HystrixCommand.Setter andThreadPoolPropertiesDefaults(com.netflix.hystrix.HystrixThreadPoolProperties.Setter threadPoolPropertiesDefaults) {
this.threadPoolPropertiesDefaults = threadPoolPropertiesDefaults;
return this;
}
}
}

且只有withGroupKey方法才能返回一个Setter对象,所以GroupKey是每个Setter必需的参数,而CommandKey则是可选参数,这一点可以从前面所说的5个重载方法中得到验证。

通过设置命令组,Hystrix会根据组来组织和统计命令的告警、仪表盘等信息。除此之外,设置命令组不仅能实现统计,且Hystrix命令默认的线程划分也是根据命令分组来实现的。默认情况下,Hystrix会让相同组名的命令使用同一个线程池,所以需要我们在创建Hystrix命令时为其指定命令组名来实现默认的线程池划分。

如果Hystrix的线程池分配仅仅依靠命令组来划分,那么它就显得不够灵活,因此Hystrix还提供了HystrixThreadPoolKey来对线程池进行设置,通过它我们可以实现更细粒度的线程池划分。

打开MovieCommand类,在里面新增包含HystrixThreadPoolKey参数的构造方法,如下所示:

1
2
3
4
5
protected MovieCommand() {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("GroupName"))
.andCommandKey(HystrixCommandKey.Factory.asKey("CommandName"))
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("ThreadPoolKey")));
}

同样在没有指定HystrixThreadPoolKey的情况下,依然会使用命令组的方式来划分线程池。通常情况下,尽量通过HystrixThreadPoolKey的方式来指定线程池的划分,而不是通过组名的默认方式实现划分,因为多个不同的命令可能从业务逻辑上来看属于同一个组,但是往往从实现本身上需要跟其他命令进行隔离。

###注解的方式实现
在前面介绍了如何为通过继承实现的HystrixCommand设置命令名称、分组以及线程池划分,现在介绍另一种基于注解方式实现的Hystrix命令的设置方式。

其实使用注解实现Hystrix命令时,想设置命令名称、分组以及线程池划分非常简单,只需要在@HystrixCommand注解上设置相应的属性即可,如表示命令名称的属性commandKey,分组的属性groupKey,线程池划分的属性threadPoolKey,这些其实就是@HystrixCommand注解的属性,查看该注解的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public @interface HystrixCommand {
String groupKey() default "";

String commandKey() default "";

String threadPoolKey() default "";

String fallbackMethod() default "";

HystrixProperty[] commandProperties() default {};

HystrixProperty[] threadPoolProperties() default {};

Class<? extends Throwable>[] ignoreExceptions() default {};

ObservableExecutionMode observableExecutionMode() default ObservableExecutionMode.EAGER;

HystrixException[] raiseHystrixExceptions() default {};

String defaultFallback() default "";
}

因此这里面还有笔者没有介绍到的其他属性,开发者可以根据需要来自行选择使用。

接下来举一个例子来演示如何基于注解来实现命令名称、分组以及线程池划分的设置,如下所示:

1
2
3
4
5
//注解方式实现命令名称、分组以及线程池划分
@HystrixCommand(commandKey = "testGroup",groupKey = "MovieGroup",threadPoolKey = "testGroupByThread")
public Movie testGroup (){
return restTemplate.getForObject("http://HELLO-SERVICE/getOneMovie",Movie.class);
}

这样关于SpringCloud Hystrix的自定义命令、服务降级、异常处理、命令名称、分组和线程池划分的介绍就到此结束,后续开始学习请求缓存、请求合并等内容。