feign

makes writing java http clients easier

Feign supports pluggable encoders and decoders. Spring Cloud adds support for Spring MVC annotations and for using the same HttpMessageConverters used by default in Spring Web. Spring Cloud integrates Ribbon and Eureka to provide a load balanced http client when using Feign.

springCloud feign是对 ribbon 和 hytrix 的整合。feign 是面向接口编程,ribbon + eureka 是面向服务编程;

1.why use feign ?

简化服务的调用

如果没有 feign,我们在调用 外部的服务会使用 restTemplate

demo1-without-feign:

  1. @RestController
  2. public class DeptController_Consumer{
  3. private static final String REST_URL_PREFIX = "http://MICROSERVICECLOUD-DEPT";
  4. @Autowired
  5. private RestTemplate restTemplate;
  6. @RequestMapping(value = "/consumer/dept/add")
  7. public boolean add(Dept dept){
  8. return restTemplate.postForObject(REST_URL_PREFIX + "/dept/add", dept, Boolean.class);
  9. }
  10. @RequestMapping(value = "/consumer/dept/get/{id}")
  11. public Dept get(@PathVariable("id") Long id){
  12. return restTemplate.getForObject(REST_URL_PREFIX + "/dept/get/" + id, Dept.class);
  13. }
  14. @SuppressWarnings("unchecked")
  15. @RequestMapping(value = "/consumer/dept/list")
  16. public List<Dept> list(){
  17. return restTemplate.getForObject(REST_URL_PREFIX + "/dept/list", List.class);
  18. }
  19. }

假设使用 feign
首先使用feignClient声明 rpc 接口

demo2-use-feign:

  1. //该接口对应于应用id为MICROSERVICECLOUD-DEPT的微服务
  2. @FeignClient(value = "MICROSERVICECLOUD-DEPT")
  3. public interface DeptClientService
  4. {
  5. @RequestMapping(value = "/dept/get/{id}",method = RequestMethod.GET)
  6. public Dept get(@PathVariable("id") long id);
  7. @RequestMapping(value = "/dept/list",method = RequestMethod.GET)
  8. public List<Dept> list();
  9. @RequestMapping(value = "/dept/add",method = RequestMethod.POST)
  10. public boolean add(Dept dept);

调用,面向接口编程的方式调用:

  1. @RestController
  2. public class DeptController_Consumer{
  3. @Autowired
  4. private DeptClientService service;
  5. @RequestMapping(value = "/consumer/dept/get/{id}")
  6. public Dept get(@PathVariable("id") Long id){
  7. return this.service.get(id);
  8. }
  9. @RequestMapping(value = "/consumer/dept/list")
  10. public List<Dept> list(){
  11. return this.service.list();
  12. }
  13. @RequestMapping(value = "/consumer/dept/add")
  14. public Object add(Dept dept){
  15. return this.service.add(dept);
  16. }
  17. }

2.Feign 的配置

see FeignClientProperties 类

(1)Spring Cloud Feign支持对请求和响应进行GZIP压缩,以提高通信效率,配置方式如下:

配置请求GZIP压缩
feign.compression.request.enabled=true
# 配置响应GZIP压缩
feign.compression.response.enabled=true
# 配置压缩支持的MIME TYPE
feign.compression.request.mime-types=text/xml,application/xml,application/json
# 配置压缩数据大小的下限
feign.compression.request.min-request-size=2048
配置压缩数据的下限是因为数据太小,压缩反而性能不好,压缩是一个比较耗费性能的动作。

(2)Feign为每一个FeignClient都提供了一个feign.Logger实例,我们可以在配置中开启日志,开启方式很简单,分两步:

第一步:application.properties中配置日志输出
application.properties中配置如下内容,表示设置日志输出级别:
# 开启日志 格式为logging.level.+Feign客户端路径
logging.level.org.sang.HelloService=debug

第二步:入口类中配置日志Bean
入口类中配置日志Bean,如下:

@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}

3.feign 原理 链接

image.png

EnableFeignClients 注解作用:

1、扫描EnableFeignClients注解上的配置信息,注册默认的配置类,这个配置类是对所有feignclient的都是生效的,即为全局的配置。

2、扫描带有 @FeignClient注解的接口,并注册配置类(此时的配置类针对当前feignclient生效)和FeignClientFactoryBean,此bean实现了FactoryBean接口,我们知道spring有两种类型的bean对象,一种是普通的 bean,另一种则是工厂bean(FactoryBean),它返回的其实是getObject方法返回的对象(更多关于FactoryBean的相关信息请查看spring官方文档)。getObject方法就是集成原生feign的核心方法,当spring注入feignclient接口时,getObject方法会被调用,得到接口的代理类。

FeignClientFactoryBean 的 getObject() 方法:

  1. @Override
  2. public Object getObject() throws Exception {
  3. FeignContext context = applicationContext.getBean(FeignContext.class);
  4. Feign.Builder builder = feign(context);
  5. if (!StringUtils.hasText(this.url)) {
  6. String url;
  7. if (!this.name.startsWith("http")) {
  8. url = "http://" + this.name;
  9. }
  10. else {
  11. url = this.name;
  12. }
  13. url += cleanPath();
  14. return loadBalance(builder, context, new HardCodedTarget<>(this.type,
  15. this.name, url));
  16. }
  17. if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
  18. this.url = "http://" + this.url;
  19. }
  20. String url = this.url + cleanPath();
  21. Client client = getOptional(context, Client.class);
  22. if (client != null) {
  23. if (client instanceof LoadBalancerFeignClient) {
  24. // not load balancing because we have a url,
  25. // but ribbon is on the classpath, so unwrap
  26. client = ((LoadBalancerFeignClient)client).getDelegate();
  27. }
  28. builder.client(client);
  29. }
  30. Targeter targeter = get(context, Targeter.class);
  31. return targeter.target(this, builder, context, new HardCodedTarget<>(
  32. this.type, this.name, url));
  33. }

FeignClient注解:
每一个 FeignClient 代表一个 http 客户端,定义的每一个方法对应一个接口;

4.feign 生产使用

feign 使用时的痛点是存在 重复写 controller 代码的情况,A 服务调用 B 服务的 rpc ,B 服务声明了一套 restful 接口。A 服务调用时,使用feignClient 需要将 A 服务的接口重复声明。

我们公司的解决方案:
每个服务一方面要处理前端的 rest 请求,一方面要处理其他服务的rpc 请求。每个项目有一个rpc 模块,这个模块只依赖 pojo 和一些基础框架包。其他服务如果要使用这个服务的rpc ,直接依赖这个模块即可使用。

在使用 feign 的时候,我们只需要通过feignClient 把 rpc 服务声明出来。调用统一对 rpc 接口调用进行拦截,拦截后直接通过注解拿到 rpc 服务的入参、方法名、服务名等我们需要的信息,路由找到 spring 容器对应的 bean 和方法,调用执行。

声明的一个rpc接口:

  1. @FeignClient(name = "E6-MS-TMS-BUSI-WEB", primary = false)
  2. public interface WaybillEquipRpcService {
  3. /**
  4. * 根据设备id查询运单的状态信息
  5. *
  6. * @param waybillEquipVO 入参
  7. * @return
  8. */
  9. @GetMapping("/rpc/com.e6yun.project.tms.busi.rpc.service.WaybillEquipRpcService/findWaybillByEquipIds")
  10. E6Wrapper<List<WaybillEquipResult>> findWaybillByEquipIds(@RequestBody WaybillEquipVO waybillEquipVO);
  11. }

调用通过 rpc filter 拦截处理,拦截处理流程:
rpcfilter.png
其中比较复杂的辑是请求参数的构造,请求时可能通过 RequestParam or ResponseBody 传递值、对象、数组等参数,需要分情况进行解析构造。

5.Feign Ribbon Hystrix 三者关系

在 Spring Cloud 微服务体系下,微服务之间的互相调用可以通过Feign进行声明式调用,在这个服务调用过程中Feign会通过Ribbon从服务注册中心获取目标微服务的服务器地址列表,之后在网络请求的过程中Ribbon就会将请求以负载均衡的方式调用到微服务的不同实例上,从而实现Spring Cloud微服务架构中最为关键的功能即服务发现及客户端负载均衡调用。

1、Ribbon

Ribbon是用来做负载均衡的,针对他的配置主要有:

  1. 连接超时时间:ribbon.ConnectTimeout=600
  2. 请求超时时间:ribbon.ReadTimeout=6000
  3. 对所有操作都进行重试:ribbon.OkToRetryOnAllOperations=true
  4. 切换实例重试次数:ribbon.MaxAutoRetriesNextServer=2
  5. 对当前实例重试次数:ribbon.MaxAutoRetries=1

上面的配置,会对失败进行重试,对当前实例重试一次,仍然失败会切换实例重试,最多切换两次。
如果我想针对不同的服务配置不同的连接超时和读取超时,那么我们可以在属性的前面加上服务的名字,例如:

  1. # 设置针对hello-service服务的连接超时时间
  2. hello-service.ribbon.ConnectTimeout=600
  3. # 设置针对hello-service服务的读取超时时间
  4. hello-service.ribbon.ReadTimeout=6000
  5. # 设置针对hello-service服务所有操作请求都进行重试
  6. hello-service.ribbon.OkToRetryOnAllOperations=true
  7. # 设置针对hello-service服务切换实例的重试次数
  8. hello-service.ribbon.MaxAutoRetriesNextServer=2
  9. # 设置针对hello-service服务的当前实例的重试次数
  10. hello-service.ribbon.MaxAutoRetries=1

2、Hystrix

Spring Cloud Hystrix中实现了线程隔离、断路器等一系列的服务保护功能。它也是基于Netflix的开源框架 Hystrix实现的,该框架目标在于通过控制那些访问远程系统、服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。Hystrix具备了服务降级、服务熔断、线程隔离、请求缓存、请求合并以及服务监控等强大功能。

  1. 配置熔断超时时间:hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=10000
  2. 关闭hystrix功能(不要和上面一起使用):feign.hystrix.enabled=false
  3. 关闭熔断功能:hystrix.command.default.execution.timeout.enabled=false

这种配置也是全局配置,如果我们想针对某一个接口配置,比如/hello接口,那么可以按照下面这种写法,如下:
# 设置熔断超时时间
hystrix.command.hello.execution.isolation.thread.timeoutInMilliseconds=10000
# 关闭熔断功能
hystrix.command.hello.execution.timeout.enabled=false