介绍Feign在项目中的正确打开方式

看了上一期Feign远程调用的小伙伴可能会问:阿鉴,你不是说上一期讲的是Feign的99%常用方式吗?怎么今天还有正确打开方式一说呀?

阿鉴:是99%的常用方式,阿鉴绝对没有诓大家,只是这一期的1%作为画龙点睛之笔而已,嘿嘿

先来一套案例

  • 商品服务接口

    1. @RestController
    2. @RequestMapping("/goods")
    3. public class GoodsController {
    4. @GetMapping("/get-goods")
    5. public Goods getGoods() throws InterruptedException {
    6. TimeUnit.SECONDS.sleep(10);
    7. System.out.println("xxxxxxx");
    8. return new Goods().setName("苹果")
    9. .setPrice(1.1)
    10. .setNumber(2);
    11. }
    12. @PostMapping("save")
    13. public void save(@RequestBody Goods goods){
    14. System.out.println(goods);
    15. }
    16. }
  • 商品服务feign接口

    1. @FeignClient(name = "my-goods", path = "/goods", contextId = "goods")
    2. public interface GoodsApi {
    3. @GetMapping("/get-goods")
    4. Goods getGoods();
    5. @PostMapping(value = "/save")
    6. void save(Goods goods);
    7. }
  • 订单服务接口

    1. @RestController
    2. @RequestMapping("/order")
    3. public class OrderController {
    4. @Resource
    5. private GoodsApi goodsApi;
    6. @GetMapping("/get-goods")
    7. public Goods getGoods(){
    8. return goodsApi.getGoods();
    9. }
    10. @PostMapping("/save-goods")
    11. public String saveGoods(){
    12. goodsApi.save(new Goods().setName("banana").setNumber(1).setPrice(1.1));
    13. return "ok";
    14. }
    15. }

没错,这就是上一期的查询和保存接口,由订单服务调用商品服务

正常情况下,这个案例运行是没有任何问题的,但是,项目实际运行中会遇到各种各样的问题。我们一个一个来。

超时

在上一次,我们了解到,当服务提供者响应超时时(网络出了问题,或者服务确实响应不过来),服务调用者是可以配置超时时间及时掐断请求来避免线程阻塞。如以下配置

  1. feign:
  2. client:
  3. config:
  4. default:
  5. # 连接超时时间 单位毫秒 默认10秒
  6. connectTimeout: 1000
  7. # 请求超时时间 单位毫秒 默认60秒
  8. readTimeout: 5000

现在,我们在商品服务的接口中进行睡眠10s来模拟超时情况

然后,发起调用,你会发现原本接口返回的数据是个json格式,现在由于超时Feign抛出异常,页面成了这个样子:

居然返回了个页面!

这样子肯定是不行的,我们需要当出现超时情况时,返回一个预期的错误,比如服务调用失败的异常

Feign的作者也想到了这一点,给我们提供一个Fallback的机制,用法如下:

  1. 开启hystrix

    1. feign:
    2. hystrix:
    3. enabled: true
  2. 编写GoodsApiFallback

    1. @Slf4j
    2. @Component
    3. public class GoodsApiFallback implements FallbackFactory<GoodsApi> {
    4. @Override
    5. public GoodsApi create(Throwable throwable) {
    6. log.error(throwable.getMessage(), throwable);
    7. return new GoodsApi() {
    8. @Override
    9. public Goods getGoods() {
    10. return new Goods();
    11. }
    12. @Override
    13. public void save(Goods goods) {
    14. }
    15. };
    16. }
    17. }
  3. 在FeignClient中添加属性fallbackFactory

    1. @FeignClient(name = "my-goods", path = "/goods", contextId = "goods", fallbackFactory = GoodsApiFallback.class)
    2. public interface GoodsApi {
    3. }

当再次请求超时时,就会启用fallback中的响应逻辑,而我们编写的逻辑是返回一个new Goods(),所以超时时请求逻辑中会得到一个空的Goods对象,就像这样:

看起来,由于超时返回的信息不友好的问题确实解决了,但是,我们在fallback中返回了一个空对象,这时候就会造成逻辑混乱:是商品服务里没有这个商品还是服务超时呢?不晓得了...

使用带有异常信息的返回对象

为了解决这个逻辑的混乱,于是我们想到使用一个可以带有异常信息的返回对象,他的结构如下:

  1. {
  2. "code": 0,
  3. "message": "",
  4. "data": {}
  5. }

我们定义,code为0时表示正确返回

基于此,我们便可以修改以上逻辑:

  • 商品服务正常返回时,code:0
  • 出现超时状态时,code: -1

被调整代码如下:

  • 商品服务

    1. @GetMapping("/get-goods")
    2. public BaseResult<Goods> getGoods() throws InterruptedException {
    3. System.out.println("xxxxxxx");
    4. return BaseResult.success(new Goods().setName("苹果")
    5. .setPrice(1.1)
    6. .setNumber(2));
    7. }
  • 商品服务feign接口

    1. @GetMapping("/get-goods")
    2. BaseResult<Goods> getGoods();
  • 商品服务feign接口Fallback

    1. return new GoodsApi() {
    2. @Override
    3. public BaseResult<Goods> getGoods() {
    4. BaseResult<Goods> result = new BaseResult<>();
    5. result.setCode(-1);
    6. result.setMessage("商品服务响应超时");
    7. return result;
    8. }
    9. }
  • 订单服务

    1. @GetMapping("/get-goods")
    2. public Goods getGoods(){
    3. BaseResult<Goods> result = goodsApi.getGoods();
    4. if(result.getCode() != 0){
    5. throw new RuntimeException("调用商品服务发生错误:" + result.getMessage());
    6. }
    7. return result.getData();
    8. }

现在,既解决了服务响应超时返回信息不友好的问题,也解决了逻辑混乱问题,大功告成?

统一异常校验并解包

以上的解决方案确实可以了,一般项目的手法也就到这里了,只是用起来。。。

你会发现一个很恶心的问题,本来我们的使用方式是这样的:

  1. Goods goods = goodsApi.getGoods();

现在成了这样:

  1. BaseResult<Goods> result = goodsApi.getGoods();
  2. if(result.getCode() != 0){
  3. throw new RuntimeException("调用商品服务发生错误:" + result.getMessage());
  4. }
  5. Goods goods = result.getData();

而且这段代码到处都是,因为很多Feign接口嘛,每个Feign接口的校验逻辑都是一模一样:

  1. BaseResult<xxx> result = xxxApi.getXxx();
  2. if(result.getCode() != 0){
  3. throw new RuntimeException("调用xxx服务发生错误:" + result.getMessage());
  4. }
  5. Xxx xxx = result.getData();

———————分割线———————

我,阿鉴,作为一个有代码洁癖的人,会允许这种事情发生吗?不可能!

什么好用的方式与安全的方式不可兼得,作为一个成年人:我都要!

现在我们就来把它变成既是原来的使用方式,又能得到友好的返回信息。

在上一期,我们提到了Feign有个编解码的流程,而解码这个动作,就会涉及到将服务端返回的信息,解析成客户端需要的内容。

所以思路就是:自定义一个解码器,将服务器返回的信息进行解码,判断BaseResult的code值,code为0直接把data返回,code不为0抛出异常。

上代码:

  • 编写自定义解码器

    1. @Slf4j
    2. public class BaseResultDecode extends ResponseEntityDecoder {
    3. public BaseResultDecode(Decoder decoder) {
    4. super(decoder);
    5. }
    6. @Override
    7. public Object decode(Response response, Type type) throws IOException, FeignException {
    8. if (type instanceof ParameterizedType) {
    9. if (((ParameterizedType) type).getRawType() != BaseResult.class) {
    10. type = new ParameterizedTypeImpl(new Type[]{type}, null, BaseResult.class);
    11. Object object = super.decode(response, type);
    12. if (object instanceof BaseResult) {
    13. BaseResult<?> result = (BaseResult<?>) object;
    14. if (result.isFailure()) {
    15. log.error("调用Feign接口出现异常,接口:{}, 异常: {}", response.request().url(), result.getMessage());
    16. throw new BusinessException(result.getCode(), result.getMessage());
    17. }
    18. return result.getData();
    19. }
    20. }
    21. }
    22. return super.decode(response, type);
    23. }
    24. }

    Feign中默认的解码器是ResponseEntityDecoder,所以我们只需要继承它,在原有的基础上作一些修改就可以了。

  • 将解码器注入到Spring中

    1. @Configuration
    2. public class DecodeConfiguration {
    3. @Bean
    4. public Decoder feignDecoder(ObjectFactory<HttpMessageConverters> messageConverters) {
    5. return new OptionalDecoder(
    6. new BaseResultDecode(new SpringDecoder(messageConverters)));
    7. }
    8. }

    这段代码是直接抄的源码,源码中是这样:

    new OptionalDecoder( new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)))

    我只是把ResponseEntityDecoder替换成了自己的BaseResultDecode

现在我们把代码换回原来的方式吧

  • 商品服务

    1. @GetMapping("/get-goods")
    2. public BaseResult<Goods> getGoods() throws InterruptedException {
    3. System.out.println("xxxxxxx");
    4. return BaseResult.success(new Goods().setName("苹果")
    5. .setPrice(1.1)
    6. .setNumber(2));
    7. }

    这里还是需要放回BaseResult

  • 商品服务feign接口

    1. @GetMapping("/get-goods")
    2. Goods getGoods();
  • 商品服务feign接口Fallback

    1. return new GoodsApi() {
    2. @Override
    3. public Goods getGoods() {
    4. throw new RuntimeException("调用商品服务发生异常");
    5. }
    6. }
  • 订单服务

    1. @GetMapping("/get-goods")
    2. public Goods getGoods(){
    3. return goodsApi.getGoods();
    4. }

打印curl日志

这个章节和前面的没有关系,只是效仿前端请求时可以复制一个curl出来,调试时十分方便。

同样的逻辑:自定义一个日志打印器

代码如下:

  • 自定义logger

    1. public class CurlLogger extends Slf4jLogger {
    2. private final Logger logger;
    3. public CurlLogger(Class<?> clazz) {
    4. super(clazz);
    5. this.logger = LoggerFactory.getLogger(clazz);
    6. }
    7. @Override
    8. protected void logRequest(String configKey, Level logLevel, Request request) {
    9. if (logger.isDebugEnabled()) {
    10. logger.debug(toCurl(request.requestTemplate()));
    11. }
    12. super.logRequest(configKey, logLevel, request);
    13. }
    14. public String toCurl(feign.RequestTemplate template) {
    15. String headers = Arrays.stream(template.headers().entrySet().toArray())
    16. .map(header -> header.toString().replace('=', ':')
    17. .replace('[', ' ')
    18. .replace(']', ' '))
    19. .map(h -> String.format(" --header '%s' %n", h))
    20. .collect(Collectors.joining());
    21. String httpMethod = template.method().toUpperCase(Locale.ROOT);
    22. String url = template.url();
    23. if(template.body() != null){
    24. String body = new String(template.body(), StandardCharsets.UTF_8);
    25. return String.format("curl --location --request %s '%s' %n%s %n--data-raw '%s'", httpMethod, url, headers, body);
    26. }
    27. return String.format("curl --location --request %s '%s' %n%s", httpMethod, url, headers);
    28. }
    29. }

    同样,直接继承默认的Slf4jLogger

  • 自定义日志工厂

    1. public class CurlFeignLoggerFactory extends DefaultFeignLoggerFactory {
    2. public CurlFeignLoggerFactory(Logger logger) {
    3. super(logger);
    4. }
    5. @Override
    6. public Logger create(Class<?> type) {
    7. return new CurlLogger(type);
    8. }
    9. }
  • 注入到Spring

    1. @Bean
    2. public FeignLoggerFactory curlFeignLoggerFactory(){
    3. return new CurlFeignLoggerFactory(null);
    4. }

效果如下:

  1. curl --location --request POST 'http://my-goods/goods/save'
  2. --header 'Content-Encoding: gzip, deflate '
  3. --header 'Content-Length: 40 '
  4. --header 'Content-Type: application/json '
  5. --header 'token: 123456 '

小结

在本章节,我给大家介绍了在实际项目中Feign的使用方式:使用带有异常信息的返回对象

以及这样使用的原因:需要让服务调用方能够得到明确的响应信息

这样使用的弊端:总是需要判断服务返回的信息是否正确

解决方式:自定义一个解码器

最后还给大家提供了一个打印curl日志的方式。

最后的最后,阿鉴想对大家说几句,不知道大家看了阿鉴的自定义解码器和自定义logger有没有什么感触,大家以前可能一直觉得对一些框架进行扩展是一件有多难,有多厉害的事情,其实也没有那么难,很多时候我们只需要基于框架中的逻辑进行一些小小的扩展即可,总结来说就是,发现它,继承它,修改它。

那么,我们下期再见~

想要了解更多精彩内容,欢迎关注公众号:程序员阿鉴,阿鉴在公众号欢迎你的到来~

个人博客空间:https://zijiancode.cn/archives/feign2

Feign实战技巧篇的更多相关文章

  1. 2天驾驭DIV+CSS (技巧篇)(转)

     这是去年看到的一片文章,感觉在我的学习中,有不少的影响.于是把它分享给想很快了解css的兄弟们.本文是技巧篇. 基础篇[知识一] “DIV+CSS” 的叫法是不准确的[知识二] “DIV+CSS” ...

  2. Swift实战技巧

    Swift实战技巧 从OC转战到Swift,差别还是蛮大的,本文记录了我再从OC转到Swift开发过程中遇到的一些问题,然后把我遇到的这些问题记录形成文章,大体上是一些Swift语言下面的一些技巧,希 ...

  3. Git 沙盒模拟实战(远程篇)

    Git 沙盒模拟实战(远程篇) >---基础篇 远程仓库 远程仓库并不复杂, 在如今的云计算盛行的世界很容易把远程仓库想象成一个富有魔力的东西, 但实际上它们只是你的仓库在另个一台计算机上的拷贝 ...

  4. 【Spring Cloud & Alibaba 实战 | 总结篇】Spring Cloud Gateway + Spring Security OAuth2 + JWT 实现微服务统一认证授权和鉴权

    一. 前言 hi,大家好~ 好久没更文了,期间主要致力于项目的功能升级和问题修复中,经过一年时间的打磨,[有来]终于迎来v2.0版本,相较于v1.x版本主要完善了OAuth2认证授权.鉴权的逻辑,结合 ...

  5. 《手把手教你》系列技巧篇(十四)-java+ selenium自动化测试-元素定位大法之By xpath上卷(详细教程)

    1.简介 按宏哥计划,本文继续介绍WebDriver关于元素定位大法,这篇介绍定位倒数二个方法:By xpath.xpath 的定位方法, 非常强大.  使用这种方法几乎可以定位到页面上的任意元素. ...

  6. 《手把手教你》系列技巧篇(十五)-java+ selenium自动化测试-元素定位大法之By xpath中卷(详细教程)

    1.简介 按宏哥计划,本文继续介绍WebDriver关于元素定位大法,这篇介绍定位倒数二个方法:By xpath.xpath 的定位方法, 非常强大.  使用这种方法几乎可以定位到页面上的任意元素. ...

  7. 《手把手教你》系列技巧篇(十六)-java+ selenium自动化测试-元素定位大法之By xpath下卷(详细教程)

    1.简介 按宏哥计划,本文继续介绍WebDriver关于元素定位大法,这篇介绍定位倒数二个方法:By xpath.xpath 的定位方法, 非常强大.  使用这种方法几乎可以定位到页面上的任意元素. ...

  8. 《手把手教你》系列技巧篇(十七)-java+ selenium自动化测试-元素定位大法之By css上卷(详细教程)

    1.简介 CSS定位方式和xpath定位方式基本相同,只是CSS定位表达式有其自己的格式.CSS定位方式拥有比xpath定位速度快,且比CSS稳定的特性.下面详细介绍CSS定位方式的使用方法.xpat ...

  9. 《手把手教你》系列技巧篇(十八)-java+ selenium自动化测试-元素定位大法之By css中卷(详细教程)

    1.简介 按计划今天宏哥继续讲解倚天剑-css的定位元素的方法:ID属性值定位.其他属性值定位和使用属性值的一部分定位(这个类似xpath的模糊定位). 2.常用定位方法(8种) (1)id(2)na ...

随机推荐

  1. sql数据库新建作业,新建步骤时报错从 IClassFactory 为 CLSID 为 {AA40D1D6-CAEF-4A56-B9BB-D0D3DC976BA2} 的 COM 组件创建实例失败,原因是出现以下错误: c001f011。 (Microsoft.SqlServer.ManagedDTS)

    简单粗暴的重启sql数据库 其他网上找的方法 32位操作系统: 打开运行(命令提示符), 一.输入 cd c:\windows\system32 进入到c:\windows\system32路径中 二 ...

  2. 简易版JDBC连接池

    JDBC连接池mini版的实现 首先是工具类 DbUtil 主要参数就是Driver.User.PWD等啦,主要用于建立连接 URL需要注意的是SSL和serverTimezone参数,和mysql驱 ...

  3. excel自动记录项目完成进度,是否逾期,逾期/提前完成天数,计算天数可以把now()改为today()

    =IF(D38="",NOW()-C38,F38) 注:如果没有启用迭代计算,可以点击"文件"-"选项"-"公式"-&q ...

  4. 关于Excel中表格转Markdown格式的技巧

    背景介绍 Excel文件转Markdown格式的Table是经常会遇到的场景. Visual Studio Code插件 - Excel to Markdown table Excel to Mark ...

  5. Redis 底层数据结构之链表

    文章参考:<Redis设计与实现>黄建宏 链表 链表提供了高效的节点重排能力,以及可以顺序访问,也可以通过增删节点灵活调整链表长度,Redis中的列表.发布订阅.慢查询.监视器等功能均用到 ...

  6. Vector ArrayList LinkedList

    三者都实现了List接口! Vector与ArrayList:采用顺序存储的方式,但是Vector是线程安全的,ArrayList是线程不安全的,按需使用: 当存储空间不足的时候,ArrayList默 ...

  7. shell下读取文件数据

    参考:https://www.imzcy.cn/1553.html while和for对文件的读取是有区别的: 1. for对文件的读是按字符串的方式进行的,遇到空格什么后,再读取的数据就会换行显示 ...

  8. Adaptive AUTOSAR 学习笔记 2 - 官方文档下载及阅读建议

    目前互联网上没有太多的 Adaptive AUTOSAR 的学习资料,官方文档是一个很不错的途径.看过官方文档才发现,目前很多关于 Adaptive AUTOSAR 的文章都是官方文档的简化翻译,不如 ...

  9. Java | 循环的控制语句

    循环的控制语句 循环的控制语句有两种:break.continue 两种. braak可以用于强制限出循环. continue可以用于强制结束本次循环. break braak可以用于强制限出循环. ...

  10. Kubernetes全栈架构师(二进制高可用安装k8s集群部署篇)--学习笔记

    目录 二进制高可用基本配置 二进制系统和内核升级 二进制基本组件安装 二进制生成证书详解 二进制高可用及etcd配置 二进制K8s组件配置 二进制使用Bootstrapping自动颁发证书 二进制No ...