feign源码
㈠ SpringCloud系列之Feign-6.Feign上下文构建解析
1.首先在构建上下文的入口是在FeignClientFactoryBean的getObject方法
2.首先第一步就是构建了FeignContext类
3.然后我们进入到feign这个方法中看下:
可以看到首先获取两个日志对象一个FeignLoggerFactory,一个Logger,然后就是把这两个日志对象赋值给feign这个对象,我们看的Builder其实类似于方法糖,让赋值更加方便而已,类似于lombok中的@builder注解一样。
除了赋值日志对象以外,还有加密解密的一个赋值操作,这个是url解析过程中也是非常常见的。然后其他的我们暂时不关注,只要知道这里是初始化了feign对象,一个空的对象,构造器。
4.然后我们继续在主方法往下走
在上一个帖子上我们注意到url是空的,那么在这里就需要使用到了,从源码中看到如果url是空的,那么就使用服务名称作为ip了,然后如果不是http开头,这里就会给一个http的前缀上去,因为feign也是基于eureka的http接口的。
我们在看下下一个方法啊 cleanPaht();
这里其实还是拼装url的操作,在@FeignClient注解中有一个path的属性,这个属性干嘛用的呢,如图:
而这个path我们最后是不需要/的,所以这里其实就是格式化一下而已,正确的拼接好url的操作。
5.主方法继续
这里就是构造动态代理对象的地方了。
我们点进去看:
可以看到首先调用了getOptional这个方法,然后使用了
传入了一个调用的服务名称就是 eureka-client这个服务名称,还有一个 feign.Client的类。
其实这个方法就是从上下文中拿到调用了这个服务的接口作为一个client。
然后把client放到Feign对象中,再构建一个Targeter类
6.然后我们进入targeter.target这个方法看看这个最核心的地方
这块逻辑做了一个判断,看这个feign是不是hystrix的feign,如果是就走上面的逻辑,否则就走下面的逻辑,降级熔断啊之类的逻辑,但我们注意到不论走哪个,最终都会走feign.target(target);
7.然后我们进入 feign.target 这个方法:
我们继续进入newInstance这个方法:
上面的三个方法没看懂,我们暂且跳过,看到target通过断点我们可以知道,type就是IService,getMethods就是获取到了FeignClient那个接口下面的所有方法了,现在就是遍历所有方法了,我们现在懂点看下遍历中做的逻辑是什么
8.首先进行判断,看他是对象吗,显然这个是方法不是对象所以跳过,然后Util.isDefault(method)我们看下:
看着大致意思就是判断这个是不是接口而且修饰符必须是这几种;
所以这个判断也没进去,最后执行了下面这个方法:
我们查看下Feign.configKey(target.type(), method) 这个是什么:
发现是这个FeignClient接口的名称#方法名称;
所以这里就在methodToHandler.put如一个方法,还有方法的Handler,所以说这个接口虽然没有实现类,但是转发到了其他的实现类中,那么这个实现类到底是什么,我们继续往下面看:
然后就把这个handler作为参数创建了一个代理proxy并返回,然后如果我们通过Feign访问的话其实访问的就是代理proxy,但是最终干活的其实就是handler。
㈡ Spring Cloud Feign实现自定义复杂对象传参
现我们服务提供端有如下的根据用户查询条件获取满足条件的用户列表controller接口
我们在使用Feign构建远程服务请求客户端的时候,会发现Feign官方版本是不支持GET请求传递自定义的对象,当我们的请求参数很多的时候,我们只能选择以下两种方式:
那么我们希望能有一种方式 保持跟controller完全一致只需要传递自定义的对象 ,既让服务提供端开发人员爽,也让服务消费端开发人员爽,两全其美。既然Feign官方不支持,那我们就自己动手撸源码,自己来实现。
对比之前的@RequestParam和Map用法,方法参数变少了,User对象复用了,对服务提供端和消费端都更方便了
最近在调研spring cloud版本升级,发现新版的Feign也支持了自定义对象传参,实现方式大同小异
㈢ open feign的超时配置及源码跟踪
content:[2021-02-26 18:43:59.939][http-nio-8080-exec-11][ERROR][c.l.c.c.r.RestCartServiceImpl:50] [reqId:] <getMyCartDetail> 操作异常: feign.RetryableException: Read timed out executing POST http://cic-proct-service/cic-proct/getProctListByIds at feign.FeignException.errorExecuting(FeignException.java:249) at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:129) at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:89) at feign.ReflectiveFeign Proxy162.getProctListByIds(Unknown Source)
问题:在application.yaml中配置的feign的超时一直不生效,在网上也找不到相应的合理解决方案,通常的答案是对feign依赖的底层ribbon设置超时来解决,但这个不是官方推荐的方式,所以我就产生了跟踪源码的兴趣。
解决方案:首先我是通过对@FeignClient注解源码进行了断点跟踪,看下在服务启动时,spring boot是如何加载OpenFeign的超时配置的,最后跟踪到
FeignClientFactoryBean的configureUsingProperties方法读取了feign的配置
跟踪到这儿,就可以知道在我们对@FeignClient不进行重写或者不额外指定configuration的配置时,他是默认加载default配置,而这个default配置默认超时是2秒
要想更改这个默认超时时间需要按照官方推荐的格式,才能生效,如下:
按照配置设置后,又对该服务配置做了验证和压测,在4核8g的mac本上,查询购物车服务支持150个并发,异常率0%,比之前的70%异常率,大大降低,至此完美解决线上问题。
㈣ SpringCloud系列之Feign-5.@EnableFeignClients底层机制深度解析
@EnableFeignClients 源码比较值得一读,读完之后我们就学会了如何自己写一个注解并成功运用起来了
1.首先我们进入到 @EnableFeignClients注解里面可以看到这个注解里面声明了几个属性,通过名称大概可以看到比如basePackages应该是包路径,value的话应该是个basePackages别名,我们暂且不管,看下这个注解上面有个@Import的注解,点进去这个类来看一下
2.如图可以看到FeignClientsRegistrar这个类实现了三个Spring的类,根据名称大概猜一下,第一个应该是关于类定义注册的类,第二个第三个相信大家可能用过或者了解过就是Spring的Aware的一些类,大致就是加载资源或者环境变量的类
那我们看一下ImportBeanDefinitionRegistrar类里面是什么:
可以看到里面定义了一个方法,那么我们重新返回到FeignClientsRegistrar看下他是怎么实现这个方法的:
这个方法主要干了两件事儿,第一个方法是注册了默认的配置信息,第二个就是注册FeignClients,我们挨个一个个详细的看下:
到了较为详细的源码时候,如果看不懂,我们最好是打断点,当我们启动了Eureka-server,Eureka-client,然后再启动Feign-consumer的时候,断点就可以进来,我们可以看到一些传参的信息:
可以看到这个metedata里面的数据刚好就是在启动类上面的三个注解,并且还带有三个注解的属性信息,下面再给大家看下主类对照下就懂了:
然后我们继续往下面学习:
这一步就比较好理解了,我们拿到EnableFeignClients这个注解的属性信息。
然后就是拼接了一个名称,这个名称就是启动类的前端加了个default.而已,然后registry没变还是传参过来,defaultAttrs.get("defaultConfiguration")这个属性从刚刚断点来看也是空的。调用了一个registerClientConfiguration方法:
这个方法就是使用了Spring的BeanDefinitionBuilder把FeignClientSpecification这个bean给注册到Spring容器中了。
然后我们继续放回到主方法中看下一个方法:
这个方法根据名称registerFeignClients来说应该就是注册FeignClients类了,进入方法中,第一个scanner我们看下:
有点看不懂,没关系,猜一下,可能是扫描类的工具把。
我们继续王下面走,scanner加载了一个resourceLoader这个类,这个类我们可以查一下,他是Spring框架中与资源相关的类,然后再往下看
下面还是获取主类中的注解EnableFeignClients的属性信息
再往下,我们可以从图中看到在属性中获取关于clients的信息,但是没有,然后scanner就加了一个类似过滤器的东东,然后调用了getBasePackages的方法
下面我们看下getBasePackages方法:
看过之前的方法,这个方法就好理解了,首先就还是获取EnableFeignClients的所有属性信息,然后把值都给取出来,取得属性分别是value还是basePackages等关于包路径的属性值,如果都没有获取到,就获取一个默认的包路径
这个包路径断点可以看到就是主类的包路径,所以整体上看,这些逻辑就是首先看注解中有没有关于FeignClient的包路径信息,如果没有配置,那程序就准备从主程序所在的包路径下找所有的FeignClient了。
我们再详细看下:
然后继续看这个registerClientConfiguration方法:
这个方法我们之前看过就是把某个类加载到Spring中所以继续下一步,看registerFeignClient方法:
获取到这个FeignClient的所有属性之后,我们就进行数据处理,把属性信息都赋值给definition
这个方法表示我们把这个类以 按照类型注入 作为属性,然后
这块逻辑就是为了防止两个有同一个父类的FeignClient出现问题所做的,我们应该都遇到过一个问题就是 使用 @Autowire注入类的时候发现报错,说是有两个类不知道注入哪一个,而如果其中一个有@primary注解的话,spring是会优先注入这个类的。
下面就没什么了,直到最后执行了
整个@EnableFeignClients的实现到此执行完毕,这个注解的源码相对来说看起来算是比较清晰明了了,而且对于我们如果有做一个新注解的需求的话,完全可以参照着做,非常具有模板意义。
㈤ springcloud feign返回Map解析处理
继上回feign多参数处理的坑刚解决完,又出现了新的问题
springcloud feign多参数调用解析处理方法
源码地址: https://gitee.com/ttx_urey/feign-multiple-param
feign返回为Map时,底层解析Map的key或value为对象时,自动把对象解析为Map,导致调用的时候类型不匹配,那没办法,只能继续解决
解决思路:指定返回对象为Map时,调用自己的解析器来处理
先新增一个自己的解析器:
只用管
和
这两个方法,因为使用ObjectMapper解析Map时,我们需要知道Map的Key和Value的类型,前面两个没有Type参数的同名方法管不了
接下来,直接创建Bean,Spring会自动注入解析器列表里面
还有一个问题,就是如果不在feign的生产者的pom文件中把jackson-dataformat-xml的jar包去掉的话,feign默认返回的格式是xml,就不能用ObjectMapper了(虽然可以用XmlMapper,但是我不喜欢XML),所以需要去掉jar包
以防万一,在commom模块中再把feign-jackson的jar包加上
在浏览器打开 http://127.0.0.1:8100/test2
后台日志中可以看到
㈥ Feign踩坑记录:JSON parse error
1.跟踪抛出异常的堆栈,发现在对返回结果的json解析中抛出异常
2.为什么会解析json失败呢,我们单独调用feign对应的接口是正常的,json也是正常可以解析的
3.难道feign的处理过返回的内容,又去跟了下fegin处理过程发现从response获取到流并没有任何异常,难道是出在了源头?但是源头又没有任何异常,此时思绪已经混乱,试着在google上查找有没有相关的问题,没想到在feign的github上找到类似问题 https://github.com/OpenFeign/feign/issues/934
4.问题已然发现,就是响应的内容经过gzip编码,feign默认的Client不支持gzip解码。那么在此跟踪一下feign的源码查看处理过程,从入口 SynchronousMethodHandler 开始,在122行开始获取响应内容
最终在 Logger 的102行找到响应流的读取,读取的流程如下:
5.最终问题出在feign使用默认的HttpURLConnection,并没有经过任何处理,导致读取的是gzip压缩后的内容。此时我们可以将其置换为Httpclient,其内部 ResponseContentEncoding 的 process 方法,取出了Content-Encoding并判断不为空,然后获取对应的处理方式。
上面所说feign默认的Client不支持gzip解码可能容易引起歧义,应该是fegin默认的Client对响应流不支持对gzip后的字节流进行解析,所以在序列化成对象时会存在解析问题。如果一定要接收可以使用 ResponseEntity<byte[]> 来接收,这样feign就不会对其反序列化了。至于 feign.compression.request.enabled=true , feign.compression.response.enabled=true 配置的内容在 , ,大致可以看出只是在请求头添加了Header而已
2020/3/13
spring已添加支持,SpringCloud版升级到Hoxton即可
https://github.com/spring-cloud/spring-cloud-openfeign/pull/230
2020/12/01
对于仍然存在问题的伙伴,可以直接使用OkHttp设置为feign的客户端(因为okhttp是默认支持gzip压缩),不需要关注spring cloud版本; 最简单的方案,也是最推荐的方案 。
㈦ FeignClient 测试
(1)Maven 依赖
(2)接收请求的 DemoController
(3)调用远程接口的 FeignClient
执行三次 main() 方法,服务端控制台输出:
源码解析:
㈧ Spring Cloud Feign 源码分析 - FeignClientFactoryBean
关于Feign的启动原理分析,参照另一篇 Spring Cloud Feign 源码分析 - feign启动原理
书接上文,上篇最后提到所有带@FeignClient注解的interface都被封装成FeignClientFactoryBean的BeanDefinition。从名字上可以得知这个类是一个FactoryBean。关于FactoryBean的介绍参考...
因此直接找getObject()。
getTarget方法首先获取FeignContext的对象,基于这个context对当前feign的配置信息存放到Builder中。
首先实例化bean:FeignContext
FeignContext的定义在FeignAutoConfiguration
第一次除了创建新的FeignContext对象之外,还设置了一组configurations,
这组configurations是FeignClientSpecification类型,通过autowired注入。
在扫描EnableFeignClients和各个FeignClient时,将configuration对应的class封装成了FeignClientSpecification的BeanDefinition,这里从容器中取出来创建对象注入到configurations
通过断点可以看到这里有15个FeignClientSpecification的对象
一个是default.开头的在启动类里配置的configuration,剩下的都是FeignClient的configuration。
FeignContext继承了NamedContextFactory,对应的范型就是FeignClientSpecification,看下NamedContextFactory构造方法
这里设置了默认的defaultConfigType,feign里用的是FeignClientsConfiguration,定义了一系列的默认值。
在获取到FeignContext之后,开始封装Feign.Builder。
首先通过context实例化FeignLoggerFactory的对象,因为context是NamedContextFactory的子类,会给每个contextId创建一个独立的上下文,每一个k-v会存储在FeignContext的全局context中,key就是contextId
这三个方法的实现完全体现了NamedContextFactory的作用:
给每个name创建一个单独的ApplicationContext子上下文对象,后续凡是这个name的ioc操作,都由独立的ApplicationContext来完成,name之间的context相互隔离。所有的子上下文保存在了Map<String, > contexts中。
在创建Context时,补充了configuration的设置:
首先(1的位置),从全局的configurations查找是否定义了只对当前name生效的configuration,也就是判断在当前name所属的FeignClient注解上是否定义了configuration。如果定义过,将这个configuration的Class封装成BeanDefinition注册到本name的子上下文中。
接着(2的位置),从全局的configurations查找是否定义了全局配置,也就是@EnableFeignClients的defaultConfiguration的值,这里固定前缀是default.。
如果也存在,就也将这个defaultConfiguration的Class封装成BeanDefinition注册到本name的子上下文中。
第一次调用完毕get方法后,给每个FeignClient创建的FeignContext就完成了configuration初始化的动作,后面的所有操作,如配置encoder、decoder都是给当前的子上下文内注册BeanDefinition。最后将所有配置封装成Builder返回。
在getTarget()构造完成builder属性之后,开始了整个请求调度过程。
先看第一段:
如果没有url属性,就用name来处理,把http:// + name + path 拼装成url,执行loadBalance()
首先实例化Client的bean对象,默认返回LoadBalancerFeignClient的实例。
从LoadBalancerFeignClient的构造方法可以看到,这里使用了delegate的设计模式来代理Client.Default,扩展execute的实现。
然后则继续实例化Targeter的bean。默认有两种实现类。
我这里返回HystrixTargeter。调用target方法。
这里重点看下feign.target(target)的实现。
可以看到,通过build()构造了一个ReflectiveFeign的对象,将一系列feign的参数封装成了SynchronousMethodHandler和ParseHandlersByName。封装的这两个对象都是为了给后面newInstance用的。
newInstance返回了扩展后的Targeter的代理类。
下面介绍下newInstance的详细过程。
apply方法就是给feignClient的每个方法都封装了一个SynchronousMethodHandler,
factory.create(...)就是为了根据当前方法的各个参数+new SynchronousMethodHandler.Factory定义的默认参数来构造SynchronousMethodHandler
key对应的是类名#方法名,如:MasterClientLocal#getPersons()。
for循环则是为了封装methodToHandler,k-v分别是reflect的Method和SynchronousMethodHandler。遍历完成后,构建一个InvocationHandler的实现类:FeignInvocationHandler
通过传入target和dispatch,其实本质就是在调用SynchronousMethodHandler的invoke方法。而invoke方法则是扩展了http的调用动作,包括请求重试,decode处理,decode404判断等。
最重要的执行则是client.execute(...);
client有两种实现类:
Default是带url的execute的实现,封装了最普通的http调用。
LoadBalanceFeignClient是eureka的实现,通过获取server列表来实现loadBalance。
也就是最开始getTarget() 方法的两段不同的实现过程的最本质区别。
至此,FeignClientFactoryBean的源码分析告一段落。
本人通过delegate方式在此基础上实现了traceId的跨feign传递。将在下一篇文章中做具体说明。
㈨ Feign源码解析二
本文会基于Feign源码,看看Feign到底是怎么实现远程调用
上文中,我们的 user-service 服务需要调用远程的 order-service 服务完成一定的业务逻辑,而基本实现是order-service提供一个spi的jar包给user-service依赖,并且在user-service的启动类上添加了一个注解
这个注解就是@EnableFeignClients,接下来我们就从这个注解入手,一步一步解开Feign的神秘面纱
该注解类上的注释大概的意思就是:
扫描那些被声明为 Feign Clients (只要有 org.springframework.cloud.openfeign.FeignClient 注解修饰的接口都是Feign Clients接口)的接口
下面我们继续追踪源码,看看到底什么地方用到了这个注解
利用IDEA的查找调用链快捷键,可以发现在.class类型的文件中只有一个文件用到了这个注解
OK,下面主要就是看这个类做了什么
通过UML图我们发现该类分别实现了 ImportBeanDefinitionRegistrar , ResourceLoaderAware 以及 EnvironmentAware 接口
这三个接口均是spring-framework框架的spring-context模块下的接口,都是和spring上下文相关,具体作用下文会分析
总结下来就是利用这两个重要属性,一个获取应用配置属性,一个可以加载classpath下的文件,那么FeignClientsRegistrar持有这两个东西之后要做什么呢?
上面将bean配置类包装成 FeignClientSpecification ,注入到容器。该对象非常重要,包含FeignClient需要的 重试策略 , 超时策略 , 日志 等配置,如果某个FeignClient服务没有设置独立的配置类,则读取默认的配置,可以将这里注册的bean理解为整个应用中所有feign的默认配置
由于 FeignClientsRegistrar 实现了 ImportBeanDefinitionRegistrar 接口,这里简单提下这个接口的作用
我们知道在spring框架中,我们如果想注册一个bean的话主要由两种方式:自动注册/手动注册
知道了 ImportBeanDefinitionRegistrar 接口的作用,下面就来看下 FeignClientsRegistrar 类是何时被加载实例化的
通过IDEA工具搜索引用链,发现该类是在注解@EnableFeignClients上被import进来的,文章开始的图片中有
这里提下@Import注解的作用
该注解仅有一个属性value,使用该注解表明导入一个或者多个@Configuration类,其作用和.xml文件中的<import>等效,其允许导入@Configuration类,ImportSelector接口/ImportBeanDefinitionRegistrar接口的实现,也同样可以导入一个普通的组件类
注意,如果是XML或非@Configuration的bean定义资源需要被导入的话,需要使用@ImportResource注解代替
这里我们导入的FeignClientsRegistrar类正是一个ImportBeanDefinitionRegistrar接口的实现
FeignClientsRegistrar重写了该接口的 registerBeanDefinitions 方法,该方法有两个参数注解元数据 metadata 和bean定义注册表 registry
该方法会由spring负责调用,继而注册所有标注为@FeignClient注解的bean定义
下面看registerBeanDefinitions方法中的第二个方法,在该方法中完成了所有@FeignClient注解接口的扫描工作,以及注册到spring中,注意这里注册bean的类型为 FeignClientFactoryBean ,下面细说
总结一下该方法,就是扫描@EnableFeignClients注解上指定的basePackage或clients值,获取所有@FeignClient注解标识的接口,然后将这些接口一一调用以下 两个重要方法 完成 注册configuration配置bean 和注册 FeignClient bean
断点位置相当重要
BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);
这里是利用了spring的代理工厂来生成代理类,即这里将所有的 feignClient的描述信息 BeanDefinition 设定为 FeignClientFactoryBean 类型,该类继承自FactoryBean,因此这是一个代理类,FactoryBean是一个工厂bean,用作创建代理bean,所以得出结论,feign将所有的 feignClient bean定义的类型包装成 FeignClientFactoryBean
最终其实就是存入了BeanFactory的beanDefinitionMap中
那么代理类什么时候会触发生成呢? 在spring 刷新容器时 ,会根据beanDefinition去实例化bean,如果beanDefinition的beanClass类型为代理bean,则会调用其 T getObject() throws Exception; 方法生成代理bean,而我们实际利用注入进来的FeignClient接口就是这些一个个代理类
这里有一个需要注意的点,也是开发中会遇到的一个 启动报错点
如果我们同时定义了两个不同名称的接口 (同一个包下/或依赖方指定全部扫描我们提供的 @FeignClient ),且这两个 @FeignClient 接口注解的 value/name/serviceId 值一样的话,依赖方拿到我们的提供的spi依赖,启动类上 @EnableFeignClients 注解扫描能同时扫描到这两个接口,就会 启动报错
原因就是Feign会为每个@FeignClient注解标识的接口都注册一个以serviceId/name/value为key,FeignClientSpecification类型的bean定义为value去spring注册bean定义,又默认不允许覆盖bean定义,所以报错
官方提示给出的解决方法要么改个@FeignClient注解的serviceId,name,value属性值,要么就开启spring允许bean定义覆写
至此我们知道利用在springboot的启动类上添加的@EnableFeignClients注解,该注解中import进来了一个手动注册bean的 FeignClientsRegistrar注册器 ,该注册器会由spring加载其 registerBeanDefinitions方法 ,由此来扫描所有@EnableFeignClients注解定义的basePackages包路径下的所有标注为@FeignClient注解的接口,并将其注册到spring的bean定义Map中,并实例化bean
下一篇博文中,我会分析为什么我们在调用(@Resource)这些由@FeignClient注解的bean的方法时会发起 远程调用
㈩ Feign-灵活的使用Hystrix熔断(自定义CommandKey)
Feign可以直接去集成Hystrix熔断。具体配置: Hystrix熔断&&Feign熔断
但是配置时,却不是很灵活,只是支持 default 和 类名#方法名() 的配置。这就不能对类或者一组方法进行统一的配置。
源码改造:
项目启动后,会遍历@Feign注解类中的每一个方法调用 create 生成 Hystrix的配置key ,也可以是线程池的key。
源码位置: feign.hystrix.HystrixInvocationHandler#toSetters 如下图所示。
yml的配置:
注:默认情况下,Feign-Hystrix会使用 服务名 作为CommandGroup,使用 类名#方法名 作为CommandKey。
而在yml配置: 全局配置是default的配置,而实例配置为commandKey配置。
更多的配置,可以参考 Hystrix(2)— 相关配置
[享学Feign] 十二、Feign通过feign-hystrix模块使其拥有熔断、降级能力