【云原生&微服务十四】SpringCloud之深度源码剖析OpenFeign如何为FeignClient生成动态代理类

一、前言

在前面的文章:

  1. SpringCloud之Feign实现声明式客户端负载均衡详细案例
  2. SpringCloud之OpenFeign实现服务间请求头数据传递(OpenFeign拦截器RequestInterceptor的使用)
  3. SpringCloud之OpenFeign的常用配置(超时、数据压缩、日志)
  4. SpringCloud之OpenFeign的核心组件(Encoder、Decoder、Contract)
  5. SpringBoot启动流程中开启OpenFeign的入口(ImportBeanDefinitionRegistrar详解)
  6. 源码剖析OpenFeign如何扫描所有的FeignClient

我们聊了以下内容:

  1. OpenFeign的概述、为什么会使用Feign代替Ribbon?
  2. Feign和OpenFeign的区别?
  3. 详细的OpenFeign实现声明式客户端负载均衡案例
  4. OpenFeign中拦截器RequestInterceptor的使用
  5. OpenFeign的一些常用配置(超时、数据压缩、日志输出)
  6. SpringCloud之OpenFeign的核心组件(Encoder、Decoder、Contract)
  7. 在SpringBoot启动流程中开启OpenFeign的入口
  8. OpenFeign如何扫描 / 注册所有的FeignClient

本文基于OpenFeign低版本(SpringCloud 2020.0.x版本之前)讨论:OpenFeign如何为FeignClient生成动态代理类

PS:本文基于的SpringCloud版本

 <properties>
    <spring-boot.version>2.3.7.RELEASE</spring-boot.version>
    <spring-cloud.version>Hoxton.SR9</spring-cloud.version>
    <spring-cloud-alibaba.version>2.2.6.RELEASE</spring-cloud-alibaba.version>

</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${spring-boot.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!--整合spring cloud-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!--整合spring cloud alibaba-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>${spring-cloud-alibaba.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

在上一篇文章(SpringCloud之深度源码剖析OpenFeign如何扫描所有的FeignClient)我们聊了OpenFeign如何扫描所有的FeignClient,本文接着聊OpenFeign是如何针对FeignClient生成动态代理类的?

0、生成FeignClient的动态代理类整理流程图

在这里插入图片描述

二、生成FeignClient的动态代理类

这里依旧是结合OpenFeign案例( SpringCloud之Feign实现声明式客户端负载均衡详细案例)聊,ServiceBController中通过@Autowired注解注入了一个被@FeignClient标注的接口ServiceAClient。

AbstractApplicationContext#refresh()方法中最后调用的finishBeanFactoryInitialization(beanFactory)方法中会将所有类全部注入到Spring容器中;在将ServiceBController注入到Spring容器过程中,会将其成员ServiceAClient也注入到Spring容器中,AbstractAutowireCapableBeanFactory#populateBean()方法会处理ServiceAClient,进而调用到AutowiredAnnotationBeanPostProcessor#postProcessProperties()方法对ServiceAClient做一个注入操作,具体执行流程如下:

在这里插入图片描述
在这里插入图片描述
…中间经过一些过程(买个坑,@AutoWired自动注入实现机制后面特地写一篇文章分析)…走到AbstractBeanFactory#getBean(String)方法获取ServiceBController依赖的成员类型ServiceAClient;
在这里插入图片描述

由于我们在注册FeignClient到Spring容器时,构建的BeanDefinition的beanClas是FeignClientFactoryBean
在这里插入图片描述
FeignClientFactoryBean是一个工厂,保存了@FeignClient注解的所有属性值,在Spring容器初始化的过程中,其会根据之前扫描出的FeignClient信息构建FeignClient的动态代理类。

在这里插入图片描述
从debug的堆栈信息我们可以看到是FeignClientFactoryBean#getObject()方法负责获取/创建动态代理类。

假如我们不debug,该如何找到哪里负责创建动态代理类?

1、FeignClientFactoryBean创建动态代理类的入口

我们知道要通过注册到Spring容器中的FeignClient的BeanDefinition的beanClass属性是FeignClientFactoryBean,所大概率和FeignClientFactoryBean是相关的,那怎么找呢?

高工、架构们选择连蒙带猜!!!!

在这里插入图片描述

注意到FeignClientFactoryBean的feign(FeignContext)方法,方法会构造一个Feign.Builder,Builder、Builder,这不是就是构造器模式嘛;基于Feign.Builder可以构造对应的FeignClient。

再看哪里调用了feign()方法;找到getTarget()方法;
在这里插入图片描述

对于有一定开发的经验而言,见到Target这一类东西,基本可以确定就是动态代理;

再往上追,看哪里调用了getTarget()方法?进入到getObject()方法;
在这里插入图片描述
而getObject()方法是FactoryBean接口中定义的方法;
在这里插入图片描述

到这里可以确定FeignClientFactoryBean#getObject()方法,在spring容器初始化时,会被作为入口来调用,进而创建一个ServiceAClient的动态代理,返回给spring容器 并 注册到Spring容器里去。

2、Feign.Builder的构建过程

上面我们得出结论:FeignClient是通过Feign.Builder来构建的,生成FeignClient动态代理的入口是FeignClientFactoryBean#getObject(),这里我们看一下Feign.Builder是如何构建的?

1)FeignContext上下文的获取

在这里插入图片描述

调用FeignClientFactoryBean#getObject()创建/获取FeignClient动态代理类时,首先要通过

FeignContext context = applicationContext.getBean(FeignContext.class);

获取Feign的上下文FeignContext在这里插入图片描述
这里的applicationContextAnnotationConfigServletWebApplicationContext

在Ribbon系列中我们聊了ribbon里有一个SpringClientFactory,就是对每个服务的调用,都会有一个独立的ILoadBalancer,IoadBalancer里面的IRule、IPing都是独立的组件,也就是说ribbon要调用的每个服务都对应一个独立的spring容器;从那个独立的spring容器中,可以取出某个服务关联的属于自己的LoadBalancer、IRule、IPing等。

FeignContext 也是类似的上下文:

我们如果要调用一个服务的话,ServiceA,那么那个服务(ServiceA)就会关联一个独立的spring容器,FeignContext(代表了一个独立的容器),关联着自己独立的一些组件,比如说独立的Logger组件,独立的Decoder组件,独立的Encoder组件,都是某个服务自己独立的。

  • 因此,可以对不同的@FeignClient自定义不同的Configuration。

FeignContext在哪里注入到Spring容器的?

FeignContext位于spring-cloud-openfeign-core项目,我们在这个项目下结合SpringBoot自动装配的特性找XxxAutoConfiguration 或 XxxConfiguration,最终找到FeignAutoConfiguration
在这里插入图片描述

FeignAutoConfiguration中使用@Bean方法将FeignContext注入到Spring容器;

在这里插入图片描述

FeignContext继承自NamedContextFactory,内部负责对每个服务都维护一个对应的spring容器(以map存储,一个服务对应一个spring容器);此处和Ribbon一样,可以参考Ribbon的文章(SpringCloud之Ribbon和服务注册中心的集成细节)。

进入到feign()方法中,以获取FeignLoggerFactory为例:
在这里插入图片描述
get(FeignContext context, Class type)方法要做的事情如下:

  • 根据服务名称(ServiceA)去FeignContext里面去获取对应的FeignLoggerFactory;
  • 其实就是根据ServiceA服务名称,先获取对应的spring容器,然后从那个spring容器中,获取自己独立的一个FeignLoggerFactory;

默认使用的FeignLoggerFactory是在spring-cloud-openfeign-core项目的FeignClientsConfiguration类中加载的DefaultFeignLoggerFactory,而DefaultFeignLoggerFactory中默认创建的是Slf4jLogger
在这里插入图片描述

2)从FeignContext中获取Feign.Builder

这里和上面获取FeignLoggerFactory一样,在spring-cloud-openfeign-core项目的FeignClientsConfiguration类中会找到两个Feign.Builder(一个和Hystrix相关,另外一个Retryer相关的(请求超时、失败重试))的注册逻辑:
在这里插入图片描述
由于默认feign.hystrix.enabled属性为false,所以默认注入的Feign.Builder是Feign.builder().retryer(retryer)

3)处理配置信息

回到feign()方法,其中调用的configureFeign(context, builder)方法负责处理Feign的相关配置(即:使用application.yml中配置的参数,来设置Feign.Builder)。
在这里插入图片描述
在这里插入图片描述

逻辑解析:

  1. FeignClientProperties是针对FeignClient的配置;
  2. 先读取application.yml中的feign.client打头的一些参数,包括了connectionTimeout、readTimeout之类的参数;如果application.yml中没有配置feign.client相关参数,则使用默认配置(Retryer retryer、ErrorDecoder、Request.Options等);
  3. 然后读取application.yml中针对当前要调用服务的配置;

所以如果在application.yml文件中同时配置了针对全部服务和单个服务的配置,则针对单个服务的配置优先级最高,因为在代码解析中它是放在后面解析的,会覆盖调前面解析的内容。

4)使用Feign.Builder构建出一个FeignClient

在这里插入图片描述
如果在@FeignClient上,没有配置url属性,也就是没有指定服务的url地址;那么Feign就会自动跟ribbon关联起来,采用ribbon来进行负载均衡,直接拿出@FeignClient中配置的name()为Ribbon准备对应的url地址:http://ServiceA

此外如果在@FeignClient注解中配置了path属性,就表示要访问的是这个ServiceA服务的某一类接口,比如:@FeignClient(value = “ServiceA”, path = “/user”),在拼接请求URL地址的时候,就会拼接成:http://ServiceA/user

FeignClientFactoryBean#loadBalance()方法是一个基于ribbon进行负载均衡的FeignClient动态代理生成方法;

其入参包括:

  1. Feign.Builder builder --> FeignClient构造器
  2. FeignContext context --> Feign上下文
  3. HardCodedTarget<T> target,target是一个HardCodedTarget,硬编码的Target,里面包含了接口类型(com.zhss.service.ServiceAClient)、服务名称(ServiceA)、url地址(http://ServiceA)

在这里插入图片描述
loadBalance()方法中首先会获取Client 和 Targeter;

  • 通过Client client = getOptional(context, Client.class)方法获取Client,返回的是LoadBalancerFeignClient,(高版本是FeignBlockingLoadBalancerClient);
  • 通过Targeter targeter = get(context, Targeter.class)方法获取Targeter,返回的是HystrixTargeter

下面我们来看一下LoadBalancerFeignClient 和 HystrixTargeter是在哪里注入到Spring容器的?

1> LoadBalancerFeignClient在哪里注入到Spring容器?

进入到LoadBalancerFeignClient类中,看哪里调用了它唯一一个构造函数;
在这里插入图片描述

找到LoadBalancerFeignClient发现有三个地方调用了它的构造函数,new了一个实例;

  • DefaultFeignLoadBalancedConfiguration
  • HttpClientFeignLoadBalancedConfiguration
  • OkHttpFeignLoadBalancedConfiguration

再结合默认的配置,只有DefaultFeignLoadBalancedConfiguration中的Client符合条件装配;

在这里插入图片描述
可以通过引入Apache HttpClient的maven依赖使用HttpClientFeignLoadBalancedConfiguration,

或引入OkHttpClient的maven依赖并在application.yml文件中指定feign.okhttp.enabled属性为true使用OkHttpFeignLoadBalancedConfiguration。

2> HystrixTargeter在哪里注入到Spring容器?

FeignAutoConfiguration类中可以找到Targeter注入到Spring容器的逻辑;

在这里插入图片描述

默认是创建HystrixTargeter;

  1. 如果有feign.hystrix.HystrixFeign这个类的话,那么就会构造一个HystrixTargeter出来;
  2. 如果没有feign.hystrix.HystrixFeign这个类的话,那么就会构造一个DefaultTargeter出来;

HystrixTargeter是用来让feign和hystrix整合使用的,在发送请求的时候可以基于hystrix实现熔断、限流、降级。

  • 生产环境如果启用feign.hystrix.enabled,则Feign.Builder也会变成HystrixFeign.Builder,默认还是Feign自己的Feign.Builder;

3> HystrixTargeter#target()方法

继续往下走,进入到HystrixTargeter#target()方法,具体代码执行流程如下:

在这里插入图片描述

Feign#target()方法中主要做两件事:

  1. build()方法将Feign.Builder中所有东西集成在一起,构建成一个ReflectiveFeign;
  2. ReflectiveFeign#newInstance()方法负责生成动态代理;

4> ReflectiveFeign#newInstance()生成动态代理类

ReflectiveFeign#newInstance()源代码如下:

@Override
public <T> T newInstance(Target<T> target) {
    
    
    // 基于我们配置的Contract、Encoder等一堆组件,加上Target对象(知道是ServiceAClient接口),去进行接口的所有spring mvc注解的解析,以及接口中各个方法的一些解析,获取了这个接口中有哪些方法
    Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
    Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
    List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();

    // 遍历ServiceAClient接口中的每个方法,
    for (Method method : target.type().getMethods()) {
    
    
        if (method.getDeclaringClass() == Object.class) {
    
    
            continue;
        } else if (Util.isDefault(method)) {
    
    
            DefaultMethodHandler handler = new DefaultMethodHandler(method);
            defaultMethodHandlers.add(handler);
            methodToHandler.put(method, handler);
        } else {
    
    
            // 将ServiceAClient接口中的每个方法,加上对应的nameToHandler中存放的对应的SynchronousMethodHandler(异步化的方法代理处理组件),放到一个map中去,
            methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
        }
    }
    
    // (JDK动态代理)基于一个factory工厂,创建了一个InvocationHandler
    InvocationHandler handler = factory.create(target, methodToHandler);
    // 基于JDK的动态代理,创建出来了一个动态代理类:Proxy,其实现ServiceAClient接口
    // new Class<?>[]{target.type()},这个就是ServiceAClient接口
    // InvocationHandler:对上面proxy动态代理类所有方法的调用,都会走这个InvocationHandler的拦截方法,由这个InvocationHandler中的一个方法来提供所有方法的一个实现的逻辑。
    T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
                                         new Class<?>[] {
    
    target.type()}, handler);

    for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
    
    
        defaultMethodHandler.bindTo(proxy);
    }
    return proxy;
}

方法中有两个Map类型的局部变量:nameToHandler、methodToHandler;

  1. nameToHandler 释义:接口中的每个方法的名称,对应一个处理这个方法的SynchronousMethodHandler由ReflectiveFeign的内部类ParseHandlersByName的apply(target)方法获取。
  2. methodToHandler 释义:接口中的每个方法(Method对象),对应一个处理这个方法的SynchronousMethodHandler

在这里插入图片描述
上述代码段中,就是JDK动态代理的体现;动态生成一个没有名字的匿名类,这个类实现了ServiceAClient(FeignClient)接口,基于这个匿名的类创建一个对象(T proxy),这就是所谓的动态代理;后续所有对这个T proxy对象所有接口方法的调用,都会交给InvocationHandler来处理,此处的InvocationHandler是ReflectiveFeign的内部类FeignInvocationHandler

下面我们继续看ReflectiveFeign的内部类ParseHandlersByName的apply(target)方法如何解析FeignClient中的方法?

<1> FeignClient接口和MethodHandler的映射map生成机制流程图

在这里插入图片描述

<2> ParseHandlersByName#apply()解析FeignClient中的方法

ParseHandlersByName#apply()方法会对我们定义的ServiceAClient接口进行解析,解析里面有哪些方法,然后为每个方法创建一个SynchronousMethodHandler出来,也就是说某个SynchronousMethodHandler专门用来处理那个方法的请求调用。

apply()方法逻辑如下:

在这里插入图片描述
其中 (1)factory.create()会为所有标注了SpringMvc注解的方法都生成一个对应的SynchronousMethodHandler
在这里插入图片描述
(2)SpringMvcContract.parseAndValidateMetadata()方法负责解析FeignClient接口中每个标注了SpringMVC注解的方法;即:Feign依靠Contract组件(SpringMvcContract)来解析接口上的spring mvc注解;

针对FeignClient接口中的每个标注了SpringMVC注解的方法都会被SpringMvcContract组件解析,针对每个方法最后都生成一个MethodMetadata,代表方法的一些元数据,包括:

  1. 方法的定义,比如:ServiceAClient#deleteUser(Long)
  2. 方法的返回类型,比如:class java.lang.String
  3. 发送HTTP请求的模板,比如:DELETE /user/{id} HTTP/1.1

5> SpringMvcContract组件的工作原理

这里看一下SpringMvcContract.parseAndValidateMetadata()是如何解析FeignClient中的每个方法。

以如下方法为例:
在这里插入图片描述

示例解析逻辑如下:

  1. 解析@RequestMapping注解,看看里面的method属性是什么?是GET/UPDATE/DELETE,然后在HTTP template里就加上GET/UPDATE/DELETE;(示例为DELETE)
  2. 找到接口上定义的@RequestMapping注解,解析里面的value值,拿到请求路径(/user),此时HTTP template变成:DELETE /user;
  3. 再次解析deleteUser()方法上的@RequestMapping注解,找到里面的value,获取到/{id},拼接到HTTP template里去:DELETE /user/{id}
  4. 接着硬编码拼死一个HTTP协议,http 1.1,HTTP template:DELETE /user/{id} HTTP/1.1
  5. indexToName:解析@PathVariable注解,第一个占位符(index是0)要替换成方法入参里的id这个参数的值。
  6. 假如后面来调用这个deleteUser()方法,传递进来的id = 1.那么此时就会拿出之前解析好的HTTP template:DELETE /user/{id} HTTP/1.1。然后用传递进来的id = 1替换掉第一个占位符的值,DELETE /user/1 HTTP/1.1

三、总结和后续文章

本文我们聊了OpenFeign如何为FeignClient生成动态代理类,SpringMvcContract组件如何解析FeignClient中标注了SpringMVC注解的方法。

下篇文章我们接着聊OpenFeign接收到一个请求如何处理?

猜你喜欢

转载自blog.csdn.net/Saintmm/article/details/125651054
今日推荐