❝
带着问题出发:在微服务中,为了让服务生产者(producer)与服务消费者(consumer)优雅通讯,Feign接口该如何定义?
❞
Feign是什么?
在微服务中,Feign是一个伪RPC组件,它底层对httpClient/okhttp等HTTP通讯框架做了一层代理(屏蔽了底层细节,也不需要关心通讯配置),基于接口注解的方式完成HTTP客户端请求封装。
Feign做了什么?
- 参数解析和装载
- 针对制定的FeignClient,生成动态代理
- 针对FeignClient中的方法描述进行解析
- 组装出一个Request对象,发起请求
Feign的两种用法
比如,订单服务要调用用户服务:
用法一:服务生产者定义Feign
- 接口定义在api模块,添加注解@FeignClient:user-api /xxx/user/feign/IUserFeignClient.java
- 实现类在service模块,添加注解@RestController:user-service /xxx/user/feign/UserFeignClient.java
- 把api模块打成jar包,deploy到maven仓库
- consumer通过maven依赖该jar包
- 注入bean类:IUserFeignClient,调用接口方法userFeignClient.login();
用法二:服务消费者定义Feign
- 接口定义在consumer的service模块,添加注解@FeignClient:order-service /xxx/user/service/UserService.java
- API实现在producer的service模块controller包下:user-service /xxx/user/controller/UserController.java,注意没有在代码层面实现consumer的UserService接口
- 注入bean类:UserService,调用接口方法userService.login();
两种用法的优劣对比
基本上两者优缺点互补,那就说说它们各自的特性吧。
服务生产者定义Feign(面向SPI)
「强约束」面向SPI是强约束的,consumer引入Jar包,在代码层面对接口进行实现。版本一致的情况下可以在编码阶段发现问题进行修改。比如producer修改了Feign接口入参数量,consumer引入最新版本jar包能立马检查出来。SPI是语言相关的,因为jar包只能在java项目引入。
「避免重复定义」producer定义好Feign接口后,consumer引入jar包后就知道怎么使用,无须在多个consumer重复定义入参出参。
服务消费者定义Feign(面向API)
「弱约束」面向API是弱约束的,producer和consumer之间通过开发人员的约定来约束。上面提到,FeignClient默认代理HttpClient做为底层实现,HttpClient就是面向API编程的。两者区别在于HttpClient需要写大量实现代码,而FeignClient只定义接口。而没有了jar包依赖,consumer就不一定是java项目了,因为API是语言无关的。
「简化开发流程」面向SPI开发模式需要在producer定义feign,然后打成jar,deploy到maven仓库,再引入到consumer工程。。。后续迭代需求得频繁修改依赖包版本,整条链路非常长。而面向API开发模式简单直接,让开发人员聚焦业务代码,这也是弱约束带来的便利。
「裁剪灵活」假设producer暴露100个接口,consumer只需要调用其中20个,这种情况下对接口裁剪就非常灵活了。不仅能对接口裁剪,还能对接口参数、返回值进行裁剪,只保留当前业务需要的入参出参,隔离性非常好。
「影响范围小」producer作为下层系统,只需要把当前系统的功能实现即可,不用受到上层系统约束。如果要配合consumer提供Feign接口,需要改造producer,把原service层的VO迁到api层变成DTO。我们都知道,DTO不该使用充血模型,但VO没有这个约束,迁层就要考虑把VO依赖的类都一起迁,对于producer就会有大量的代码改造。开发一个需求还需要改动大量下层系统的代码,无疑增加了许多风险。
使用场景
从约束性、隔离性、灵活性、影响范围等整体考虑,个人更推荐使用第二种方式,也就是服务消费者定义Feign接口,而且定义在service层的service包下即可,无须跟普通的Service类区别对待。当然,文中只列举了部分特性,实际生产还要结合大家各自项目的特点来选择。
心得体会
学会从不同的角度去思考问题本身,不要让过往的经验成为通向远方的绊脚石。
「关注我,一起学习探讨生产落地方案~」