介绍Feign在项目中的正确打开方式
看了上一期Feign远程调用的小伙伴可能会问:阿鉴,你不是说上一期讲的是Feign的99%常用方式吗?怎么今天还有正确打开方式一说呀?
阿鉴:是99%的常用方式,阿鉴绝对没有诓大家,只是这一期的1%作为画龙点睛之笔而已,嘿嘿
先来一套案例
- 商品服务接口
@RestController @RequestMapping("/goods") public class GoodsController { @GetMapping("/get-goods") public Goods getGoods() throws InterruptedException { TimeUnit.SECONDS.sleep(10); System.out.println("xxxxxxx"); return new Goods().setName("苹果") .setPrice(1.1) .setNumber(2); } @PostMapping("save") public void save(@RequestBody Goods goods){ System.out.println(goods); } }
- 商品服务feign接口
@FeignClient(name = "my-goods", path = "/goods", contextId = "goods") public interface GoodsApi { @GetMapping("/get-goods") Goods getGoods(); @PostMapping(value = "/save") void save(Goods goods); }
- 订单服务接口
@RestController @RequestMapping("/order") public class OrderController { @Resource private GoodsApi goodsApi; @GetMapping("/get-goods") public Goods getGoods(){ return goodsApi.getGoods(); } @PostMapping("/save-goods") public String saveGoods(){ goodsApi.save(new Goods().setName("banana").setNumber(1).setPrice(1.1)); return "ok"; } }
没错,这就是上一期的查询和保存接口,由订单服务调用商品服务
正常情况下,这个案例运行是没有任何问题的,但是,项目实际运行中会遇到各种各样的问题。我们一个一个来。
超时
在上一次,我们了解到,当服务提供者响应超时时(网络出了问题,或者服务确实响应不过来),服务调用者是可以配置超时时间及时掐断请求来避免线程阻塞。如以下配置
feign: client: config: default: # 连接超时时间 单位毫秒 默认10秒 connectTimeout: 1000 # 请求超时时间 单位毫秒 默认60秒 readTimeout: 5000
现在,我们在商品服务的接口中进行睡眠10s来模拟超时情况
然后,发起调用,你会发现原本接口返回的数据是个json格式,现在由于超时Feign抛出异常,页面成了这个样子:
居然返回了个页面!
这样子肯定是不行的,我们需要当出现超时情况时,返回一个预期的错误,比如服务调用失败的异常
Feign的作者也想到了这一点,给我们提供一个Fallback的机制,用法如下:
- 开启hystrix
feign: hystrix: enabled: true
- 编写GoodsApiFallback
@Slf4j @Component public class GoodsApiFallback implements FallbackFactory<GoodsApi> { @Override public GoodsApi create(Throwable throwable) { log.error(throwable.getMessage(), throwable); return new GoodsApi() { @Override public Goods getGoods() { return new Goods(); } @Override public void save(Goods goods) { } }; } }
- 在FeignClient中添加属性fallbackFactory
@FeignClient(name = "my-goods", path = "/goods", contextId = "goods", fallbackFactory = GoodsApiFallback.class) public interface GoodsApi { }
当再次请求超时时,就会启用fallback中的响应逻辑,而我们编写的逻辑是返回一个new Goods(),所以超时时请求逻辑中会得到一个空的Goods对象,就像这样:
看起来,由于超时返回的信息不友好的问题确实解决了,但是,我们在fallback中返回了一个空对象,这时候就会造成逻辑混乱:是商品服务里没有这个商品还是服务超时呢?不晓得了...
使用带有异常信息的返回对象
为了解决这个逻辑的混乱,于是我们想到使用一个可以带有异常信息的返回对象,他的结构如下:
{ "code": 0, "message": "", "data": {} }
我们定义,code为0时表示正确返回
基于此,我们便可以修改以上逻辑:
- 商品服务正常返回时,code:0
- 出现超时状态时,code: -1
被调整代码如下:
- 商品服务
@GetMapping("/get-goods") public BaseResult<Goods> getGoods() throws InterruptedException { System.out.println("xxxxxxx"); return BaseResult.success(new Goods().setName("苹果") .setPrice(1.1) .setNumber(2)); }
- 商品服务feign接口
@GetMapping("/get-goods") BaseResult<Goods> getGoods();
- 商品服务feign接口Fallback
return new GoodsApi() { @Override public BaseResult<Goods> getGoods() { BaseResult<Goods> result = new BaseResult<>(); result.setCode(-1); result.setMessage("商品服务响应超时"); return result; } }
- 订单服务
@GetMapping("/get-goods") public Goods getGoods(){ BaseResult<Goods> result = goodsApi.getGoods(); if(result.getCode() != 0){ throw new RuntimeException("调用商品服务发生错误:" + result.getMessage()); } return result.getData(); }
现在,既解决了服务响应超时返回信息不友好的问题,也解决了逻辑混乱问题,大功告成?
统一异常校验并解包
以上的解决方案确实可以了,一般项目的手法也就到这里了,只是用起来。。。
你会发现一个很恶心的问题,本来我们的使用方式是这样的:
Goods goods = goodsApi.getGoods();
现在成了这样:
BaseResult<Goods> result = goodsApi.getGoods(); if(result.getCode() != 0){ throw new RuntimeException("调用商品服务发生错误:" + result.getMessage()); } Goods goods = result.getData();
而且这段代码到处都是,因为很多Feign接口嘛,每个Feign接口的校验逻辑都是一模一样:
BaseResult<xxx> result = xxxApi.getXxx(); if(result.getCode() != 0){ throw new RuntimeException("调用xxx服务发生错误:" + result.getMessage()); } Xxx xxx = result.getData();
———————分割线———————
我,阿鉴,作为一个有代码洁癖的人,会允许这种事情发生吗?不可能!
什么好用的方式与安全的方式不可兼得,作为一个成年人:我都要!
现在我们就来把它变成既是原来的使用方式,又能得到友好的返回信息。
在上一期,我们提到了Feign有个编解码的流程,而解码这个动作,就会涉及到将服务端返回的信息,解析成客户端需要的内容。
所以思路就是:自定义一个解码器,将服务器返回的信息进行解码,判断BaseResult的code值,code为0直接把data返回,code不为0抛出异常。
上代码:
- 编写自定义解码器
@Slf4j public class BaseResultDecode extends ResponseEntityDecoder { public BaseResultDecode(Decoder decoder) { super(decoder); } @Override public Object decode(Response response, Type type) throws IOException, FeignException { if (type instanceof ParameterizedType) { if (((ParameterizedType) type).getRawType() != BaseResult.class) { type = new ParameterizedTypeImpl(new Type[]{type}, null, BaseResult.class); Object object = super.decode(response, type); if (object instanceof BaseResult) { BaseResult<?> result = (BaseResult<?>) object; if (result.isFailure()) { log.error("调用Feign接口出现异常,接口:{}, 异常: {}", response.request().url(), result.getMessage()); throw new BusinessException(result.getCode(), result.getMessage()); } return result.getData(); } } } return super.decode(response, type); } }
Feign中默认的解码器是ResponseEntityDecoder,所以我们只需要继承它,在原有的基础上作一些修改就可以了。
- 将解码器注入到Spring中
@Configuration public class DecodeConfiguration { @Bean public Decoder feignDecoder(ObjectFactory<HttpMessageConverters> messageConverters) { return new OptionalDecoder( new BaseResultDecode(new SpringDecoder(messageConverters))); } }
这段代码是直接抄的源码,源码中是这样:
new OptionalDecoder( new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)))
我只是把ResponseEntityDecoder替换成了自己的BaseResultDecode
现在我们把代码换回原来的方式吧
- 商品服务
@GetMapping("/get-goods") public BaseResult<Goods> getGoods() throws InterruptedException { System.out.println("xxxxxxx"); return BaseResult.success(new Goods().setName("苹果") .setPrice(1.1) .setNumber(2)); }
这里还是需要放回BaseResult
- 商品服务feign接口
@GetMapping("/get-goods") Goods getGoods();
- 商品服务feign接口Fallback
return new GoodsApi() { @Override public Goods getGoods() { throw new RuntimeException("调用商品服务发生异常"); } }
- 订单服务打印curl日志
@GetMapping("/get-goods") public Goods getGoods(){ return goodsApi.getGoods(); }
这个章节和前面的没有关系,只是效仿前端请求时可以复制一个curl出来,调试时十分方便。
同样的逻辑:自定义一个日志打印器
代码如下:
- 自定义logger
public class CurlLogger extends Slf4jLogger { private final Logger logger; public CurlLogger(Class<?> clazz) { super(clazz); this.logger = LoggerFactory.getLogger(clazz); } @Override protected void logRequest(String configKey, Level logLevel, Request request) { if (logger.isDebugEnabled()) { logger.debug(toCurl(request.requestTemplate())); } super.logRequest(configKey, logLevel, request); } public String toCurl(feign.RequestTemplate template) { String headers = Arrays.stream(template.headers().entrySet().toArray()) .map(header -> header.toString().replace('=', ':') .replace('[', ' ') .replace(']', ' ')) .map(h -> String.format(" --header '%s' %n", h)) .collect(Collectors.joining()); String httpMethod = template.method().toUpperCase(Locale.ROOT); String url = template.url(); if(template.body() != null){ String body = new String(template.body(), StandardCharsets.UTF_8); return String.format("curl --location --request %s '%s' %n%s %n--data-raw '%s'", httpMethod, url, headers, body); } return String.format("curl --location --request %s '%s' %n%s", httpMethod, url, headers); } }
同样,直接继承默认的Slf4jLogger
- 自定义日志工厂
public class CurlFeignLoggerFactory extends DefaultFeignLoggerFactory { public CurlFeignLoggerFactory(Logger logger) { super(logger); } @Override public Logger create(Class<?> type) { return new CurlLogger(type); } }
- 注入到Spring
@Bean public FeignLoggerFactory curlFeignLoggerFactory(){ return new CurlFeignLoggerFactory(null); }
效果如下:
curl --location --request POST 'http://my-goods/goods/save' --header 'Content-Encoding: gzip, deflate ' --header 'Content-Length: 40 ' --header 'Content-Type: application/json ' --header 'token: 123456 '
小结
在本章节,我给大家介绍了在实际项目中Feign的使用方式:使用带有异常信息的返回对象
以及这样使用的原因:需要让服务调用方能够得到明确的响应信息
这样使用的弊端:总是需要判断服务返回的信息是否正确
解决方式:自定义一个解码器
最后还给大家提供了一个打印curl日志的方式。
最后的最后,阿鉴想对大家说几句,不知道大家看了阿鉴的自定义解码器和自定义logger有没有什么感触,大家以前可能一直觉得对一些框架进行扩展是一件有多难,有多厉害的事情,其实也没有那么难,很多时候我们只需要基于框架中的逻辑进行一些小小的扩展即可,总结来说就是,发现它,继承它,修改它。
那么,我们下期再见~