1. 引入&说明

服务消费者调用服务提供者的时候使用 RestTemplate 技术存在不便之处:① 拼接 url;② restTmplate.getForObject。这两处代码都比较模板化,能不能不让我我们来写这种模板化的东西?另外来说,拼接 url 、拼接字符串、拼接参数,很 low 还容易出错。

https://github.com/spring-cloud/spring-cloud-openfeign

Feign 是 Netflix 开发的一个轻量级 RESTful 的 HTTP 服务客户端(用它来发起请求,远程调用的),是以 Java 接口注解的方式调用 HTTP 请求,而不用像 Java 中通过封装 HTTP 请求报文的方式直接调用,Feign 被广泛应用在 SpringCloud 的解决方案中。

  • Feign 可帮助我们更加便捷优雅的调用 HTTP API,即不需要我们去拼接 url 然后调用 RestTemplate 的 API。在 SpringCloud 中,使用 Feign 非常简单,创建一个服务接口(Feign 在消费端使用),并在接口上添加一些注解,代码就完成了;
  • SpringCloud 对 Feign 进行了增强,使其支持了 SpringMVC 标准注解和 HttpMessageConverters(→ OpenFeign);
  • Feign 也支持可插拔式的编码器和解码器;
  • Feign 可以与 Enreka 和 Ribbon 组合使用以支持负载均衡。

本质:封装了 HTTP 调用流程,更符合面向接口化的编程习惯,类似于 Dubbo 的服务调用。

Dubbo 的调用方式其实就是很好的面向接口编程:服务消费者拿到服务提供者的接口,然后像调用本地接口方法一样去调用,实际发出的是远程的请求。

restTemplate 调用FeignClien feign resttemplate性能_服务提供者

2. 基本使用

效果上来说,Feign = RestTemplate + Ribbon + Hystrix。

  1. 8080 消费端服务引入依赖;
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  1. 8080 消费端服务主启动类上新增注解 @EnableFeignClients(可去掉 Hystrix 的支持注解 @EnableCircuitBreaker,因为 Feign 会自动引入);
  2. 8080 消费端服务新增 8001/8002 服务 Controller 对应的接口 ResumeServiceFeignClient;
// @FeignClient 表明当前类是一个 Feign 客户端
// value 指定该客户端要请求的服务名称(服务提供者的 spring.application.name)
@FeignClient(value = "cloud-service-resume")
@RequestMapping("/resume")
public interface ResumeServiceFeignClient {
    /**
     * 查询 payment
     * Feign 要做的事情就是,拼装 url 发起请求。调用该方法就是调用
     * 本地接口方法,那么实际上做的是远程请求。
     * @param id
     * @return
     */
    @GetMapping("/payment/{id}")
    public Integer findDefaultResumeState(@PathVariable("id") Long id);
}
  1. 调用消费者端会进行远程调用的接口,即可观察 Feign 调用结果。

说明:

  • @FeignClient 注解的 name 属性用于指定要调用的服务提供者名称,和服务提供者 yml 文件中 spring.application.name 要保持一致;
  • 接口中的接口方法,就好比是远程服务提供者 Controller 中的 handle 方法(只不过如同本地调用了),那么在进行参数绑定的时,可以使用 @PathVariable@RequestParam@RequestHeader 等,这也是 OpenFeign 对 SpringMVC 注解的支持,但是需要注意 value 必须设置,否则会抛出异常;
  • 使用接口中方法完成远程调用(注入接口即可,实际注入的是接口的实现)。

3. 对负载均衡的支持

Feign 本身已经集成了 Ribbon 依赖和自动配置,因此我们不需要额外引入依赖,可以通过 ribbon.xxx 来进行全局配置,也可以通过 服务名.ribbon.xxx 来对指定服务进行细节配置配置(参考之前,此处略)。

Feign 默认的请求处理超时时长 1s,有时候我们的业务确实执行的需要一定时间,那么这个时候,我们就需要调整请求处理超时时长,Feign 自己有超时设置,如果配置 Ribbon 的超时,则会以 Ribbon 的为准。

# 针对的被调用方微服务名称,不加就是全局生效
cloud-service-resume:
  ribbon:
    # 请求连接超时时间
    ConnectTimeout: 2000
    # 请求处理超时时间 (=> Feign 超时时长设置!)
    ReadTimeout: 3000
    # 对所有操作都进行重试
    OkToRetryOnAllOperations: true
    # 根据如上配置,当访问到故障请求的时候,它会再尝试访问一次当前实例(次数由 MaxAutoRetries
    # 配置),如果不行就换一个实例进行访问,如果还不行,再换一次实例访问(更换次数由
    # MaxAutoRetriesNextServer 配置)。如果依然不行,返回失败信息!
    MaxAutoRetries: 0 # 对当前选中实例重试次数,不包括第 1 次调用
    MaxAutoRetriesNextServer: 0 # 切换实例的重试次数
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule # 负载策略调整

4. 对熔断器的支持

(1)在 Feign 客户端工程配置文件(application.yml)中开启 Feign 对熔断器的支持;

# 开启 Feign 的熔断功能
feign:
  hystrix:
    enabled: true

Feign 的超时时长设置那其实就上面 Ribbon 的超时时长设置!Hystrix 超时设置就按照之前 Hystrix 设置的方式就 ok~

注意:

  • 开启 Hystrix 之后,Feign 中的方法都会被管理了,一旦出现问题就进入对应的回退逻辑处理;
  • 针对「超时」这一点,当前有两个超时时间设置(Feign/Hystrix),熔断的时候是根据这两个时间的最小值来进行的!即处理时长超过最短的那个超时时间了就熔断进入回退降级逻辑。
# 开启Feign的熔断功能
feign:
  hystrix:
    enabled: false
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            # => Hystrix 的超时时长设置!
            timeoutInMilliseconds: 15000

(2)自定义 FallBack 处理类(需要实现 FeignClient 接口);

@Component
public class ResumeServiceFallback implements ResumeServiceFeignClient {
    @Override
    public Integer findDefaultResumeState(Long id);
        return 1101;
    }
}

(3)在 @FeignClient 中关联 2 中自定义的处理类;

// 使用 fallback 的时候,类上的 @RequestMapping 的 url 前缀限定移到配置在 @FeignClient#path 属性中,防止出错。
// @RequestMapping("/resume")
@FeignClient(value = "cloud-service-resume", path = "/resume", fallback = ResumeServiceFallback.class)
public interface ResumeServiceFeignClient {

5. 日志级别配置

Feign 提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解 Feign 中 Http 请求的细节。说白了就是对 Feign 接口的调用情况进行监控和输入。

Feign 日志级别:

  • NONE:默认的,不显示任何日志;
  • BASIC:仅记录请求方法、URL、响应状态码及执行时间;
  • HEADERS:除了 BASIC 中定义的信息之外,还有请求和响应的头信息;
  • FULL:除了 HEADERS 中定义的信息之外,还有请求和响应的正文及元数据。
  1. 开启 Feign 日志功能及配置级别;
@Configuration
public class FeignConfig {
    @Bean
    Logger.Level feignLoggerLevel(){
        return Logger.Level.FULL;
    }
}
  1. 配置 Feign 客户端的 log 日志级别为 debug;
# 以指定日志级别监控指定请求接口
logging:
  level:
    cn.edu.nuist.cloud.feign.ResumeServiceFeignClient: debug
  1. 发送请求,然后查看控制台;

6. 对请求压缩和响应压缩的支持

Feign 支持对请求和响应进行 GZIP 压缩,以减少通信过程中的性能损耗。通过下面的参数即可开启请求与响应的压缩功能:

feign:
  compression:
    request:
      # 开启请求压缩
      enabled: true
      # 设置压缩的数据类型 (此处也是默认值)
      mime-types: text/html,application/xml,application/json
      # 设置触发压缩的大小下限 (此处也是默认值)
      min-request-size: 2048
    response:
      # 开启响应压缩
      enabled: true

7. 源码分析

7.1 代理对象的产生

(0)@FeignClient 是对接口产生了一个代理对象。

restTemplate 调用FeignClien feign resttemplate性能_客户端_02

(1)从启动类上的 @EnableFeignClients 正向切入;

restTemplate 调用FeignClien feign resttemplate性能_客户端_03

(2)深入这个 register;

restTemplate 调用FeignClien feign resttemplate性能_spring_04

(3)接下来,深入另一个方法 registerFeignClients(metadata, registry)

restTemplate 调用FeignClien feign resttemplate性能_客户端_05

(4)注册客户端,给每一个客户端生成代理对象;

restTemplate 调用FeignClien feign resttemplate性能_客户端_06

(5)下一步,关注 FeignClientFactoryBean 这个工厂 Bean 的 getObject() 方法,预估这个方法会返回我们一开始看到的代理对象;

restTemplate 调用FeignClien feign resttemplate性能_客户端_07

(6)org.springframework.cloud.openfeign.HystrixTargeter#target

restTemplate 调用FeignClien feign resttemplate性能_客户端_08

restTemplate 调用FeignClien feign resttemplate性能_spring_09

7.2 增强逻辑处理过程

(1)请求进来的时候,是进入增强逻辑的,所以接下来要关注增强部分逻辑 —— FeignInvocationHandler#invoke

restTemplate 调用FeignClien feign resttemplate性能_spring_10

(2)SynchronousMethodHandler#invoke

restTemplate 调用FeignClien feign resttemplate性能_服务提供者_11

(3)AbstractLoadBalancerAwareClient#executeWithLoadBalancer

restTemplate 调用FeignClien feign resttemplate性能_spring_12

(4)进入 submit 方法,我们就会进一步发现是使用 Ribbon 在做负载均衡啦;

restTemplate 调用FeignClien feign resttemplate性能_spring_13

(5)最终发起请求使用的是 HttpURLConnection。

restTemplate 调用FeignClien feign resttemplate性能_客户端_14

8. 各组件超时时间

在 Spring Cloud 中,应用的组件较多,只要涉及通信,就有可能会发生请求超时。那么如何设置超时时间? 在 Spring Cloud 中,超时时间只需要重点关注 Ribbon 和 Hystrix 即可。

8.1 Ribbon

Ribbon 如果采用的是服务发现方式,就可以通过服务名去进行转发,需要配置 Ribbon 的超时。Ribbon 的超时可以配置全局 ribbon.ReadTimeoutribbon.ConnectTimeout。也可以在前面指定服务名,为每个服务单独配置,比如 user-service.ribbon.ReadTimeout

8.2 Hystrix

其次是 Hystrix 的超时配置,Hystrix 的超时时间要大于 Ribbon 的超时时间,因为 Hystrix 将请求包装了起来,特别需要注意的是,如果 Ribbon 开启了重试机制,比如重试 3 次,Ribbon 的超时为 1s,那么 Hystrix 的超时时间应该大于 3s,否则就会出现 Ribbon 还在重试中,而 Hystrix 已经超时的现象。

restTemplate 调用FeignClien feign resttemplate性能_服务提供者_15

Hystrix 全局超时配置就可以用 default 来代替具体的 command 名称,hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=3000 如果想对具体的 command 进行配置,那么就需要知道 command 名称的生成规则,才能准确的配置。

如果我们使用 @HystrixCommand 的话,可以自定义 commandKey。如果使用 FeignClient 的话,可以为 FeignClient 来指定超时时间 hystrix.command.UserRemoteClient.execution.isolation.thread.timeoutInMilliseconds = 3000,如果想对 FeignClient 中的某个接口设置单独的超时,可以在 FeignClient 名称后加上具体的方法:hystrix.command.UserRemoteClient#getUser(Long).execution.isolation.thread.timeoutInMilliseconds=3000

8.3 Feign

Feign 本身也有超时时间的设置,如果此时设置了 Ribbon 的时间就以 Ribbon 的时间为准,如果没设置 Ribbon 的时间但配置了 Feign 的时间,就以 Feign 的时间为准。Feign 的时间同样也配置了连接超时时间(feign.client.config.服务名称.connectTimeout)和读取超时时间(feign.client.config.服务名称.readTimeout)。

建议,我们配置 Ribbon 超时时间和 Hystrix 超时时间即可。