这下,我终于弄明白了OpenFeign的核心架构原理!!

3,791 阅读16分钟

大家好,我是三友~~

在很久之前,我写过两篇关于OpenFeign和Ribbon这两个SpringCloud核心组件架构原理的文章

但是说实话,从我现在的角度来看,这两篇文章的结构和内容其实还可以更加完善

刚好我最近打算整个SpringCloud各个组件架构原理的小册子

所以趁着这个机会,我就来重新写一下这两篇文章,弥补之前文章的不足

这一篇文章就先来讲一讲OpenFeign的核心架构原理

整篇文章大致分为以下四个部分的内容:

第一部分,脱离于SpringCloud,原始的Feign是什么样的?

第二部分,Feign的核心组件有哪些,整个执行链路是什么样的?

第三部分,SpringCloud是如何把Feign融入到自己的生态的?

第四部分,OpenFeign有几种配置方式,各种配置方式的优先级是什么样的?

好了,话不多说,接下来就直接进入主题,来探秘OpenFeign核心架构原理

公众号:三友的java日记

原始Feign是什么样的?

在日常开发中,使用Feign很简单,就三步

第一步:引入依赖

 <dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-openfeign</artifactId>
     <version>2.2.5.RELEASE</version>
</dependency>

第二步:在启动引导类加上@EnableFeignClients注解

@SpringBootApplication
@EnableFeignClients
public class MyApplication {

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }

}

第三步:写个FeignClient接口

@FeignClient(name = "order")
@RequestMapping("/order")
public interface OrderApiClient {

    @GetMapping
    Order queryOrder(@RequestParam("orderId") Long orderId);

}

之后当我们要使用时,只需要注入OrderApiClient对象就可以了

虽然使用方便,但这并不是Feign最原始的使用方式,而是SpringCloud整合Feign之后的使用方式

Feign最开始是由Netflix开源的

后来SpringCloud就将Feign进行了一层封装,整合到自己的生态,让Feign使用起来更加简单

并同时也给它起了一个更高级的名字,OpenFeign

接下来文章表述有时可能并没有严格区分Feign和OpenFeign的含义,你知道是这么个意思就行了。

Feign本身有自己的使用方式,也有类似Spring MVC相关的注解,如下所示:

public interface OrderApiClient {

    @RequestLine("GET /order/{orderId}")
    Order queryOrder(@Param("orderId") Long orderId);

}

OrderApiClient对象需要手动通过Feign.builder()来创建

public class FeignDemo {

    public static void main(String[] args) {
        OrderApiClient orderApiClient = Feign.builder()
                .target(OrderApiClient.class"http://localhost:8088");
        orderApiClient.queryOrder(9527L);
    }

}

Feign的本质:动态代理 + 七大核心组件

相信稍微了解Feign的小伙伴都知道,Feign底层其实是基于JDK动态代理来的

所以Feign.builder()最终构造的是一个代理对象

Feign在构建动态代理的时候,会去解析方法上的注解和参数

获取Http请求需要用到基本参数以及和这些参数和方法参数的对应关系

比如Http请求的url、请求体是方法中的第几个参数、请求头是方法中的第几个参数等等

之后在构建Http请求时,就知道请求路径以及方法的第几个参数对应是Http请求的哪部分数据

当调用动态代理方法的时候,Feign就会将上述解析出来的Http请求基本参数和方法入参组装成一个Http请求

然后发送Http请求,获取响应,再根据响应的内容的类型将响应体的内容转换成对应的类型

这就是Feign的大致原理

在整个Feign动态代理生成和调用过程中,需要依靠Feign的一些核心组件来协调完成

如下图所示是Feign的一些核心组件

这些核心组件可以通过Feign.builder()进行替换

由于组件很多,这里我挑几个重要的跟大家讲一讲

1、Contract

前面在说Feign在构建动态代理的时候,会去解析方法上的注解和参数,获取Http请求需要用到基本参数

而这个Contract接口的作用就是用来干解析这件事的

Contract的默认实现是解析Feign自己原生注解的

解析时,会为每个方法生成一个MethodMetadata对象

MethodMetadata就封装了Http请求需要用到基本参数以及这些参数和方法参数的对应关系

SpringCloud在整合Feign的时候,为了让Feign能够识别Spring MVC的注解,所以就自己实现了Contract接口

2、Encoder

通过名字也可以看出来,这个其实用来编码的

具体的作用就是将请求体对应的方法参数序列化成字节数组

Feign默认的Encoder实现只支持请求体对应的方法参数类型为String和字节数组

如果是其它类型,比如说请求体对应的方法参数类型为AddOrderRequest.class类型,此时就无法对AddOrderRequest对象进行序列化

这就导致默认情况下,这个Encoder的实现很难用

于是乎,Spring就实现了Encoder接口

可以将任意请求体对应的方法参数类型对象序列化成字节数组

3、Decoder

Decoder的作用恰恰是跟Encoder相反

Encoder是将请求体对应的方法参数序列化成字节数组

而Decoder其实就是将响应体由字节流反序列化成方法返回值类型的对象

Decoder默认情况下跟Encoder的默认情况是一样的,只支持反序列化成字节数组或者是String

所以,Spring也同样实现了Decoder,扩展它的功能

可以将响应体对应的字节流反序列化成任意返回值类型对象

4、Client

从接口方法的参数和返回值其实可以看出,这其实就是动态代理对象最终用来执行Http请求的组件

默认实现就是通过JDK提供的HttpURLConnection来的

除了这个默认的,Feign还提供了基于HttpClient和OkHttp实现的

在项目中,要想替换默认的实现,只需要引入相应的依赖,在构建Feign.builder()时设置一下就行了

SpringCloud环境底下会根据引入的依赖自动进行设置

除了上述的三个实现,最最重要的当然是属于它基于负载均衡的实现

如下是OpenFeign用来整合Ribbon的核心实现

这个Client会根据服务名,从Ribbon中获取一个服务实例的信息,也就是ip和端口

之后会通过ip和端口向服务实例发送Http请求

5、InvocationHandlerFactory

InvocationHandler我相信大家应该都不陌生

对于JDK动态代理来说,必须得实现InvocationHandler才能创建动态代理

InvocationHandler的invoke方法实现就是动态代理走的核心逻辑

而InvocationHandlerFactory其实就是创建InvocationHandler的工厂

所以,这里就可以猜到,通过InvocationHandlerFactory创建的InvocationHandler应该就是Feign动态代理执行的核心逻辑

InvocationHandlerFactory默认实现是下面这个

SpringCloud环境下默认也是使用它的这个默认实现

所以,我们直接去看看InvocationHandler的实现类FeignInvocationHandler

从实现可以看出,除了Object类的一些方法,最终会调用方法对应的MethodHandler的invoke方法

所以注意注意,这个MethodHandler就封装了Feign执行Http调用的核心逻辑,很重要,后面还会提到

虽然说默认情况下SpringCloud使用是默认实现,最终使用FeignInvocationHandler

但是当其它框架整合SpringCloud生态的时候,为了适配OpenFeign,有时会自己实现InvocationHandler

比如常见的限流熔断框架Hystrix和Sentinel都实现了自己的InvocationHandler

这样就可以对MethodHandler执行前后,也就是Http接口调用前后进行限流降级等操作。

6、RequestInterceptor

RequestInterceptor它其实是一个在发送请求前的一个拦截接口

通过这个接口,在发送Http请求之前再对Http请求的内容进行修改

比如我们可以设置一些接口需要的公共参数,如鉴权token之类的

@Component
public class TokenRequestInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate template) {
        template.header("token""token值");
    }

}

7、Retryer

这是一个重试的组件,默认实现如下

默认情况下,最大重试5次

在SpringCloud下,并没有使用上面那个实现,而使用的是下面这个实现

所以,SpringCloud下默认是不会进行重试

小总结

这一节主要是介绍了7个Feign的核心组件以及Spring对应的扩展实现

为了方便你查看,我整理了如下表格

接口作用Feign默认实现Spring实现
Contract解析方法注解和参数,将Http请求参数和方法参数对应Contract.DefaultSpringMvcContract
Encoder将请求体对应的方法参数序列化成字节数组Encoder.DefaultSpringEncoder
Decoder将响应体的字节流反序列化成方法返回值类型对象Decoder.DefaultSpringDecoder
Client发送Http请求Client.DefaultLoadBalancerFeignClient
InvocationHandlerFactoryInvocationHandler工厂,动态代理核心逻辑InvocationHandlerFactory.Default
RequestInterceptor在发送Http请求之前,再对Http请求的内容进行拦截修改
Retryer重试组件Retryer.Default

除了这些之外,还有一些其它组件这里就没有说了

比如日志级别Logger.Level,日志输出Logger,有兴趣的可以自己查看

Feign核心运行原理分析

上一节说了Feign核心组件,这一节我们来讲一讲Feign核心运行原理,主要分为两部分内容:

  • 动态代理生成原理
  • 一次Feign的Http调用执行过程

1、动态代理生成原理

这里我先把上面的Feign原始使用方式的Demo代码再拿过来

public class FeignDemo {

    public static void main(String[] args) {
        OrderApiClient orderApiClient = Feign.builder()
                .target(OrderApiClient.class"http://localhost:8088");
        orderApiClient.queryOrder(9527L);
    }

}

通过Demo可以看出,最后是通过Feign.builder().target(xx)获取到动态代理的

而上述代码执行逻辑如下所示:

最终会调用ReflectiveFeign的newInstance方法来创建动态代理对象

而ReflectiveFeign内部设置了前面提到的一些核心组件

接下我们来看看newInstance方法

这个方法主要就干两件事:

第一件事首先解析接口,构建每个方法对应的MethodHandler

MethodHandler在前面讲InvocationHandlerFactory特地提醒过

动态代理(FeignInvocationHandler)最终会调用MethodHandler来处理Feign的一次Http调用

在解析接口的时候,就会用到前面提到的Contract来解析方法参数和注解,生成MethodMetadata,这里我代码我就不贴了

第二件事通过InvocationHandlerFactory创建InvocationHandler

然后再构建出接口的动态代理对象

ok,到这其实就走完了动态代理的生成过程

所以动态代理生成逻辑很简单,总共也没几行代码,画个图来总结一下

2、一次Feign的Http调用执行过程

前面说了,调用接口动态代理的方式时,通过InvocationHandler(FeignInvocationHandler),最终交给MethodHandler的invoke方法来执行

MethodHandler是一个接口,最终会走到它的实现类SynchronousMethodHandler的invoke方法实现

SynchronousMethodHandler中的属性就是我们前面提到的一些组件

由于整个代码调用执行链路比较长,这里我就不截代码了,有兴趣的可以自己翻翻

不过这里我画了一张图,可以通过这张图来大致分析整个Feign一次Http调用的过程

  • 首先就是前面说的,进入FeignInvocationHandler,找到方法对应的SynchronousMethodHandler,调用invoke方法实现
  • 之后根据MethodMetadata和方法的入参,构造出一个RequestTemplate,RequestTemplate封装了Http请求的参数,在这个过程中,如果有请求体,那么会通过Encoder序列化
  • 然后调用RequestInterceptor,通过RequestInterceptor对RequestTemplate进行拦截扩展,可以对请求数据再进行修改
  • 再然后将RequestTemplate转换成Request,Request其实跟RequestTemplate差不多,也是封装了Http请求的参数
  • 接下来通过Client去根据Request中封装的Http请求参数,发送Http请求,得到响应Response
  • 最后根据Decoder,将响应体反序列化成方法返回值类型对象,返回

这就是Feign一次Http调用的执行过程

如果有设置重试,那么也是在这个阶段生效的

SpringCloud是如何整合Feign的?

SpringCloud在整合Feign的时候,主要是分为两部分

  • 核心组件重新实现,支持更多SpringCloud生态相关的功能
  • 将接口动态代理对象注入到Spring容器中

第一部分核心组件重新实现前面已经都说过了,这里就不再重复了

至于第二部分我们就来好好讲一讲,Spring是如何将接口动态代理对象注入到Spring容器中的

1、将FeignClient接口注册到Spring中

使用OpenFeign时,必须加上@EnableFeignClients

这个注解就是OpenFeign的发动机

@EnableFeignClients最后通过@Import注解导入了一个FeignClientsRegistrar

FeignClientsRegistrar实现了ImportBeanDefinitionRegistrar

所以最终Spring在启动的时候会调用registerBeanDefinitions方法实现

之所以会调用registerBeanDefinitions方法,是@Import注解的作用,不清楚的同学可以看一下扒一扒Bean注入到Spring的那些姿势,你会几种?

最终会走到registerFeignClients这个方法

这个方法虽然比较长,主要是干了下面这个2件事:

第一件事,扫描@EnableFeignClients所在类的包及其子包(如果有指定包就扫指定包),找出所有加了@FeignClient注解的接口,生成一堆BeanDefinition

这个BeanDefinition包含了这个接口的信息等信息

第二件事,将扫描到的这些接口注册到Spring容器中

在注册的时候,并非直接注册接口类型,而是FeignClientFactoryBean类型

好了,到这整个@EnableFeignClients启动过程就结束了

虽然上面写的很长,但是整个@EnableFeignClients其实也就只干了一件核心的事

扫描到所有的加了@FeignClient注解的接口

然后为每个接口生成一个Bean类型为FeignClientFactoryBean的BeanDefinition

最终注册到Spring容器中

2、FeignClientFactoryBean的秘密

上一节说到,每个接口都对应一个class类型为FeignClientFactoryBean的BeanDefinition

如上所示,FeignClientFactoryBean是一个FactoryBean

并且FeignClientFactoryBean的这些属性,是在生成BeanDefinition的时候设置的

并且这个type属性就是代表的接口类型

由于实现了FactoryBean,所以Spring启动过程中,一定为会调用getObject方法获取真正的Bean对象

FactoryBean的作用就不说了,不清楚的小伙伴还是可以看看扒一扒Bean注入到Spring的那些姿势,你会几种?这篇文章

getObject最终会走到getTarget()方法

从如上代码其实可以看出来,最终还是会通过Feign.builder()来创建动态代理对象

只不过不同的是,SpringCloud会替换Feign默认的组件,改成自己实现的

总的来说,Spring是通过FactoryBean的这种方式,将Feign动态代理对象添加到Spring容器中

OpenFeign的各种配置方式以及对应优先级

既然Feign核心组件可以替换,那么在SpringCloud环境下,我们该如何去配置自己的组件呢?

不过在说配置之前,先说一下FeignClient配置隔离操作

在SpringCloud环境下,为了让每个不同的FeignClient接口配置相互隔离

在应用启动的时候,会为每个FeignClient接口创建一个Spring容器,接下来我就把这个容器称为FeignClient容器

这些FeignClient的Spring容器有一个相同的父容器,那就是项目启动时创建的容器

SpringCloud会给每个FeignClient容器添加一个默认的配置类FeignClientsConfiguration配置类

这个配置类就声明了各种Feign的组件

所以,默认情况下,OpenFeign就使用这些配置的组件构建代理对象

知道配置隔离之后,接下来看看具体的几种方式配置以及它们之间的优先级关系

1、通过@EnableFeignClients注解的defaultConfiguration属性配置

举个例子,比如我自己手动声明一个Contract对象,类型为MyContract

public class FeignConfiguration {
    
    @Bean
    public Contract contract(){
        return new MyContract();
    }
    
}

注意注意,这里FeignConfiguration我没加@Configuration注解,原因后面再说

此时配置如下所示:

@EnableFeignClients(defaultConfiguration = FeignConfiguration.class)

之后这个配置类会被加到每个FeignClient容器中,所以这个配置是对所有的FeignClient生效

并且优先级大于默认配置的优先级

比如这个例子就会使得FeignClient使用我声明的MyContract,而不是FeignClientsConfiguration中声明的SpringMvcContract

2、通过@FeignClient注解的configuration属性配置

还以上面的FeignConfiguration配置类举例,可以通过@FeignClient注解配置

@FeignClient(name = "order", configuration = FeignConfiguration.class)

此时这个配置类会被加到自己FeignClient容器中,注意是自己FeignClient容器

所以这种配置的作用范围是自己的这个FeignClient

并且这种配置的优先级是大于@EnableFeignClients注解配置的优先级

3、在项目启动的容器中配置

前面提到,由于所有的FeignClient容器的父容器都是项目启动的容器

所以可以将配置放在这个项目启动的容器中

还以FeignConfiguration为例,加上@Configuration注解,让项目启动的容器的扫描到就成功配置了

这种配置的优先级大于前面提到的所有配置优先级

并且是对所有的FeignClient生效

所以,这就是为什么使用注解配置时为什么配置类不能加@Configuration注解的原因,因为一旦被项目启动的容器扫描到,这个配置就会作用于所有的FeignClient,并且优先级是最高的,就会导致你其它的配置失效,当然你也可以加@Configuration注解,但是一定不能被项目启动的容器扫到

4、配置文件

除了上面3种编码方式配置,OpenFeign也是支持通过配置文件的方式进行配置

并且也同时支持对所有FeignClient生效和对单独某个FeignClient生效

对所有FeignClient生效配置:

feign:
  client:
    config:
      default: # default 代表对全局生效
        contract: com.sanyou.feign.MyContract

对单独某个FeignClient生效配置:

feign:
  client:
    config:
      order: # 具体的服务名
        contract: com.sanyou.feign.MyContract

在默认情况下,这种配置文件方式优先级最高

但是如果你在配置文件中将配置项feign.client.default-to-properties设置成false的话,配置文件的方式优先级就是最低了

feign:
  client:
    default-to-properties: false

小总结

这一节,总共总结了4种配置OpenFeign的方式以及它们优先级和作用范围

画张图来总结一下

如果你在具体使用的时候,还是遇到了一些优先级的问题,可以debug这部分源码,看看到底生效的是哪个配置

总结

到这,总算讲完了OpenFeign的核心架构原理了

这又是一篇洋洋洒洒的万字长文

由于OpenFeign它只是一个框架,并没有什么复杂的机制

所以整篇文章还是更多偏向源码方面

不知道你看起来感觉如何

如果你感觉还不错,欢迎点赞、在看、收藏、转发分享给其他需要的人

你的支持就是我更新的最大动力,感谢感谢!

更多SpringCloud系列的文章,可以在公众号三友的java日记后台菜单栏中查看。

好了,本文就讲到这里,让我们下期再见,拜拜!