OpenFeign调用服务的核心原理解析
引言OpenFeign在分布式服务中运用非常广泛,他和RPC所要达到的效果一致,就是为了简化远程服务调用的操作,通过使用OpenFeign可以使得调用远程服务就像调用本地服务一样方便。但是其和RPC在实现上还是不太一样,不一样的地方主要还是调用的方式,OpenFeign是内部实现了rest服务调用,从而一个本地服务调用远程服务的接口时,主要还是通过rest服务调用的方式,那么对于服务端的要求就是其
引言
OpenFeign
在分布式服务中运用非常广泛,他和RPC
所要达到的效果一致,就是为了简化远程服务调用的操作,通过使用OpenFeign
可以使得调用远程服务就像调用本地服务一样方便。
但是其和RPC
在实现上还是不太一样,不一样的地方主要还是调用的方式,OpenFeign
是内部实现了rest服务调用,从而一个本地服务调用远程服务的接口时,主要还是通过rest服务调用的方式,那么对于服务端的要求就是其服务需要将这个接口通过rest暴露出来,不然OpenFeign
将无法工作;而RPC则不需要服务端将该接口以rest服务的形式暴露出来,而是直接通过底层netty进行通信来互相沟通的。
上图就是RPC
和REST
的主要区别。
在了解OpenFeign
的主要功能后,我们可以自己思考一下,如果我们自己来实现这个功能,我们需要实现哪些地方才能使得这个功能正常运行起来。
一、获取调用服务的信息
这个服务信息包括服务提供方的ip,端口,因为只要知道这些我们才能和服务建立连接。知道了服务的这些信息,我们还需要知道调用服务的rest接口信息,不然OpenFeign
也不知道你想要调用服务提供方的哪个实现。
OpenFeign实现
带着上面的问题我们可以去OpenFeign
中去寻找他是如何实现的。OpenFeign
是把这些信息放在注解上的
其中@FeignClient
上的name存放了服务的名称,fallbackFactory
是失败后的降级工厂,对于每一个接口可以在接口上添加Rest注解,例如图中的@GetMapping
,这就表示了这个接口对应的rest信息。
相信大家这时有很多疑问了。
咋没有看到服务提供方的ip,不是说必须要有服务提供方的网络地址才能和服务方进行通信么?其实在获取到@FeignClient
上的name存放了服务的名称后,OpenFeign
会拿着这个服务名称去注册中心中寻找对应的服务的网络地址。那么在注册中心中存在多个这个服务名称的服务咋办,这个确实有可能,因为现在分布式服务中服务往往会进行水平扩展来增加服务的可用性。那么OpenFeign
如果在获取到多个服务时,其内置有ribbon
会挑选其中一个服务器地址。
@OpenFeign
是如何解析获取到这些信息的呢?
1、获取到所有@FeignClient
标识的接口
我们需要在程序启动处添加@EnableFeignClients
注解,该注解会导入FeignClientsRegistrar
类进入到spring容器中,该类实现了ImportBeanDefinitionRegistrar
接口,在spring启动的过程中会调用FeignClientsRegistrar
中实现的registerBeanDefinitions
,(如果大家有对这方便不太熟悉,建立去大致学习一下spring boot的启动过程,其实大部分三方组件实现接入的方式都大致相同),那我们就去看下FeignClientsRegistrar#registerBeanDefinitions
方法。
我们就看下registerFeignClients
方法
org.springframework.cloud.openfeign.FeignClientsRegistrar#registerFeignClients
第一部分是为了找到@Feignclient
标识的接口类,第二部分就是对找出的接口类进行处理了处理,主要关注registerClientConfiguration
和registerFeignClient
函数。
其中registerClientConfiguration
是为了处理@FeignClient#configuration
属性的,在这个函数会往spirng容器中添加#{serviceName}.FeignClientSpecification
作为名字的FeignClientSpecification类对象,例如user-center.FeignClientSpecification。那么这个对象有什么作用呢?按住不表,下面再讲。
而registerFeignClient
函数则是处理接口类的主要方法了。我们在之前考虑到,我们在接口上填写了@FeignClient
注解,在之后程序中我们可以直接引用这个接口对象来调用接口上的函数,理论分析一波:接口如果没有实现类,是不能直接在spring中直接进行注入并调用相应的方法的,一定需要我们去实现这个接口,那么我们可以想到,OpenFeign
中一定做了这样的操作。
接下来就来看下registerFeignClient
函数
org.springframework.cloud.openfeign.FeignClientsRegistrar#registerFeignClient
注意咯,敲黑板咯,上面截图的就是OpenFeign
对于接口的动态实现。这时候可能会有同学问,什么,真的么,factoryBean
是啥东东,这代码里我没有看到半点动态代理的影子。
确实在上面我们只看到factoryBean
的组装过程,我们看到往factoryBean
中塞了好多属性,只有最后调用了factoryBean.getObject()
产生了BeanDefinitionBuilder
对象,这个对象说的是OpenFeign
对于接口的动态实现类。那么我们就一起来看下factoryBean
是个神马东西,
这个factoryBean
就是上面的FeignClientFactoryBean
的实现部分,而他就是利用getTarget()
方法来依次处理最终得到了生成的实例。在上面流转图中就已经解析好了@EnableFeign
和@FeignClient
注解中的信息,在解析完数据后就应该考虑如何生成@FeignClient
标识接口的动态代理。
二、生成接口的动态代理
大家想一下,在咱们java中对于动态代理是不是笼统就两种
- 基于JDK的动态代理
- CGLib实现
两种实现方式的区别,或者可以说,为啥有了JDK这个原生实现后,又有大佬实现了CGLib,原因就是CGLib实现动态代理不依赖于接口,可以对某一个类进行扩展继承,而JDK若要进行动态代理,则必须要一个接口。在一般情况下,如果存在接口的时,还是会选择JDK实现。那么在我们的OpenFeign
中,是存在接口的,通过猜想和源码验证,确实在OpenFeign
中是使用的JDK中的方式来实现动态代理的。
在上面的流程图中,我们可以知道动态代理的实现是在ReflectiveFeign#newInstance
方法中,那么我们就进入到这个方法的学习
feign.ReflectiveFeign#newInstance
下面对上面三步分别解析下
第一步分析
我们可以看到第三步就是创建了接口的动态代理对象,这个动态代理对象中最应该关心的就是这个handler
,这个是在第一步和第二步进行处理的,那么我们就来看下第一步是在干啥?
feign.ReflectiveFeign.ParseHandlersByName#apply
第一步就是获取到接口类中方法,然后返回以#{interfaceClassName} (#) #{methodName}(#{paramType})
为key,方法对应的methodHandler
作为value的map。而这个methodHandler
就是SynchronousMethodHandler.Factory#create
创建的SynchronousMethodHandler
对象,这个SynchronousMethodHandler
对象中包含了许多配置信息,包括对应代理方法的元信息。
第二步分析
可以分析第二步输入的数据和输出的数据,我们可以知道其实第二步就是将map中的key从一开始的#{interfaceClassName} (#) #{methodName}(#{paramType})
形式的String转成了具体的Method对象,那么为啥要这么转一下呢?让我们带着问题来看第三步。
第三步
其中Proxy.newProxyInstance
这个方法是固定用法,我们就不需要关注,只需要注意
InvocationHandler handler = factory.create(target, methodToHandler);
这个第三步就是创建了InvocateHandler
对象,其实际类是FeignInvocationHandler
,在这个类的invoke
方法上,该方法就是在执行代理方法时都会先走到这个invoke
方法(这个是属于动态代理方面的知识,如有有同学对一块还不太熟,建议先去了解一下动态代理的实现),在invoke
方法中,参数就有当前的method,这样在第二步中转成<Method,MethodHandler>
的map就可以直接通过method去获取到对应的MethodHandler方法,从而调用对应的MethodHandler方法。
三、具体调用实现方式
在生成代理对象后,我们调用对应的方法后就会自动调用代理对象的invoke
方法,在OpenFeign
中就会调用到对应SynchronousMethodHandler#invoke
方法。
feign.SynchronousMethodHandler#invoke
RequestTemplate
就是http请求的对象了,里面包含了本次http请求的所有相关配置数据,里面的逻辑有点复杂,如果有同学感兴趣可以自己去研究一下这个http请求是如何生成的。下图就是这个RequestTemplate
的数据格式
我们主要来看下executeAndDecode
方法
feign.SynchronousMethodHandler#executeAndDecode
哦豁,这里内容还挺多,但是快到终点了,这里还是分为3部分,我们先来看下第一部分是在干啥?
第一步
啪,很快啊,直接进入这个方法的实现
feign.SynchronousMethodHandler#targetRequest
显而易见,这不是就是在发送前执行切面么。那么我们进入到RequestInterceptor
这解释非常好理解呀,并且可能有小伙伴在平时工作中也用过这个类似的功能,俺举个栗子
这段代码就是创建了一个RequestInterceptor
对象,他的作用就是我们在使用OpenFeign
调用时往http请求的head上添加traceId,方便我们进行链式追踪。这样第一步的作用相信讲的hin清楚了吧。让我们进入到第二步。
第二步
非常明显,就是发送http请求的,然后接收到reponse。这里就不再细讲了。
第三步
代码上说明的就是如果配置了decode
对象,那么直接用decode
去解码reponse,如果没有配置,则使用AsyncResponseHandler#handleResponse
方法去处理reponse,那么首先这个deocde
是在哪里配置的呢?经过我代码搜寻,发现了这个是创建SynchronousMethodHandler
对象时传入的forceDecoding
参数来控制的,具体截图就不放了,大家可以自己去找下,并且这forceDecoding
默认值是false,也就是说,一般情况下处理reponse的方式都是走的AsyncResponseHandler#handleResponse
方法,这个方法里就有OpenFeign
对于response的各种处理。
最后大家可能会有疑问,哇,这个处理reponse的AsyncResponseHandler
,看这个名字是异步的呀,那OpenFeign
难道也是异步返回结果的么?答案是否定的,具体我们来看下代码
feign.SynchronousMethodHandler#executeAndDecode
这边也是第三部分的代码截图,只是上面最后的一丢丢截图截不下了,可以看到,这边最后是调用了CompleteFuture#join
方法,这个方法会堵塞主线程,直到resultFuture
结果返回,也就是说处理reponse的过程是异步的,但是返回结果还是同步的,在最后结果处理结束之前,主线程都是堵塞的。
篇外
这里再插一句哈,就是大家可能对于Future类的方法,其Futre#get
的方法用的比较多,也知道这个方法也是等待子线程处理结束的作用,那么和CompleteFuture#join
方法有什么区别呢?其实我感觉最主要的区别就是使用了join方法不用我们自己包裹一个try-catch方法来捕获异常,join方法自己会返回一个异常,而get方法则需要我们自己包裹try-catch来捕获异常。
结束
本篇文章是笔者自己的理解,如果有描述不正确的地方,欢迎大家批评指正,一起进步~~
更多推荐
所有评论(0)