
SpringCloud系列—Spring Cloud 源码分析之OpenFeign
作者 | 宇木木兮
来源 |今日头条
学习目标
- 为什么加一个注解就能实现远程过程调用呢?推导它底层的实现主流程?
- OpenFeign怎么实现RPC的基本功能的
- 通过源码验证
第1章 OpenFeign主流程推导
要明确OpenFeign的主流程首先我们还是要明确它的核心目标是什么?
说白了,OpenFeign最核心的目标就是让客户端在远程调用过程中不需要做什么多余的操作,只要拿到一个对象,然后调用该对象的方法就好了,剩下的操作都交给OpenFeign去帮你完成,那剩下一些什么操作呢?
- 首先肯定是保证网络通信,那我们大胆地猜测一下,OpenFeign其实底层帮我们封装了请求的地址、端口、请求参数以及响应的参数。
- 其次,当我们要用对象去请求方法,那这个对象是远程的服务,这个对象肯定不简单,这里也大胆猜测一下,该对象也是OpenFeign给我们创建的。
- 然后在调用过程中,如果服务是由多台服务器提供的,那又涉及到负载均衡了,这肯定也是OpenFeign帮我们完成了。
- 除了上面的问题,再联想一下,既然存在服务端是集群的情况,那服务端的地址和端口还需要一个注册中心来注册,这肯定也不能由客户端来完成,因为客户端只关注业务代码。那想都不用想,也是OpenFeign来完成了。
OK,上面推导了OpenFeign应该完成的主要目标,接下来我们再来分析分析它是怎么做的。之前的文章有讲过一个概念,不管是什么组件,只要是集成spring或者springboot的话,那一定是想通过spring或者springboot去管理bean对象的创建的,当通过容器拿到对象之后再去调用对象的核心方法,那OpenFeign在集成springboot的时候理念也应该是这样。
- 所以第一步,OpenFeign集成springboot,通过springboot拿到核心bean对象,例如上图中的userService对象。
- 这个对象肯定不简单,不可能只有getUser的功能。那想一想,spring中做功能增强可以用什么来做呢?——代理嘛,那这个对象的类型就呼之欲出了,代理对象。
- 调用代理对象的方法时,流程先进入到invoke中,在这个invoke中,做的增强包括了负载均衡LoadBalance,因为我在真正调用getUser的时候要知道具体是调用哪台服务器的服务。
- 负载均衡做完就得拼接具体的http请求参数,请求头,请求地址,请求端口了。
OK,以上分析了OpenFeign底层要实现的具体功能,也分析了它的处理流程,那么接下来我们通过源码来验证一下,它是不是这么玩的。
第2章 源码验证
2.1 EnableFeignClients
我们从下面这个注解进行切入,这个注解开启了FeignClient的解析过程。
这个注解的声明如下,它用到了一个@Import注解,我们知道Import是用来导入一个配置类的,接下来去看一下FeignClientsRegistrar的定义。
FeignClientsRegistrar实现了
ImportBeanDefinitionRegistrar,它是一个动态注入bean的接口,Spring Boot启动的时候,会去调用这个类中的registerBeanDefinitions来实现动态Bean的装载。registerBeanDefinitions是在spring容器启动时执行invokeBeanFactoryPostProcessors方法,然后对相应的类进行解析注册,它的作用类似于ImportSelector。
2.1.1 ImportBeanDefinitionRegistrar
简单给大家演示一下
ImportBeanDefinitionRegistrar的作用。
- 定义一个需要被装载到IOC容器中的类HelloService
- 定义一个Registrar的实现,定义一个bean,装载到IOC容器
- 定义一个注解类
- 启动类
- 通过结果演示可以发现,HelloService这个bean 已经装载到了IOC容器。
这就是动态装载的功能实现,它相比于@Configuration配置注入,会多了很多的灵活性。 ok,再回到FeignClient的解析中来。
2.1.2 FeignClientsRegistrar
- registerDefaultConfiguration 方法内部从 SpringBoot 启动类上检查是否有@EnableFeignClients, 有该注解的话, 则完成 Feign 框架相关的一些配置内容注册
- registerFeignClients 方法内部从 classpath 中, 扫描获得 @FeignClient 修饰的类, 将类的内容解析为 BeanDefinition , 最终通过调用 Spring 框架中的BeanDefinitionReaderUtils.resgisterBeanDefinition 将解析处理过的 FeignClientBeanDeifinition 添加到 spring 容器中.
2.2 registerDefaultConfiguration
方法的入参BeanDefinitionRegistry是spring框架用于动态注册BeanDefinition信息的接口,调用registerBeanDefinition方法可以将BeanDefinition注册到Spring容器中,其中name属性就是注册的BeanDefinition的名称,在这里它注册了一个FeignClientSpecification的对象。
FeignClientSpecification实现了
NamedContextFactory.Specification接口,它是Feign实例化的重要一环,在上面的方法中,它持有自定义配置的组件实例,SpringCloud使用NamedContextFactory创建一些列的运行上下文ApplicationContext来让对应的Specification在这些上下文中创建实例对象。
NamedContextFactory有3个功能:
- 创建AnnotationConfigApplicationContext上下文。
- 在上下文中创建并获取bean实例。
- 当上下文销毁时清除其中的feign实例。
NamedContextFactory有个非常重要的子类FeignContext,用于存储各种OpenFeign的组件实例。
FeignContext是哪里构建的呢?
配置见:
pring-cloud-openfeign-core-2.2.3.RELEASE.jar!\META-INF\spring.factories2.2.1 FeignAutoConfiguration
将默认的FeignClientsConfiguration作为参数传递给构造函数
FeignContext创建的时候会将之前FeignClientSpecification通过setConfigurations设置给context上下文。
2.2.2 createContext
代码详见:
org.springframework.cloud.context.named.NamedContextFactory#createContext方法。
FeignContext的父类的createContext方法会将创建
AnnotationConfigApplicationContext实例,这实例将作为当前上下文的子上下文,用于关联feign组件的不同实例。在调用FeignClientFactoryBean的getObject方法时调用。(createContext调用在下文会讲解)
由于NamedContextFactory实现了DisposableBean,所以当实例消亡的时候会调用
总结:NamedContextFactory会创建出
AnnotationConfigApplicationContext实例,并以name作为唯一标识,然后每个AnnotationConfigApplicationContext实例都会注册部分配置类,从而可以给出一系列的基于配置类生成的组件实例,这样就可以基于name来管理一系列的组件实例,为不同的FeignClient准备不同配置组件实例。
2.3 registerFeignClients
这个方法主要是扫描类路径下所有的@FeignClient注解,然后进行动态Bean的注入。它最终会调用 registerFeignClient 方法。
在这个方法中,就是去组装BeanDefinition,也就是Bean的定义,然后注册到Spring IOC容器。
我们关注一下,BeanDefinitionBuilder是用来构建一个BeanDefinition的,它是通过genericBeanDefinition 来构建的,并且传入了一个FeignClientFactoryBean的类。
我们可以发现,FeignClient被动态注册成了一个FactoryBean
Spring Cloud FengnClient实际上是利用Spring的代理工厂来生成代理类,所以在这里才会把所有的FeignClient的BeanDefinition设置为FeignClientFactoryBean类型,而FeignClientFactoryBean继承自FactoryBean,它是一个工厂Bean。
在Spring中,FactoryBean是一个工厂Bean,用来创建代理Bean。
工厂 Bean 是一种特殊的 Bean, 对于 Bean 的消费者来说, 他逻辑上是感知不到这个 Bean 是普通的 Bean 还是工厂 Bean, 只是按照正常的获取 Bean 方式去调用, 但工厂bean 最后返回的实例不是工厂Bean 本身, 而是执行工厂 Bean 的 getObject 逻辑返回的示例。(也就是在实例化工厂Bean的时候会去调用它的getObject方法)
简单来说,FeignClient标注的这个接口,会通过
FeignClientFactoryBean.getObject()这个方法获得一个代理对象。
2.3.1 FeignClientFactoryBean.getObject
getObject调用的是getTarget方法,它从applicationContext取出FeignContext,FeignContext继承了NamedContextFactory,它是用来来统一维护feign中各个feign客户端相互隔离的上下文。
接着,构建feign.builder,在构建时会向FeignContext获取配置的Encoder,Decoder等各种信息。FeignContext在上篇中已经提到会为每个Feign客户端分配了一个容器,它们的父容器就是spring容器
配置完Feign.Builder之后,再判断是否需要LoadBalance,如果需要,则通过LoadBalance的方法来设置。实际上他们最终调用的是Target.target()方法。
2.3.2 loadBalance
生成具备负载均衡能力的feign客户端,为feign客户端构建起绑定负载均衡客户端.
Client client = (Client)this.getOptional(context, Client.class); 从上下文中获取一个Client,默认是LoadBalancerFeignClient。
它是在FeignRibbonClientAutoConfiguration这个自动装配类中,通过Import实现的
2.3.3 DefaultTarget.target
2.3.4 ReflectiveFeign.newInstance
这个方法是用来创建一个动态代理的方法,在生成动态代理之前,会根据Contract协议(协议解析规则,解析接口类的注解信息,解析成内部的MethodHandler的处理方式。
从实现的代码中可以看到熟悉的Proxy.newProxyInstance方法产生代理类。而这里需要对每个定义的接口方法进行特定的处理实现,所以这里会出现一个MethodHandler的概念,就是对应方法级别的InvocationHandler。
2.4 接口定义的参数解析
根据FeignClient接口的描述解析出对应的请求数据。
2.4.1 targetToHandlersByName.apply(target)
根据Contract协议规则,解析接口类的注解信息,解析成内部表现:
targetToHandlersByName.apply(target);会解析接口方法上的注解,从而解析出方法粒度的特定的配置信息,然后生产一个SynchronousMethodHandler 然后需要维护一个<method,MethodHandler>的map,放入InvocationHandler的实现FeignInvocationHandler中。
2.4.2 SpringMvcContract
当前Spring Cloud 微服务解决方案中,为了降低学习成本,采用了Spring MVC的部分注解来完成 请求议解析,也就是说 ,写客户端请求接口和像写服务端代码一样:客户端和服务端可以通过SDK的方式进行约定,客户端只需要引入服务端发布的SDK API,就可以使用面向接口的编码方式对接服务。
该类继承了Contract.BaseContract并实现了ResourceLoaderAware接口,
其作用就是对RequestMapping、RequestParam、RequestHeader等注解进行解析的。
2.5 OpenFeign调用过程
在前面的分析中,我们知道OpenFeign最终返回的是一个#
ReflectiveFeign.FeignInvocationHandler的对象。
那么当客户端发起请求时,会进入到
FeignInvocationHandler.invoke方法中,这个大家都知道,它是一个动态代理的实现。
而接着,在invoke方法中,会调用 this.dispatch.get(method)).invoke(args) 。this.dispatch.get(method) 会返回一个SynchronousMethodHandler,进行拦截处理。
这个方法会根据参数生成完成的RequestTemplate对象,这个对象是Http请求的模版,代码如下。
2.5.1 executeAndDecode
经过上述的代码,我们已经将restTemplate拼装完成,上面的代码中有一个 executeAndDecode() 方法,该方法通过RequestTemplate生成Request请求对象,然后利用Http Client获取response,来获取响应信息。
2.5.2 Client.execute
默认采用JDK的 HttpURLConnection 发起远程调用。
