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

网友投稿 254 2022-08-23

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

文章目录

​​一、前言​​

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

​​二、生成FeignClient的动态代理类​​

​​1、FeignClientFactoryBean创建动态代理类的入口​​​​2、Feign.Builder的构建过程​​

​​1)FeignContext上下文的获取​​​​2)从FeignContext中获取Feign.Builder​​​​3)处理配置信息​​​​4)使用Feign.Builder构建出一个FeignClient​​

​​1> LoadBalancerFeignClient在哪里注入到Spring容器?​​​​2> HystrixTargeter在哪里注入到Spring容器?​​​​3> HystrixTargeter#target()方法​​​​4> ReflectiveFeign#newInstance()生成动态代理类​​

​​<1> FeignClient接口和MethodHandler的映射map生成机制流程图​​​​<2> ParseHandlersByName#apply()解析FeignClient中的方法​​

​​5> SpringMvcContract组件的工作原理​​

​​三、总结和后续文章​​

一、前言

在前面的文章:

我们聊了以下内容:

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

PS:本文基于的SpringCloud版本

2.3.7.RELEASE Hoxton.SR9 2.2.6.RELEASE org.springframework.boot spring-boot-dependencies ${spring-boot.version} pom import org.springframework.cloud spring-cloud-dependencies ${spring-cloud.version} pom import com.alibaba.cloud spring-cloud-alibaba-dependencies ${spring-cloud-alibaba.version} pom import

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

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

二、生成FeignClient的动态代理类

在​​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​​。

这里的​​applicationContext​​​是​​AnnotationConfigServletWebApplicationContext​​。

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

​​FeignContext​​

我们如果要调用一个服务的话,ServiceA,那么那个服务(ServiceA)就会关联一个独立的spring容器;关联着自己独立的一些组件,比如说独立的Logger组件,独立的Decoder组件,独立的Encoder组件;FeignContext则代表了一个独立的容器工厂,里面记录了每个服务对应的容器​​AnnotationConfigApplicationContext​​。因此,可以对不同的@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)。

逻辑解析:

​​FeignClientProperties​​是针对FeignClient的配置;先读取application.yml中的feign.client打头的一些参数,包括了connectionTimeout、readTimeout之类的参数;如果application.yml中没有配置feign.client相关参数,则使用默认配置(Retryer retryer、ErrorDecoder、Request.Options等);然后读取application.yml中针对当前要调用服务的配置;所以如果在application.yml文件中同时配置了针对全部服务和单个服务的配置,则针对单个服务的配置优先级最高,因为在代码解析中它是放在后面解析的,会覆盖调前面解析的内容。

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

如果在@FeignClient上,没有配置​​url属性​​​,也就是没有指定服务的url地址;那么Feign就会自动跟ribbon关联起来,采用ribbon来进行负载均衡,直接拿出@FeignClient中配置的name()为Ribbon准备对应的url地址:​​= “ServiceA”, path = “/user”),在拼接请求URL地址的时候,就会拼接成:​​builder --> FeignClient构造器FeignContext context --> Feign上下文​​HardCodedTarget​​​ target,​​target​​是一个HardCodedTarget,硬编码的Target,里面包含了接口类型(com.zhss.service.ServiceAClient)、服务名称(ServiceA)、url地址(和 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了一个实例;

DefaultFeignLoadBalancedConfigurationHttpClientFeignLoadBalancedConfigurationOkHttpFeignLoadBalancedConfiguration

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

可以通过引入Apache HttpClient的maven依赖使用HttpClientFeignLoadBalancedConfiguration,

或引入OkHttpClient的maven依赖并在application.yml文件中指定​​feign.okHystrixTargeter在哪里注入到Spring容器?

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

默认是创建HystrixTargeter;如果有feign.hystrix.HystrixFeign这个类的话,那么就会构造一个HystrixTargeter出来;如果没有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()​​方法中主要做两件事:

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

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

​​ReflectiveFeign#newInstance()​​源代码如下:

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

nameToHandler释义:接口中的每个方法的名称,对应一个处理这个方法的​​SynchronousMethodHandler​​;由ReflectiveFeign的内部类ParseHandlersByName的apply(target)方法获取。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,代表方法的一些元数据,包括:方法的定义,比如:ServiceAClient#deleteUser(Long)方法的返回类型,比如:class java.lang.String发送HTTP请求的模板,比如:DELETE /user/{id} HTTP/1.1

5> SpringMvcContract组件的工作原理

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

以如下方法为例:

示例解析逻辑如下:

解析@RequestMapping注解,看看里面的method属性是什么?是GET/UPDATE/DELETE,然后在HTTP template里就加上GET/UPDATE/DELETE;(示例为DELETE)找到接口上定义的@RequestMapping注解,解析里面的value值,拿到请求路径(/user),此时HTTP template变成:DELETE /user;再次解析deleteUser()方法上的@RequestMapping注解,找到里面的value,获取到/{id},拼接到HTTP template里去:DELETE /user/{id}接着硬编码拼死一个HTTP协议,1.1,HTTP template:DELETE /user/{id} HTTP/1.1indexToName:解析@PathVariable注解,第一个占位符(index是0)要替换成方法入参里的id这个参数的值。假如后面来调用这个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接收到一个请求如何处理?

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:【云原生】原来2020.0.X版本开始的OpenFeign底层不再使用Ribbon了
下一篇:妇炎洁擦边球营销翻车,背后的仁和药业是什么来头?(仁和妇炎洁广告)
相关文章

 发表评论

暂时没有评论,来抢沙发吧~