介绍 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("调用商品服务产生异样"); } }
-
订单服务
@GetMapping("/get-goods") public Goods getGoods(){return goodsApi.getGoods(); }
打印 curl 日志
这个章节和后面的没有关系,只是效仿前端申请时能够复制一个 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 有没有什么感触,大家以前可能始终感觉对一些框架进行扩大是一件有多难,有多厉害的事件,其实也没有那么难,很多时候咱们只须要基于框架中的逻辑进行一些小小的扩大即可,总结来说就是,发现它,继承它,批改它。
那么,咱们下期再见~
想要理解更多精彩内容,欢送关注公众号:程序员阿鉴,阿鉴在公众号欢送你的到来~
集体博客空间:https://zijiancode.cn/archive…