撸了一个 Feign 增强包 V2.0 升级版-编程思维

前言

大概在两年前我写过一篇 撸了一个 Feign 增强包,当时准备是利用 SpringBoot + K8s 构建应用,这个库可以类似于 SpringCloud 那样结合 SpringBoot 使用声明式接口来达到服务间通讯的目的。

但后期由于技术栈发生变化(改为 Go),导致该项目只实现了基本需求后就搁置了。

巧合的时最近内部有部分项目又计划采用 SpringBoot + K8s 开发,于是便着手继续维护;现已经内部迭代了几个版本比较稳定了,也增加了一些实用功能,在此分享给大家。

https://github.com/crossoverJie/feign-plus

首先是新增了一些 features:

  • 更加统一的 API。
  • 统一的请求、响应、异常日志记录。
  • 自定义拦截器。
  • Metric 支持。
  • 异常传递。

示例

结合上面提到的一些特性做一些简单介绍,统一的 API 主要是在使用层面:

在上一个版本中声明接口如下:

@FeignPlusClient(name = "github", url = "${github.url}")
public interface Github {
    @RequestLine("GET /repos/{owner}/{repo}/contributors")
    List<GitHubRes> contributors(@Param("owner") String owner, @Param("repo") String repo);
}

其中的 @RequestLine 等注解都是使用 feign 包所提供的。

这次更新后改为如下方式:

@RequestMapping("/v1/demo")
@FeignPlusClient(name = "demo", url = "${feign.demo.url}", port = "${feign.demo.port}")
public interface DemoApi {
    @GetMapping("/id")
    String sayHello(@RequestParam(value = "id") Long id);

    @GetMapping("/id/{id}")
    String id(@PathVariable(value = "id") Long id);

    @PostMapping("/create")
    Order create(@RequestBody OrderCreateReq req);

    @GetMapping("/query")
    Order query(@SpringQueryMap OrderQueryDTO dto);
}

熟悉的味道,基本都是 Spring 自带的注解,这样在使用上学习成本更低,同时与项目中原本的接口写法保持一致。

@SpringQueryMap(top.crossoverjie.feign.plus.contract.SpringQueryMap) 是由 feign-plus 提供,其实就是从 SpringCloud 中 copy 过来的。

我这里写了两个 demo 来模拟调用:

provider: 作为服务提供者提供了一系列接口供消费方调用,并对外提供了一个 api 模块。


demo:作为服务消费者依赖 provider-api 模块,根据其中声明的接口进行远程调用。

配置文件:

server:
  port: 8181

feign:
  demo:
    url : http://127.0.0.1
    port: 8080

logging:
  level:
    top:
      crossoverjie: debug

management:
  endpoints:
    web:
      base-path: /actuator
      exposure:
        include: '*'
  metrics:
    distribution:
      percentiles:
        all: 0.5,0.75,0.95,0.99
    export:
      prometheus:
        enabled: true
        step: 1m
spring:
  application:
    name: demo

当我们访问 http://127.0.0.1:8181/hello/2 接口时从控制台可以看到调用结果:

日志记录

从上图中可以看出 feign-plus 会用 debug 记录请求/响应结果,如果需要打印出来时需要将该包下的日志级别调整为 debug:

logging:
  level:
    top:
      crossoverjie: debug

由于内置了拦截器,也可以自己继承 top.crossoverjie.feign.plus.log.DefaultLogInterceptor 来实现自己的日志拦截记录,或者其他业务逻辑。

@Component
@Slf4j
public class CustomFeignInterceptor extends DefaultLogInterceptor {
    @Override
    public void request(String target, String url, String body) {
        super.request(target, url, body);
        log.info("request");
    }

    @Override
    public void exception(String target, String url, FeignException feignException) {
        super.exception(target, url, feignException);
    }

    @Override
    public void response(String target, String url, Object response) {
        super.response(target, url, response);
        log.info("response");
    }
}

监控 metric

feign-plus 会自行记录每个接口之间的调用耗时、异常等情况。

访问 http://127.0.0.1:8181/actuator/prometheus 会看到相关埋点信息,通过 feign_call* 的 key 可以自行在 Grafana 配置相关面板,类似于下图:

异常传递

rpc(远程调用)要使用起来真的类似于本地调用,异常传递必不可少。

// provider
	public Order query(OrderQueryDTO dto) {
		log.info("dto = {}", dto);
		if (dto.getId().equals("1")) {
			throw new DemoException("provider test exception");
		}
		return new Order(dto.getId());
	}

// consumer
        try {
            demoApi.query(new OrderQueryDTO(id, "zhangsan"));
        } catch (DemoException e) {
            log.error("feignCall:{}, sourceApp:[{}], sourceStackTrace:{}", e.getMessage(), e.getAppName(), e.getDebugStackTrace(), e);
        }	

比如 provider 中抛出了一个自定义的异常,在 consumer 中可以通过 try/catch 捕获到该异常。

为了在 feign-plus 中实现该功能需要几个步骤:

  1. 自定义一个通用异常。
  2. 服务提供方需要实现一个全局拦截器,当发生异常时统一对外响应数据。
  3. 服务消费方需要自定义一个异常解码器的 bean。

这里我在 provider 中自定义了一个 DemoException

通常这个类应该定义在公司内部的通用包中,这里为了演示方便。

接着定义了一个 HttpStatus 的类用于统一对外响应。

@Data
@AllArgsConstructor
@NoArgsConstructor
public class HttpStatus {
    private String appName;
    private int code;
    private String message;
    private String debugStackTrace;
}

这个也应该放在通用包中。

然后在 provider 中定义全局异常处理:

当出现异常时便会返回一个 http_code=500 的数据:

到这一步又会出现一个引战话题:HTTP 接口返回到底是全部返回 200 然后通过 code 来来判断,还是参考 http_code 进行返回?

这里不做过多讨论,具体可以参考耗子叔的文章:
“一把梭:REST API 全用 POST”

feign-plus 默认采用的 http_code !=200 才会认为发生了异常。

而这里的 http_status 也是参考了 Google 的 api 设计:

具体可以参考这个链接:
https://cloud.google.com/apis/design/errors#propagating_errors

然后定义一个异常解析器:

@Configuration
public class FeignExceptionConfig {
    @Bean
    public FeignErrorDecoder feignExceptionDecoder() {
        return (methodName, response, e) -> {
            HttpStatus status = JSONUtil.toBean(response, HttpStatus.class);
            return new DemoException(status.getAppName(), status.getCode(), status.getMessage(), status.getDebugStackTrace());
        };
    }
}

通常这块代码也是放在基础包中。


这样当服务提供方抛出异常时,消费者便能成功拿到该异常:

实现原理

实现原理其实也比较简单,了解 rpc 原理的话应该会知道,服务提供者返回的异常调用方是不可能接收到的,这和是否由一种语言实现也没关系。

毕竟两个进程之间的栈是完全不同的,不在一台服务器上,甚至都不在一个地区。

所以 provider 抛出异常后,消费者只能拿到一串报文,我们只能根据这段报文解析出其中的异常信息,然后再重新创建一个内部自定义的异常(比如这里的 DemoException),也就是我们自定义异常解析器所干的事情。

下图就是这个异常传递的大致流程:

code message 模式

由于 feign-plus 默认是采用 http_code != 200 的方式来抛出异常的,所以采用 http_code=200, code message 的方式响应数据将不会传递异常,依然会任务是一次正常调用。

不过基于该模式传递异常也是可以实现的,但没法做到统一,比如有些团队习惯 code !=0 表示异常,甚至字段都不是 code;再或者异常信息有些是放在 message 或 msg 字段中。

每个团队、个人习惯都不相同,所以没法抽象出一个标准,因此也就没做相关适配。

这也印证了使用国际标准所带来的好处。

限于篇幅,如果有相关需求的朋友也可以在评论区沟通,实现上会比现在稍微复杂一点点🤏🏻。

总结

项目源码:
https://github.com/crossoverJie/feign-plus

基于2022年云原生这个背景,当然更推荐大家使用 gRPC 来做服务间通信,这样也不需要维护类似于这样的库了。

不过在一些调用第三方接口而对方也没有提供 SDK 时,这个库也有一定用武之地,虽然使用原生 feign 也能达到相同目的,但使用该库可以使得与 Spring 开发体验一致,同时内置了日志、metric 等功能,避免了重复开发。

你的点赞与分享是对我最大的支持

版权声明:本文版权归作者所有,遵循 CC 4.0 BY-SA 许可协议, 转载请注明原文链接
https://www.cnblogs.com/crossoverJie/p/16228335.html

【Java分享客栈】超简洁SpringBoot使用AOP统一日志管理-纯干货干到便秘-编程思维

前言 请问今天您便秘了吗?程序员坐久了真的会便秘哦,如果偶然点进了这篇小干货,就麻烦您喝杯水然后去趟厕所一边用左手托起对准嘘嘘,一边用右手滑动手机看完本篇吧。 实现 本篇AOP统一日志管理写法来源于国外知名开源框架JHipster的AOP日志管理方式 1、引入依赖 <!-- spring aop --&g

MyBatisPlus实现分页和查询操作就这么简单-编程思维

《SpringBoot整合MybatisPlus基本的增删改查,保姆级教程》在这篇文章中,我们详细介绍了分页的具体实现方法。但是,在日常的开发中还需要搜索功能的。下面让我们一起动起手来,实现一下吧。 定义查询字段 定义一个类,存放需要用到的查询字段。如下: package com.didiplus.modules.s

从服务间的一次调用分析整个springcloud的调用过程(一)-编程思维

首先我们知道springcloud是一个微服务框架,按照官方文档的说法,springcloud提供了一些开箱即用的功能:       1 分布式/版本化配置       2 服务的注册与发现       3 路由       4 服务到服务之间调用       5 负载均衡       6 断路器       7 分布

分享一个 SpringCloud Feign 中所埋藏的坑-编程思维

背景 前段时间同事碰到一个问题,需要在 SpringCloud 的 Feign 调用中使用自定义的 URL;通常情况下是没有这个需求的;毕竟都用了 SpringCloud 的了,那服务之间的调用都是走注册中心的,不会需要自定义 URL 的情况。 但也有特殊的,比如我们这里碰到 ToB 场景,需要对每个商户自定义的 U

net core天马行空系列-微服务篇:全声明式http客户端feign快速接入微服务中心nacos-编程思维

1.前言 hi,大家好,我是三合,距离上一篇博客已经过去了整整两年,这两年里,博主通关了《人生》这个游戏里的两大关卡,买房和结婚。最近闲了下来,那么当然要继续写博客了,今天这篇博客的主要内容是,net core/.net6中,如何利用SummerBoot(点我打开详情介绍)中的feign模块快速接入微服务中心nacos

net core天马行空系列:降低net core门槛,数据库操作和http访问仅需写接口,实现类由框架动态生成-编程思维

引文   hi,大家好,我是三合。不知各位有没有想过,如果能把数据库操作和http访问都统一封装成接口(interface)的形式, 然后接口对应的实现类由框架去自动生成,那么必然能大大降低工作量,因为不需要去写很多重复的代码了,还有一个好处是,都是提供接口,我们把原来数据库操作的部分,改成http访问,对于业务层来说