微服务服务间调用组件Feign使用介绍、原理、优化技巧

2023-10-17 14:16:38 浏览数 (1)

微服务服务间调用组件Feign使用介绍、原理、优化技巧

Feign是一个声明式的Web Service客户端。它让微服务之间的调用变得更简单。Feign具有可插拔式的注解支持,包括Feign 注解和JAX-RS注解。Feign还支持可插拔的编码器和解码器。Spring Cloud增加了对Spring MVC注解的支持,并且也支持Spring WebFlux。

Feign可以与Eureka和Ribbon组合使用以支持负载均衡。

内容介绍

本文的主要内容主要内容包括:

  1. Feign的基本概念、原理与使用。Feign采用声明式的接口,自动拼接URL、添加参数等工作,简化HTTP客户端的开发。
  2. Feign的高级特性,如日志、压缩、重试、监听器、故障处理等。这些特性使Feign成为一个强大的客户端。
  3. Feign的性能优化,包括连接池、HTTP客户端更换、超时设置、GZIP压缩等。这些措施可以大幅提高Feign的性能。
  4. Feign的高可用方案。通过与服务注册中心、熔断器、链路追踪等组件结合,保障Feign服务的高可用。
  5. Feign的最佳实践。通过超时设置、降级处理、日志级别控制等最佳实践,合理使用Feign。
  6. Feign的源码分析。解析Feign的核心组件如Feign类、Contract接口、Client接口、Logger等,理解Feign的内部机制。
  7. Feign与OpenFeign的区别。OpenFeign在Feign的基础上做了大量增强,更贴近Spring Cloud体系,所以Spring Cloud较偏向OpenFeign。
  8. Feign与RestTemplate的对比。在Spring Cloud体系下,Feign比RestTemplate更适合作为HTTP客户端。
  9. Feign常见问题解答。对Feign使用中常见的问题如调用404、超时、注解不生效等进行解答。

Feign的使用

  1. 添加Feign依赖
代码语言:javascript复制
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
  1. 创建接口并添加@FeignClient注解
代码语言:javascript复制
//定义一个Feign接口
@FeignClient("eureka-client") 
public interface ComputeClient {
   @GetMapping("/add")
   int add(@RequestParam(value = "a") int a, @RequestParam(value = "b") int b);
}

@FeignClient("eureka-client")指定调用的服务名称,这里调用注册在Eureka中的eureka-client服务。

  1. @FeignClient中的接口使用Spring MVC注解来声明Web Service请求
  2. 在主启动类添加@EnableFeignClients注解开启Feign的功能
代码语言:javascript复制
@SpringBootApplication
@EnableFeignClients 
public class Application {
   public static void main(String[] args) {
      SpringApplication.run(Application.class, args);
   }
}
  1. 调用Feign接口的方法即可完成服务调用
代码语言:javascript复制
@Autowired
private ComputeClient computeClient;

public int add(int a, int b) {
   return computeClient.add(a, b);  
}

调用computeClient.add(a, b)便可完成对eureka-client服务的调用。

Feign内部集成了Ribbon,所以以上的例子也具有客户端负载均衡的功能。

Feign的执行流程概述

Feign在调用其他服务时,会根据注解中的url进行请求转发。它的工作原理是:

  1. 根据@FeignClient的value值找到服务,如果配置了服务注册中心,还会根据服务名获取服务实例列表。
  2. 构造请求参数,根据方法上的注解(如@PathVariable、@RequestParam、@RequestBody等)确定请求参数。
  3. 根据注解(@GetMapping、@PostMapping、@DeleteMapping等)确定请求方法和路径。
  4. 发起请求调用其他服务,获取结果。
  5. 封装结果,返回给调用方。

Feign的定制化

Feign提供了多种定制化手段:

  1. 日志级别:Feign支持日志记录,我们可以设置日志级别以查看Feign的调用详情。
代码语言:javascript复制
feign:
  client: 
    config:
      default: 
        loggerLevel: FULL 

设置为ALL,则会展示完整的请求、响应日志详情。

  1. 编码器和解码器:Feign默认使用JSON进行编码和解码,我们可以设置自定义的编解码器。
代码语言:javascript复制
@Component
public class CustomEncoder implements Encoder {
    //...
}

@Bean
public Encoder feignEncoder() {
    return new CustomEncoder();
}
  1. Feign拦截器:我们可以通过实现RequestInterceptorResponseInterceptor接口来定制拦截请求和响应。
代码语言:javascript复制
@Bean 
public RequestInterceptor requestInterceptor() { 
  return template -> {
     //自定义逻辑...
     template.header("Content-Type", "application/json");
   }; 
}
  1. Feign接口的继承:我们的Feign接口可以继承另一个接口,这样我们可以重写父接口的方法来实现特定的定制需求。
代码语言:javascript复制
@FeignClient("some-service")
public interface SomeServiceClient extends SomeService {
    @Override
    @GetMapping("/{path}")
    String doSomething(@PathVariable("path") String path);
}
  1. Contract契约:Spring Cloud Feign默认使用SpringMvcContract,我们可以实现自定义契约来控制Feign的一些行为,例如路径、请求方法等。

Feign的运维实践

在实际项目中使用Feign也会遇到一些问题,这里给出一些运维方面的实践建议:

  1. Feign默认使用JDK原生的URLConnection发送HTTP请求,我们可以选择更高性能的HTTP客户端,如Apache HTTP Client、OkHttp等。
代码语言:javascript复制
feign:
  client: 
    config:
      default: 
        httpClient:
          enabled: true
          connectionTimeout: 5000
          followRedirects: true 
        loggerLevel: full
        okhttp:
          enabled: true # 开启OkHttp
  1. 超时设置:Feign客户端的超时设置包括:
  • connectTimeout:连接超时,默认10秒。
  • readTimeout:读取超时,默认60秒。

我们可以根据服务调用的耗时情况进行设置,避免超时。

关于feign默认配置参数可以参看FeignClientConfiguration源码类的字段属性。

  1. 服务降级:当服务调用失败或超时,我们需要有降级策略,避免影响整体系统。我们可以为Feign接口增加fallback指定降级实现类。
代码语言:javascript复制
@FeignClient(value = "compute-service", fallback = ComputeClientFallback.class)
public interface ComputeClient {
    //..
}

public class ComputeClientFallback implements ComputeClient {
    public int add(int a, int b) {
        return -1; 
    }
} 

当compute-service服务调用失败时,会调用ComputeClientFallback里的方法作为降级返回。

  1. 重试:我们可以为Feign增加重试设置,在服务调用失败时进行重试,避免故障扩散。
代码语言:javascript复制
feign:
  client:
    retry:
      enabled: true # 开启重试
      maxAttempts: 5 # 最大重试次数
      period: 5000    # 重试间隔时间

参看源码类:feign.Retryer.Default

  1. 负载均衡:Feign默认集成Ribbon进行客户端负载均衡,我们可以设置一些Ribbon相关参数进行配置,如连接超时、重试等。
代码语言:javascript复制
ribbon: 
  ConnectTimeout: 1000 # 连接超时时间
  ReadTimeout: 3000 # 读取超时时间
  OkToRetryOnAllOperations: true # 是否对所有的请求方法都重试
  MaxAutoRetriesNextServer: 1 # 切换实例后的最大重试次数

Feign常见问题排查

  1. Feign调用返回404

这通常是因为Feign调用的url不正确导致的。我们可以:

  • 确认@FeignClient中的value值正确,对应服务注册中心中的服务名。
  • 确认url中的路径正确,可以打印Feign的日志查看实际请求路径。
  • 如果服务实例很多,还需要确认Ribbon的负载均衡策略是否导致请求到错误实例。
  1. Feign调用返回超时

这种情况通常有两种原因:

  • 服务提供方处理时间过长,超出Feign的读超时时间。我们需要适当增加Feign的readTimeout
  • 服务调用链路过长,中间某个服务处理时间过长,导致Feign总调用时间超出。这种情况下需要检查各个链路的处理时间,进行优化。
  1. 服务降级没有生效

这种情况主要有两种原因:

  • @FeignClient没有指定fallback属性,没有降级实现类。这种情况下需要添加降级类。
  • 降级类没有在Spring容器中,没有被扫描到。这种情况需要确认降级类是否被@Component注解,或者增加@FeignClientfallbackFactory指定工厂类来生成降级类实例。
  1. Feign日志级别设置无效

这是因为Feign的日志级别设置有两种方式,且优先级不同:

  • 通过logging.level.xx=DEBUG设置Feign的日志级别,优先级高。
  • 通过feign.Client.config.loggerLevel=FULL设置,优先级低。

所以如果两种方式同时设置,logging.level.xx=DEBUG的设置会覆盖feign.Client.config.loggerLevel=FULL。我们只需要设置一种方式即可,且优先使用logging.level.xx=DEBUG设置Feign日志级别。

  1. 自定义Client不生效?
  • 检查feign.client.name是否设置,且与ClientName一致。
  • 检查@FeignClient(“name”)的name属性是否正确,对应feign.client.name。
  • 检查Client实现类是否加了@Component注解,是否已正确注入Spring容器。
  1. 想使用Spring MVC注解,但不生效?
  • 确认是否使用的OpenFeign,因为OpenFeign才支持Spring MVC注解。
  • 检查OpenFeign的版本是否过低,低版本OpenFeign的注解支持不完整。
  • 检查方法与参数上是否都有相应的注解,某个注解缺失会导致不生效。
  1. 如何给Feign的Bean添加拦截器、AOP等?
  • 由于FeignClient通过JDK代理产生,无法直接为其Bean添加拦截器、AOP等。
  • 可以通过在FeignClient定义的接口上添加拦截器注解的方式给FeignClient添加拦截器。
  • 也可以在FeignClient的实现类上(默认是Feign.Default)添加AOP等。
  • 定义自己的Feign拦截器@Component并配置到feign.client.config.defaultInterceptors。
  1. Feign如何实现文件上传?
  • 文件上传需要使用多部分表单,Feign默认的表单编码器FormEncoder不支持。
  • 需要添加对multipart/form-data的支持,需要引入feign-form的依赖。
  • 并在feign.clients.default.encoders添加MULTIPART_FORM_ENCODER。
  • 然后就可以定义接收MultipartFile的Feign接口进行文件上传了。
  1. Feign可以调用HTTPS接口吗?

可以。Feign默认的Client就支持HTTPS,只需要在@FeignClient的url指定https协议和证书相关配置即可。也可以采用ApacheHttpClient替换默认Client,实现更复杂的HTTPS调用方案。

  1. Feign支持Form表单提交吗?

支持。Feign可以通过SpringMVC的@RequestParam注解传递Form表单值,也支持请求类型application/x-www-form-urlencoded。在Feign Client的方法上使用@RequestMapping(method = RequestMethod.POST)和@RequestParam即可完成Form提交。

这些就是使用Feign过程中常见的几个问题以及解决思路,通过理解Feign的原理加上踩坑经验,可以更好地运维Feign,使其应用更加稳定。

Feign性能优化

作为一个服务调用组件,Feign的性能也是我们需要考虑的点。这里给出一些优化Feign性能的建议:

  1. 选择更高性能的HTTP客户端:如前所述,替换Feign默认的URLConnection,选择Apache HTTP Client、OkHttp等更高性能的HTTP客户端。
  2. 连接池优化:
  • 合理设置连接池大小,不宜太大也不宜太小。
  • 选择支持连接池复用的HTTP客户端,如OkHttp。
  • Ribbon也有连接池设置,与Feign的HTTP客户端配合优化。
  1. 超时优化:
  • 合理设置Feign的连接超时和读取超时。连接超时不宜太长,读取超时根据服务调用耗时设置。
  • Ribbon也有相应超时设置,与Feign协同优化。
  1. 服务线程池优化:

Feign使用JDK默认线程池,我们可以进行定制:

代码语言:javascript复制
@Bean
public ExecutorService feignExecutorService(){
    // 设置核心线程数,最大线程数,队列大小,释放资源时的延迟时间
    return new ThreadPoolExecutor(xx, yy, zz, TimeUnit.SECONDS, 
        new LinkedBlockingQueue<Runnable>(zzz), 
        new NamedThreadFactory("feign"));
}
  1. 重试优化:
  • 合理设置Feign的重试次数和时间间隔。
  • 区分对不同服务的重试策略,防止重试过度导致系统资源消耗过大。
  1. 日志级别优化:

Feign的日志虽然可以用于调试,但是日志级别过详细也会对性能产生影响,所以可以根据环境设置不同日志级别:

  • 开发环境:Logger.Level.FULL,方便开发调试。
  • 测试环境:Logger.Level.BASIC,了解大体调用流程。
  • 生产环境:Logger.Level.NONE,关闭日志提高性能。
  1. GZIP压缩:

Feign支持对请求和响应进行GZIP压缩,以提高网络传输性能。我们可以开启:

代码语言:javascript复制
feign:
  compression: 
    request:
      enabled: true # 开启请求GZIP压缩
    response:
      enabled: true # 开启响应GZIP压缩

以上这些优化方案可以很好地提高Feign的性能,使其更加适合生产环境。但优化总是有代价的,需要根据系统的吞吐量、调用链路等实际情况进行权衡和调优。

Feign 性能测试

对Feign客户端进行、性能和可靠性测试也很重要,这里给出一些测试建议:

  1. 单元测试:

我们可以为Feign接口编写单元测试,调用接口并校验响应结果,保证接口逻辑正确。

代码语言:javascript复制
@RunWith(SpringRunner.class) 
@SpringBootTest
public class ComputeClientTest {

    @Autowired
    private ComputeClient computeClient;

    @Test
    public void testAdd() {
        int result = computeClient.add(1, 2);
        assertEquals(3, result);
    }
} 
  1. 集成测试:

启动全部服务,然后调用Feign客户端进行端到端测试,校验整体流程和结果正确性。

  1. 压力测试:

使用工具如JMeter对Feign客户端进行大并发调用,测试Feign在高负载下的性能表现是否达标。主要关注:

  • QPS:每秒查询率,需要达到系统预期的QPS。
  • RT:响应时间,需要控制在可接受范围内,不宜太长。
  • 错误率:服务调用错误率需要控制在可接受范围内。
  • 资源消耗:CPU、内存、网络bandwidth等资源消耗是否在可控范围。
  1. 稳定性测试:
  • 长时间高负载测试:模拟高访问量场景,长时间大并发访问Feign客户端,测试其稳定性。
  • 服务故障测试:模拟服务提供方部分实例故障,测试Feign的容灾性和降级是否生效。
  • 网络故障测试:模拟网络抖动、高延迟、短时网络中断等情况,测试Feign的稳定性。

这些测试可以有效检验Feign客户端的健壮性,保证其在复杂环境下仍能稳定运行。我们还可以根据系统的重要程度和流量规模,制定不同的性能指标和可用性指标,对Feign进行严苛的SLA考核。

套用一些流行框架如:

  • Spring Cloud Contract用于微服务contract测试
  • Resilience4j进行熔断、限流、重试等过载防护
  • Hystrix进行熔断和线程隔离

这可以更好保证Feign的高可用。综上,测试和过载防护是保证Feign稳定性的重要一环,希望通过这些测试实践和框架应用,可以让Feign在复杂环境下表现更加可靠。

Feign高可用方案

对于一个微服务系统来说,服务调用是非常重要的一个环节,Feign作为一个重要的调用组件,其高可用性直接影响整个系统的高可用。这里给出一些提高Feign高可用的方案:

  1. 服务发现与注册:

Feign常与Eureka、Consul等服务注册中心协同使用,这些服务注册中心本身也支持集群部署,可以提高Feign服务调用的高可用性。

  1. Ribbon负载均衡:

Feign内置的Ribbon组件,我们可以设置多个服务实例,并选择合适的负载均衡策略,避免单点故障。

  1. Hystrix容错保护:

Hystrix可以进行线程隔离、熔断等策略保护Feign,避免在高并发下服务被过载。熔断机制可以快速失败,避免操作阻塞。

  1. Http客户端连接池:

使用连接池,如Apache HTTP Client、OKHttp等,可以进行连接复用,避免每次调用都建立新的连接。并且这些客户端本身也支持高可用配置,如设置多个Url地址。

  1. 超时与重试机制:

合理设置Feign的连接超时、读取超时时间,可以快速发现服务问题并快速失败,避免资源占用过长时间。配合重试机制,在一定次数后快速返回,防止长时间的不可用服务导致系统不可用。

  1. 熔断与限流:

除Hystrix外,可以使用Resilience4J等开源组件进行更加全面和强大的熔断、限流、重试。限流可以防止突发高流量导致系统不可用。

  1. 服务跟踪:

使用组件如Zipkin进行服务调用链路跟踪,一旦出现高延迟或不可用服务,可以快速定位问题所在。

  1. 降级策略:

为Feign接口指定Fallback以及合适的降级策略,在服务不可用时提供备选方案,避免不可用服务导致依赖服务完全不可用。

  1. 日志记录与监控:

合理配置Feign日志级别,并结合ELK等日志收集工具进行监控。一旦服务有异常状况,可以快速发现并定位问题。

综上,Feign高可用需要多方面的保障和运维,需要与服务注册中心、熔断限流组件、链路跟踪组件、监控日志组件等协同配合,共同提高Feign和依赖其的整个微服务系统的高可用性。

Feign源码分析

理解Feign的源码,有助于我们更深入理解其工作原理,从而合理使用和定制Feign。这里简要分析Feign的源码:

  1. Feign类:Feign类是Feign的入口,主要工作是:
  • 解析Feign注解,获取接口方法与url映射关系,请求类型等信息。
  • 构建ReflectiveFeign类,封装接口方法与请求细节的映射。
  • 构建Feign.Builder,用于创建Feign实例。
  • 创建Logger用于记录Feign日志。
  • 绑定Contract契约,默认是SpringMvcContract。
  1. ReflectiveFeign类:
  • 维护Feign接口方法与请求模板(RequestTemplate)的映射。
  • 调用接口方法时,查找请求模板,使用请求参数构造URL,发起HTTP请求。
  • 将响应结果转换为接口方法 defined 返回类型,返回给调用方。
  1. Contract接口与SpringMvcContract:
  • Contract接口定义了诸如生成请求模板、构造参数值到模板变量等规则。
  • SpringMvcContract实现了Spring MVC注解方式,将方法、参数注解转化为请求模板变量与值。
  1. Client接口与Client.Default实现:
  • Client接口定义了发起HTTP请求的方法。Feign使用构建器模式,允许我们选择不同Client实现来发送请求。
  • Client.Default实现了使用JDK原生URLConnection发送HTTP请求。我们可以实现自定义Client,如使用OKHttp等。
  1. Encoder和Decoder:
  • Encoder负责对请求参数进行编码,默认使用SpringEncoder对参数进行JSON编码。
  • Decoder负责对响应结果进行解码,默认使用SpringDecoder对JSON响应进行解码。
  1. Logger和LoggingInterceptor:
  • Logger定义了记录Feign日志的规范,有4个级别:NONE、BASIC、HEADERS、FULL。
  • LoggingInterceptor拦截Feign请求与响应,将详细信息记录为Feign日志, logfile可指定日志记录位置。
  1. Retryer接口:定义重试策略,Feign内置支持backoff、exponential backoff重试策略。我们也可以自定义Retryer实现。

这些是Feign的主要组成部分, Feign的高效与灵活正是因为这些组件采用接口设计,允许我们灵活选择与替换。理解这些组件的作用与关系,有助于我们使用Feign的源码进行定制化开发。

Feign与OpenFeign区别

Feign和OpenFeign都是Netflix开源的声明式HTTP客户端,但有一定的区别:

  1. 源起:

Feign起源于Netflix,后捐给Spring Cloud并成为Spring Cloud Netflix的一部分。OpenFeign是Spring Cloud对Feign进行增强,成为Spring Cloud的组成部分,目的是提供Spring MVC注解的支持、 wrongly监听机制等新功能。

  1. 注解支持:

Feign仅支持JAX-RS注解,对Spring MVC注解不支持。OpenFeign支持Spring MVC注解,更贴近Spring开发体验,支持内容协商、验证等机制。

  1. 编解码器:

Feign仅支持QueryStringEncoder、FormEncoder和JsonEncoder三种编解码器。OpenFeign内置了SpringEncoder和StringDecoder,支持更丰富的对象与HTTP请求的编解码,如集合、 maps等。

  1. contract:

Feign仅支持接口方法签名与url的映射,请求细节无法定制。OpenFeign支持SpringMvcContract,可以定制请求方法、参数绑定等细节。

  1. 拦截器:

Feign不支持请求与响应拦截器。OpenFeign支持ClientRequestInterceptor和ClientResponseInterceptor,允许拦截并自定义Feign的请求与响应。

  1. 监听支持:

Feign不支持对指标与事件的监听。OpenFeign支持监听连接池大小、请求计数、处理时间等指标,以及连接成功、失败等事件。方便监控Feign运行状态。

  1. 对Cloud致敬:

Feign是Netflix的开源项目,不依赖Spring Cloud。OpenFeign致敬Spring Cloud,与Spring Cloud深度整合,依赖Spring Cloud Context与Spring Boot。

可见,OpenFeign在Feign的基础上进行了大量增强,更加贴近Spring Cloud体系,功能更加丰富完善,维护也更加活跃。所以在Spring Cloud微服务架构下,OpenFeign往往是一个更好的选择。但Feign本身也是一个成熟可靠的HTTP客户端,如果我们对Spring体系不太依赖,直接使用Feign同样是一个好选择。总之,这 still需要根据我们的技术选型和需求来权衡。

Feign与RestTemplate对比

Feign和RestTemplate都是比较常用的HTTP客户端,但有以下主要区别:

  1. 使用方式:

RestTemplate的使用方式比较接近传统HTTP API,需要手动构建URL,拼接参数等:

代码语言:javascript复制
RestTemplate restTemplate = new RestTemplate();
String url = "http://example.com/users?name={name}";
String result = restTemplate.getForObject(url, String.class, "Jack");

Feign的使用方式是声明式的,只需要定义接口并注解,更加面向接口:

代码语言:javascript复制
@FeignClient("example.com")
public interface UserClient {
    @GetMapping("/users")
    String getUser(@RequestParam("name") String name);
}
  1. 可维护性:

Feign的接口更加抽象,屏蔽了实现细节,有利于后续维护和替换。RestTemplate的调用方式过于具体,不利于变更。

  1. 可扩展性:

Feign有更加丰富的扩展点,如支持多种编解码器、拦截器、Client等。RestTemplate扩展比较困难。

  1. 支持功能:

Feign内置了负载均衡、重试、监听等机制。而RestTemplate需要与其他组件配合使用才能完成,如Ribbon等。

  1. 与Spring的整合:

Feign是Spring Cloud的一部分,与Spring框架深度整合。而RestTemplate需要自己进行与Spring的整合。

Feign与其它组件的关系、区别

  1. Feign与Ribbon的区别和关系:
  • Feign和Ribbon都是Netflix开源的组件,用于微服务调用。
  • Feign是一个声明式的HTTP客户端,主要负责HTTP请求的发送。
  • Ribbon是一个负载均衡器,主要用于客户端的负载均衡。
  • Feign内部使用Ribbon进行负载均衡,所以当使用Feign时,不需要再单独使用Ribbon。
  • 但我们仍然可以在Feign中配置或替换Ribbon,实现自定义的负载均衡策略。
  1. Feign与Hystrix的区别和关系:
  • Feign和Hystrix也都是Netflix开源的组件,用于微服务体系。
  • Feign是一个HTTP客户端,主要用来发送HTTP请求。
  • Hystrix是一个容错管理工具,主要用来提高微服务的可靠性与容错能力。
  • Feign可以与Hystrix结合,使用Hystrix的容错机制来保护Feign的服务调用。
  • 我们只需要在Feign客户端上添加@FeignClient中的fallback指定容错方法即可启用Hystrix容错。
  • 也可以手动在fallback方法中使用HystrixCommand进行服务降级等操作。
  1. Feign与Zuul的区别和关系:
  • Feign是Netflix的HTTP客户端,用于服务调用。Zuul是Netflix的网关,用于路由转发。
  • 一个微服务体系通常会同时使用Feign和Zuul这两个组件。
  • Feign用于服务内部的调用,Zuul主要用于外部访问系统的统一入口。
  • Zuul可以与Feign结合,将外部访问路由到内部服务,而这些内部服务之间可以使用Feign相互调用。
  • 所以Zuul和Feign虽然功能不同,但可以良好配合,共同支撑起一个微服务架构。

这些内容的理解可以让我们对Feign与其他微服务组件有一个更全面的认知,知晓它们之间的关系与区别,这有助于我们在设计微服务方案时做出更好的选择与组合。Feign虽然功能十分强大,但它通常并不孤立存在,而是与其他组件配合使用,发挥更大的价值。

Feign的高级内容、自定义扩展实践

  1. Feign的继承支持:

Feign天生支持接口的继承,子接口自动继承父接口的方法与注解。这让我们可以设计出层次清晰的Feign接口,如:

代码语言:javascript复制
public interface HelloService {
  @RequestMapping(method = RequestMethod.GET, value = "/hello")
  String sayHello(); 
}

public interface HelloServiceExtended extends HelloService {
  @RequestMapping(method = RequestMethod.GET, value = "/hi")
  String sayHi();
}

然后在@FeignClient中指定HelloServiceExtended接口,那么FeignClient将同时具有sayHello()sayHi()两个方法。

  1. Feign的请求模板:

有时候我们的Feign方法中存在大量重复的注解与参数,这时可以使用Feign的请求模板进行抽取。如:

代码语言:javascript复制
@Configuration 
public class FeignConfig {
  @Bean
  public RequestInterceptor basicAuthRequestInterceptor() {
    return template -> {
      template.header("Authorization", "Basic YWRtaW46YWRtaW4=");  
      template.query("limit", 100);
    };
  }
}

然后在@FeignClient中指定这个拦截器:

代码语言:javascript复制
@FeignClient(name = "example", configuration = FeignConfig.class)
public interface ExampleFeignClient {
 //这个方法将自动继承basicAuthRequestInterceptor中的模板参数    
  @GetMapping("/users") 
  List<User> getUsers();
} 

这样getUsers()方法就不需要再添加limit参数和Authorization header了。

  1. Feign的 Contract 协定:

Feign通过Contract协定将方法映射到请求模板,包括请求绑定、URL映射规则等。Feign提供的Contract有:

  • DefaultContract:简单的约定,将方法名映射到相同的URL,并使用JAXRS注解绑定参数。
  • SpringMvcContract:采用SpringMVC的约定,支持路径变量、请求参数等,更贴近Spring开发风格。
  • HystrixDelegatingContract:在SpringMvcContract的基础上,为每个方法创建一个HystrixCommand,以支持容错。

我们可以通过feign.client.contract指定某个Contract,或者继承Contract进行自定义。Contract让我们可以改变Feign的调用约定,使其适配不同的服务样式。

这里为您介绍了Feign中的继承支持、请求模板、Contract协定等内容。这些内容的理解可以让我们在设计Feign接口与调用方式时,拥有更丰富的选择与能力。Feign虽然简单易用,但其实它的机制相当灵活丰富,满足我们的定制需求。

  1. Feign的拦截器:

Feign支持使用拦截器对其请求进行拦截,我们可以实现以下拦截器:

  • RequestInterceptor:在请求发出之前拦截,可以修改请求。
  • ResponseInterceptor:在得到响应之后拦截,可以修改响应。
  • ErrorInterceptor:在出现异常时拦截,可以修改或重试请求。

可以通过 feign.client.config.defaultInterceptors添加拦截器,如:

代码语言:javascript复制
feign:
  client:
    config:
      defaultInterceptors:
        - feign.auth.BasicAuthRequestInterceptor  #添加BasicAuth拦截器
        - com.example.logging.LoggingInterceptor  #添加日志拦截器 

也可以为指定的FeignClient添加拦截器:

代码语言:javascript复制
@FeignClient(name="example", configuration = ExampleFeignConfig.class) 
public interface ExampleFeignClient {
}

@Configuration
class ExampleFeignConfig {
  @Bean
  public RequestInterceptor requestInterceptor() {
    return new BasicAuthRequestInterceptor("username", "password");  
  }  
}

拦截器让我们可以更灵活地定制Feign的行为,实现复杂的请求处理逻辑。

  1. Feign的解码器:

Feign默认使用ResponseEntityDecoder对响应进行解码,将其解码为ResponseEntity<T>对象。我们可以实现自己的解码器,如:

代码语言:javascript复制
public class ExampleDecoder implements Decoder {
  @Override
  public Object decode(Response response, Type type) {
    if (type == Example.class) {
      //对响应进行定制化解码
      return ... ; 
    } 
  }
}

然后通过feign.client.config.decoder指定这个解码器,如:

代码语言:javascript复制
feign:
  client: 
    config:
      example:   #指定FeignClient名称
        decoder: com.example.ExampleDecoder

定制的解码器让我们可以对Feign的响应进行转换和定制,以适配服务的响应格式。

  1. Feign的校验器:

Feign也支持Validator校验器,当接收到的响应无法正确解码时,可以使用校验器进行二次校验和处理。我们需要实现Validator接口,并指定给FeignClient使用。

Feign的拦截器、解码器和校验器让我们可以高度定制Feign的请求/响应处理过程,这也是Feign高扩展性的体现。利用这些机制,我们可以轻易地让Feign支持定制的协议格式、复杂的请求/响应流程。这使得Feign不仅仅是一个简单的HTTP客户端,还可以成为一个功能丰富的通用服务调用方案。

  1. Feign的API优化:

随着业务发展,Feign的API会变得越来越庞大,这会带来一定的维护难度。我们可以通过以下方式优化Feign的API:

  • 根据业务模块拆分FeignClient:不要定义一个太庞大的FeignClient,可以根据业务模块拆分为多个FeignClient。
  • 使用接口继承进行抽象:定义一个基础接口,让其他接口继承,抽取公共方法。
  • 利用请求模板减少重复注解:通过Feign的请求模板机制,减少Feign方法中的重复注解。
  • 采用DTO作为方法参数:不要使用基础类型作为Feign方法的参数,改用DTO对象,让方法变得更具语义化。
  • 选择语义化的方法名:给Feign的方法起一个语义化的名字,而不仅仅是URL路径。
  • 抽取公共参数为Constant:如果某些Feign方法存在大量重复的参数,可以把它抽取为一个Constant,方法中只传入这个Constant。
  • 采用Builder模式构造复杂参数:当Feign方法需要一个复杂的参数时,可以使用Builder模式构建这个参数,而不是定义一个过于庞大的参数DTO。

这些优化措施可以让我们的Feign API更清晰、更易维护。Feign虽然简单,但随着业务成长,如果不加以管理,也可以变得难以维护,所以API的优化是一个比较重要的话题。

  1. Feign的可选参数:

Feign默认采用JAXRS注解将方法映射到HTTP请求,但JAXRS注解不支持可选参数。为支持可选参数,我们可以:

  • 给可选参数定义默认值:
代码语言:javascript复制
@GetMapping("/users")
List<User> getUsers(@RequestParam(value="age", required=false) Integer age); 
  • 使用@Nullable注解标记可选参数:
代码语言:javascript复制
@GetMapping("/users")
List<User> getUsers(@Nullable @RequestParam Integer age);
  • 在方法中追加一个java.util.Optional<T>类型的参数:
代码语言:javascript复制
@GetMapping("/users")
List<User> getUsers(Optional<@RequestParam Integer> age); 
  • 采用Builder模式构建参数,可选参数不传入Builder:
代码语言:javascript复制
@GetMapping("/users") 
List<User> getUsers(UserSearchCriteria criteria);

UserSearchCriteria criteria = UserSearchCriteria.builder()  
                                          .age(30)  
                                          .build();

这些方式可以让我们的Feign接口支持可选参数,变得更加灵活。

  1. Feign的文件上传:

Feign默认不直接支持文件上传,我们有以下方式实现:

  • 使用feign.codec.Encodedecode.MultiPartFormContent编码器:
代码语言:javascript复制
@PostMapping("/upload")
void uploadFile(@Part("file") RequestBody file); 

然后在FeignClient中添加:

代码语言:javascript复制
@FeignClient(configuration=MultipartSupportConfig.class)
public interface FileUploadClient {
} 

@Configuration
public class MultipartSupportConfig {
  @Bean
  public Encoder feignEncoder() {
    return new MultipartFormContent();
  }
}  
  • 直接调用Feign的底层客户端进行文件上传:
代码语言:javascript复制
@PostMapping("/upload")
void uploadFile(); 

public void uploadFile() {
  Client client = ...;  //获取Feign的Client
  Request request = ... //构建文件上传请求
  client.execute(request); 
}
  • 采用Feign的拦截器获取文件,然后手动构建MultiPart请求:
代码语言:javascript复制
@PostMapping("/upload")
void uploadFile();  

public void uploadFile() {
  File file = ...; //获取要上传的文件
  String uploadUrl = ...; //获取上传URL
  
  //手动构建MultiPart请求
  MultiPartRequest request = new MultiPartRequest(uploadUrl);
  request.addFile("file", file);
  ... ...
} 

这些方式可以让我们的Feign接口支持文件上传的功能,对接那些需要文件上传的服务。

Feign虽然简单易用,但我们也可以通过各种方式进行扩展,实现较为复杂的服务调用需求。

  1. Feign的OAuth2支持:

Feign默认不支持OAuth2,我们可以通过以下方式实现:

  • 自定义RequestInterceptor拦截器,在每个请求加入OAuth2的Authorization头。
  • 扩展Feign的Contract,把OAuth2的Authorization头数据注入到每个请求模板中。
  • 利用Zuul的OAuth2支持,在网关处获得访问令牌,然后把令牌转发给Feign客户端。
  • 直接使用Feign的底层客户端,在执行每个请求前,从OAuth2服务器获取访问令牌,并手动加入到请求头中。

Feign的Hystrix支持

Feign默认提供了对Hystrix的集成支持,我们可以很容易地为Feign接口启用Hystrix。主要有以下方式:

  • 在application.yml中全局指定:
代码语言:javascript复制
feign:
  hystrix:
    enabled: true
  • 在@FeignClient中指定fallback:
代码语言:javascript复制
@FeignClient(name = "user", fallback = UserClientFallback.class)
public interface UserClient {
}
  • 实现FallbackFactory:
代码语言:javascript复制
@Component 
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
  @Override
  public UserClient create(Throwable cause) {
    return new UserClientFallback(cause); 
  }
}

@FeignClient(name = "user", fallbackFactory = UserClientFallbackFactory.class)
public interface UserClient { 
}
  • 配置Hystrix的command属性:
代码语言:javascript复制
feign:
  client:
    config:
      user:  #FeignClient名称
        hystrix:
          command:  #HystrixCommand配置
            default:
              execution.isolation.thread.timeoutInMilliseconds: 1000 
  • 定制Hystrix的线程池、信号量隔离策略:
代码语言:javascript复制
@Configuration
public class FeignHystrixConcurrencyStrategy {
  @Bean
  public HystrixConcurrencyStrategy feignHystrixConcurrencyStrategy() {
    return hystrixConfig -> {  
      // 定制 Hystrix 线程池策略
      HystrixThreadPoolProperties.Setter()...
      // 定制 Hystrix 信号量隔离策略  HystrixSemaphoreProperties.Setter()... 
    };
  }
}

Feign的Hystrix支持让我们可以方便地对Feign接口应用熔断与容错措施,提高微服务的稳定性。

Feign的实践案例

这里提供一个Feign的最佳实践案例:

代码语言:javascript复制
@FeignClient(name = "order", url = "http://order-service")
public interface OrderClient {
    @GetMapping("/orders/{id}")
    Order getOrder(@PathVariable("id") Long id);
}

@FeignClient(name = "inventory", url = "http://inventory-service")
public interface InventoryClient {
    @GetMapping("/inventories/{productId}")
    Product getInventory(@PathVariable("productId") String productId);
}

@Service
public class BussinessService {
    @Autowired
    private OrderClient orderClient;
    @Autowired
    private InventoryClient inventoryClient;

    public void purchase(Long orderId, String customer) {
        //查询订单
        Order order = orderClient.getOrder(orderId);
        //查询库存
        Product product = inventoryClient.getInventory(order.getProductId()); 
        //进行采购流程
        //...
    }
}

这是一个简单的采购业务场景,主要有:

  • OrderClient:用于调用Order微服务,提供订单查询方法。
  • InventoryClient:用于调用Inventory微服务,提供库存查询方法。
  • BussinessService:提供采购业务逻辑,通过Feign Client调用Order和Inventory服务。
  • 这里OrderClient和InventoryClient声明为Feign Client,通过url指定服务地址,采用标准的SpringMVC注解。
  • BussinessService可以像调用本地方法一样,轻松调用这些Feign Client。
  • 这些Feign Client天然支持Ribbon负载均衡,我们不需要额外配置。
  • 也可以轻易为这些Feign Client启用Hystrix熔断机制。

这是一个典型的Feign最佳实践案例,采用Feign实现微服务之间的调用,既简单又优雅。

Feign的线程模型

Feign默认采用SimpleClient的线程模型,主要特征如下:

  • 一个Client只有一个线程,所有的请求串行执行。
  • 该线程同时也负责发起连接,传输响应等任务。
  • 该线程会在空闲时进入阻塞,减少资源占用。
  • 该线程 daemon状态,随应用关闭而关闭。

这种简单的线程模型,可以有效减少线程切换与管理 overhead,提高性能。但也存在几个问题:

  • 一个慢请求会阻塞其他请求,影响整体延迟。
  • 无法充分利用高并发环境下的资源。
  • 难以调优,线程利用率不可控。

为此,Feign也提供了其他线程模型:

  • HystrixFeign:通过Hystrix线程池提供隔离,避免一个慢请求影响其他请求,提高容错性。
  • 线程池Feign:使用自定义线程池,可以配置线程数量,提高并发性和控制延迟,利用资源更优。

可以通过设置feign.client.config.default 配置项来选择线程模型。

选择合适的线程模型,可以显著优化Feign的性能表现,提高资源利用率和容错性。但也需要权衡相关配置的复杂性。

Feign的连接池

Feign默认也使用连接池,主要特征如下:

  • 默认最大连接数为200。
  • 连接空闲时间默认为60秒,超时则关闭。
  • 默认支持100个连接打开时间超过30s的长时间连接。
  • 连接池使用 CommonsPool实现,简单可靠。

我们可以通过feign.client.config配置连接池参数,如:

代码语言:javascript复制
feign:
  client: 
    config:
      default: 
        pool: 
          maxTotal: 500     #最大连接数
          maxPerRoute: 50   #每个路由最大连接数  
          validateAfterInactivity: 1s #连接空闲校验时间      

连接池的配置与优化,也是提高Feign性能和稳定性的重要一环。

Feign的HTTP连接管理

Feign对HTTP连接的管理主要包含:

  • 长连接与短连接:Feign默认采用Keep-Alive头,维持长连接,可以配置connectionTimeout指定连接最长存活时间。也支持配置为短连接close。
  • 最大连接数:每个url对应一个连接池,默认最大连接数为200。可以配置maxConnections自定义。
  • 复用连接:对同一主机和连接的请求可以复用连接,实现连接重用。unless连接被关闭。
  • 空闲连接管理:Feign会定期清理超过一定空闲时间的连接。默认60秒。可配置idleTimeout自定义。
  • 失效连接清理:Feign也会定期清理超过最大存活时间的失效连接。默认5分钟。可配置timeToLive自定义。
  • 连接预热:第一次访问url时,Feign会预先与服务建立一定数量的连接,默认为url的最大连接数,实现快速响应。可关闭预热,配置initialLineRequests为0。

这些连接管理机制可以使Feign充分复用连接,合理分配连接资源,实现高效网络 I/O。但是,也需要根据实际场景权衡配置:

  • 长连接时间过长,连接资源可能过度占用。
  • 空闲连接过期时间过短,连接无法充分复用,带来额外开销。
  • 连接预热数量过多,启动阶段延迟较高,并发能力可能受限。
  • 定期清理频率过低,无法快速释放失效连接,导致资源浪费。

所以,Feign的连接管理参数配置需要综合考虑系统稳定性、并发量与延迟指标而定制。

Feign的Encoder和Decoder

Feign默认提供了对JSON的编解码支持,我们也可以通过Encoder和Decoder接口扩展Feign的编解码能力。常见的编解码器包括:

  • ContentTypeDecoder: 根据Content-Type解析响应体,支持多格式。
  • FormEncoder: 编码表单请求,Content-Type为application/x-www-form-urlencoded。
  • XMLDecoder/XMLEncoder: 支持XML格式编解码。
  • GZIPDecoder/GZIPEncoder: 支持GZIP压缩的响应解码和请求编码。
  • CBORDecoder/CBOREncoder: 支持CBOR二进制格式的编解码,性能更高。

扩展Feign的编解码器,可以使其支持更多格式的响应与请求,如XML、Protobuf、CBOR等,实现多样化的服务集成。 我们可以通过两种方式扩展:

  1. 全局配置: 覆盖feign.codec.decoder和feign.codec.encoder属性。
  2. 局部配置: 为某个FeignClient指定decoder和encoder。

例如,配置XML编解码器:

代码语言:javascript复制
feign:
  client: 
    config:
      default: 
        decoder: feign.codec.xml.XMLDecoder
        encoder: feign.codec.xml.XMLEncoder

对某Client配置:

代码语言:javascript复制
@FeignClient(name="clientName", decoder = XMLDecoder.class, encoder = XMLEncoder.class)
public interface ClientInterface {
}  

扩展Feign的编解码能力,可以使其不仅支持传统的JSON/XML接口,也可以配合自定义编解码器实现任意格式的接口集成,成为连接各种服务的统一调度入口。

Feign的添加头信息

Feign可以通过以下方式添加请求头信息:

  1. 方法上的@Header注解:
代码语言:javascript复制
@FeignClient("serviceName")
public interface Client {        
   @RequestMapping(method = RequestMethod.GET, "/hello")
   @Header("token: 1234")   //添加头信息
   String hello();
}
  1. 为FeignClient添加defaultHeaders属性:
代码语言:javascript复制
@FeignClient(name = "clientName", defaultHeaders = {
   "token: 1234"              
})
public interface Client {  
   //...
}
  1. 配置feign.client.headers:
代码语言:javascript复制
feign:
  client:
    headers: 
      token: 1234   
  1. 为方法添加HeaderInterceptor:
代码语言:javascript复制
@FeignClient(name = "clientName")
public interface Client {
   @RequestMapping(method = RequestMethod.GET, "/hello")
   String hello(); 
}

//Header Interceptor
public class HeaderInterceptor implements RequestInterceptor {
   @Override
   public void apply(RequestTemplate template) {
       template.header("token", "1234"); 
   }
}

//为Client接口增加拦截器 
@FeignClient(name = "clientName",interceptors = HeaderInterceptor.class )
public interface Client {  
   // ...
}

Feign提供了多种方式添加请求头,以实现服务鉴权、传递上下文等功能。但是,配置方式较为分散,维护稍显不便。

本文的内容就到此了,我们介绍了很多关于Feign的使用技巧以及优化经验,相信能帮助大家在实际生产项目中正确使用。

0 人点赞