是什么

Feign是一个http请求调用的轻量级框架,可以以Java接口注解的方式调用Http请求。Spring Cloud引入 Feign并且集成了Ribbon实现客户端负载均衡调用。
封装了Http调用流程,更适合面向接口化的变成习惯

实现流程

流程图1
v2-54575ad99c82530a9a4aad07746c67ff_1440w.jpeg
流程图2
截屏2021-12-01 下午9.32.21.png
核心就是通过一系列的封装和处理,将以 Java 注解的方式定义的远程调用 API 接口,最终转换成 HTTP 的请求形式,然后将 HTTP 的请的响应结果,解码成 Java Bean,返回给调用者

Feign 源码实现

v2-44b7dac257c310653bb6e3473bd80a3e_r.jpeg

基于面向接口的动态代理方式生成实现类

开启扫描所有@FeignClient注解的接口,将所有的访问路径以及接口信息封装成一个FeignClientFactoryBean注册到Spring容器中。
在使用feign 时,会定义对应的接口类,在接口类上使用Http相关的注解,标识HTTP请求参数信息
在 Feign 底层,通过基于面向接口的动态代理方式生成实现类,将请求调用委托到动态代理实现类,基本原理如下所示:

为了创建Feign的远程接口的代理实现类,Feign提供了自己的一个默认的调用处理器,叫做 FeignInvocationHandler 类,该类处于 feign-core 核心jar包中。当然,调用处理器可以进行替换,如果Feign与Hystrix结合使用,则会替换成HystrixInvocationHandler调用处理器类,类处于 feign-hystrix 的jar包中

根据 Contract 协议规则,解析接口类的注解信息,解析成内部表现

v2-7f12a3e6a0775db7f83b1de59fd785a8_b.jpeg

  1. /**
  2. * Defines what annotations and values are valid on interfaces.
  3. */
  4. public interface Contract {
  5. /**
  6. * Called to parse the methods in the class that are linked to HTTP requests.
  7. * 传入接口定义,解析成相应的方法内部元数据表示
  8. * @param targetType {@link feign.Target#type() type} of the Feign interface.
  9. */
  10. // TODO: break this and correct spelling at some point
  11. List<MethodMetadata> parseAndValidatateMetadata(Class<?> targetType);
  12. }

基于 RequestBean,动态生成 Request

根据传入的Bean对象和注解信息,从中提取出相应的值,来构造Http Request 对象

使用Encoder 将Bean转换成 Http报文正文(消息解析和转码逻辑)

Feign 最终会将请求转换成Http 消息发送出去,传入的请求对象最终会解析成消息体

拦截器负责对请求和返回进行装饰处理

在请求转换的过程中,Feign 抽象出来了拦截器接口,用于用户自定义对请求的操作,比如,如果希望Http消息传递过程中被压缩,可以定义一个请求拦截器。

  1. public interface RequestInterceptor {
  2. /**
  3. * 可以在构造RequestTemplate 请求时,增加或者修改Header, Method, Body 等信息
  4. * Called for every request. Add data using methods on the supplied {@link RequestTemplate}.
  5. */
  6. void apply(RequestTemplate template);
  7. }

比如,如果希望Http消息传递过程中被压缩,可以定义一个请求拦截器:

  1. public class FeignAcceptGzipEncodingInterceptor extends BaseRequestInterceptor {
  2. /**
  3. * Creates new instance of {@link FeignAcceptGzipEncodingInterceptor}.
  4. *
  5. * @param properties the encoding properties
  6. */
  7. protected FeignAcceptGzipEncodingInterceptor(FeignClientEncodingProperties properties) {
  8. super(properties);
  9. }
  10. /**
  11. * {@inheritDoc}
  12. */
  13. @Override
  14. public void apply(RequestTemplate template) {
  15. // 在Header 头部添加相应的数据信息
  16. addHeader(template, HttpEncoding.ACCEPT_ENCODING_HEADER, HttpEncoding.GZIP_ENCODING,
  17. HttpEncoding.DEFLATE_ENCODING);
  18. }
  19. }

日志记录

在发送和接收请求的时候,Feign定义了统一的日志门面来输出日志信息 , 并且将日志的输出定义了四个等级:NONE、BASIC、HEADERS、FULL
具体实现位于:feign.Logger

基于重试器发送HTTP请求

Feign 内置了一个重试器,当HTTP请求出现IO异常时,Feign会有一个最大尝试次数发送请求

  1. final class SynchronousMethodHandler implements MethodHandler {
  2. // 省略部分代码
  3. @Override
  4. public Object invoke(Object[] argv) throws Throwable {
  5. //根据输入参数,构造Http 请求。
  6. RequestTemplate template = buildTemplateFromArgs.create(argv);
  7. // 克隆出一份重试器
  8. Retryer retryer = this.retryer.clone();
  9. // 尝试最大次数,如果中间有结果,直接返回
  10. while (true) {
  11. try {
  12. return executeAndDecode(template);
  13. } catch (RetryableException e) {
  14. retryer.continueOrPropagate(e);
  15. if (logLevel != Logger.Level.NONE) {
  16. logger.logRetry(metadata.configKey(), logLevel);
  17. }
  18. continue;
  19. }
  20. }
  21. }
  22. }

发送 HTTP 请求

Feign 真正发送HTTP请求是委托给 feign.Client 来做的。

  1. public interface Client {
  2. /**
  3. * Executes a request against its {@link Request#url() url} and returns a response.
  4. * 执行Http请求,并返回Response
  5. * @param request safe to replay.
  6. * @param options options to apply to this request.
  7. * @return connected response, {@link Response.Body} is absent or unread.
  8. * @throws IOException on a network error connecting to {@link Request#url()}.
  9. */
  10. Response execute(Request request, Options options) throws IOException;
  11. }

Feign 默认底层通过JDK 的 java.net.HttpURLConnection 实现了feign.Client接口类,在每次发送请求的时候,都会创建新的HttpURLConnection 链接,这也就是为什么默认情况下Feign的性能很差的原因。可以通过拓展该接口,使用Apache HttpClient 或者OkHttp3等基于连接池的高性能Http客户端。
而这个client会委托给org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient进行负载均衡地调用,采用了观察者模式。

  1. public class LoadBalancerFeignClient implements Client {
  2. //省略.....
  3. @Override
  4. public Response execute(Request request, Request.Options options) throws IOException {
  5. try {
  6. URI asUri = URI.create(request.url());
  7. String clientName = asUri.getHost();
  8. URI uriWithoutHost = cleanUrl(request.url(), clientName);
  9. FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
  10. this.delegate, request, uriWithoutHost);
  11. IClientConfig requestConfig = getClientConfig(options, clientName);
  12. return lbClient(clientName)
  13. .executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse();
  14. }
  15. catch (ClientException e) {
  16. IOException io = findIOException(e);
  17. if (io != null) {
  18. throw io;
  19. }
  20. throw new RuntimeException(e);
  21. }
  22. }
  23. //省略.....
  24. }