Spring Cloud Feign 熔断机制填坑
问题
最近在项目开发中,使用 Feign 调用服务,当触发熔断机制时,遇到了以下问题:
- 异常信息形如:
TestService#addRecord(ParamVO) failed and no fallback available.; - 获取不到服务提供方抛出的原始异常信息;
- 实现某些业务方法不进入熔断,直接往外抛出异常;
接下来将一一解决上述问题。
对于failed and no fallback available.这种异常信息,是因为项目开启了熔断:
1feign.hystrix.enabled: true
当调用服务时抛出了异常,却没有定义fallback方法,就会抛出上述异常。由此引出了第一个解决方式。
@FeignClient加上fallback方法,并获取异常信息
为@FeignClient修饰的接口加上fallback方法有两种方式,由于要获取异常信息,所以使用fallbackFactory的方式:
1@FeignClient(name = "serviceId", fallbackFactory = TestServiceFallback.class)
2public interface TestService {
3
4 @RequestMapping(value = "/get/{id}", method = RequestMethod.GET)
5 Result get(@PathVariable("id") Integer id);
6
7}
在@FeignClient注解中指定fallbackFactory,上面例子中是TestServiceFallback:
1import feign.hystrix.FallbackFactory;
2import org.apache.commons.lang3.StringUtils;
3
4@Component
5public class TestServiceFallback implements FallbackFactory<TestService> {
6
7 private static final Logger LOG = LoggerFactory.getLogger(TestServiceFallback.class);
8
9 public static final String ERR_MSG = "Test接口暂时不可用: ";
10
11 @Override
12 public TestService create(Throwable throwable) {
13 String msg = throwable == null ? "" : throwable.getMessage();
14 if (!StringUtils.isEmpty(msg)) {
15 LOG.error(msg);
16 }
17 return new TestService() {
18 @Override
19 public String get(Integer id) {
20 return ResultBuilder.unsuccess(ERR_MSG + msg);
21 }
22 };
23 }
24}
通过实现FallbackFactory,可以在create方法中获取到服务抛出的异常。但是请注意,这里的异常是被Feign封装过的异常,不能直接在异常信息中看出原始方法抛出的异常。这时得到的异常信息形如:
1status 500 reading TestService#addRecord(ParamVO); content:
2{"success":false,"resultCode":null,"message":"/ by zero","model":null,"models":[],"pageInfo":null,"timelineInfo":null,"extra":null,"validationMessages":null,"valid":false}
说明一下,本例子中,服务提供者的接口返回信息会统一封装在自定义类Result中,内容就是上述的content:
1{"success":false,"resultCode":null,"message":"/ by zero","model":null,"models":[],"pageInfo":null,"timelineInfo":null,"extra":null,"validationMessages":null,"valid":false}
因此,异常信息我希望是message的内容:/ by zero,这样打日志时能够方便识别异常。
保留原始异常信息
当调用服务时,如果服务返回的状态码不是200,就会进入到Feign的ErrorDecoder中,因此如果我们要解析异常信息,就要重写ErrorDecoder:
1import feign.Response;
2import feign.Util;
3import feign.codec.ErrorDecoder;
4
5/**
6 * @Author: CipherCui
7 * @Description: 保留 feign 服务异常信息
8 * @Date: Created in 1:29 2018/6/2
9 */
10public class KeepErrMsgConfiguration {
11
12 @Bean
13 public ErrorDecoder errorDecoder() {
14 return new UserErrorDecoder();
15 }
16
17 /**
18 * 自定义错误解码器
19 */
20 public class UserErrorDecoder implements ErrorDecoder {
21
22 private Logger logger = LoggerFactory.getLogger(getClass());
23
24 @Override
25 public Exception decode(String methodKey, Response response) {
26 Exception exception = null;
27 try {
28 // 获取原始的返回内容
29 String json = Util.toString(response.body().asReader());
30 exception = new RuntimeException(json);
31 // 将返回内容反序列化为Result,这里应根据自身项目作修改
32 Result result = JsonMapper.nonEmptyMapper().fromJson(json, Result.class);
33 // 业务异常抛出简单的 RuntimeException,保留原来错误信息
34 if (!result.isSuccess()) {
35 exception = new RuntimeException(result.getMessage());
36 }
37 } catch (IOException ex) {
38 logger.error(ex.getMessage(), ex);
39 }
40 return exception;
41 }
42 }
43
44}
上面是一个例子,原理是根据response.body()反序列化为自定义的Result类,提取出里面的message信息,然后抛出RuntimeException,这样当进入到熔断方法中时,获取到的异常就是我们处理过的RuntimeException。
注意上面的例子并不是通用的,但原理是相通的,大家要结合自身的项目作相应的修改。
要使上面代码发挥作用,还需要在@FeignClient注解中指定configuration:
1@FeignClient(name = "serviceId", fallbackFactory = TestServiceFallback.class, configuration = {KeepErrMsgConfiguration.class})
2public interface TestService {
3
4 @RequestMapping(value = "/get/{id}", method = RequestMethod.GET)
5 String get(@PathVariable("id") Integer id);
6
7}
不进入熔断,直接抛出异常
有时我们并不希望方法进入熔断逻辑,只是把异常原样往外抛。这种情况我们只需要捉住两个点:不进入熔断、原样。
原样就是获取原始的异常,上面已经介绍过了,而不进入熔断,需要把异常封装成HystrixBadRequestException,对于HystrixBadRequestException,Feign会直接抛出,不进入熔断方法。
因此我们只需要在上述KeepErrMsgConfiguration的基础上作一点修改即可:
1/**
2 * @Author: CipherCui
3 * @Description: feign 服务异常不进入熔断
4 * @Date: Created in 1:29 2018/6/2
5 */
6public class NotBreakerConfiguration {
7
8 @Bean
9 public ErrorDecoder errorDecoder() {
10 return new UserErrorDecoder();
11 }
12
13 /**
14 * 自定义错误解码器
15 */
16 public class UserErrorDecoder implements ErrorDecoder {
17
18 private Logger logger = LoggerFactory.getLogger(getClass());
19
20 @Override
21 public Exception decode(String methodKey, Response response) {
22 Exception exception = null;
23 try {
24 String json = Util.toString(response.body().asReader());
25 exception = new RuntimeException(json);
26 Result result = JsonMapper.nonEmptyMapper().fromJson(json, Result.class);
27 // 业务异常包装成 HystrixBadRequestException,不进入熔断逻辑
28 if (!result.isSuccess()) {
29 exception = new HystrixBadRequestException(result.getMessage());
30 }
31 } catch (IOException ex) {
32 logger.error(ex.getMessage(), ex);
33 }
34 return exception;
35 }
36 }
37
38}
总结
为了更好的达到熔断效果,我们应该为每个接口指定fallback方法。而根据自身的业务特点,可以灵活的配置上述的KeepErrMsgConfiguration和NotBreakerConfiguration,或自己编写Configuration。
以上例子特殊性较强,不足之处请不吝指教。希望大家可以从中获取到有用的东西,应用到自己的项目中,感谢阅读。
