Feign原理记录
背景:使用feign将参数封装为对象后,报错405,困惑了很久,所以有必要了解一下feign原理
一、Feign、OpenFeign、Spring Cloud Feign发布历史
发布历史 | maven坐标 | |
Feign |
|
<dependency> <groupId>com.netflix.feign</groupId> <artifactId>feign-core</artifactId> </dependency> |
OpenFeign |
|
<dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-core</artifactId> </dependency> |
SpringCloud OpenFeign |
Spring Cloud Feign 是 Spring 对 Feign 的封装,方便集成到 Spring 项目中,添加了对 Spring MVC 注解的支持,并简化了与负载均衡、服务注册发现、断路器等组件的集成。 受 Feign 更名影响,Spring Cloud Feign 也有两个
Spring Cloud Feign 发版历史
|
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-feign</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> |
总结:
- Feign 最开始是 Netflix 公司开源是一个组件
- 2016年,Netflix 将 Feign 捐献给社区,并改名为 OpenFeign
- Spring Cloud Feign 是对 Feign 的封装,以方便在 Spring 环境中使用
spring-cloud-starter-feign
是 Spring Cloud Feign 的早期版本,现已不在维护spring-cloud-starter-openfeign
是目前正在使用中的 Spring Cloud Feign
二、SpringCloud OpenFeign核心流程
- 基于面向接口的动态代理方式生成实现类
- 根据 Contract 协议规则,解析接口类的注解信息,解析成内部表现
- 基于 RequestBean,动态生成 Request
- 使用 Encoder 将 Bean 转换成 Http 报文正文
- 拦截器负责对请求和返回进行装饰处理
- 日志记录器记录请求日志
- 基于重试器发送 HTTP 请求
三、源码分析
读源码的思维导图,更好的理解:
3.1 环境准备
- JDK:1.8
- SpringBoot版本:2.7.3
- SpringCloud版本:2021.0.4
-
<properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <spring-boot.version>2.7.3</spring-boot.version> <!-- <spring-cloud.version>Edgware.RELEASE</spring-cloud.version>--> <spring-cloud.version>2021.0.4</spring-cloud.version> <hutool-all.version>5.7.21</hutool-all.version> <prometheus.verion>1.1.3</prometheus.verion> <prometheus-bootclient.version>0.8.1</prometheus-bootclient.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> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
3.2 源码总体流程
1、使用JDK动态代理为接口创建代理对象
2、执行接口的方法时,调用代理对象的invoker方法
3、读取FeignClient的注解得到要调用的远程服务的接口
4、通过Ribbon负载均衡得到一个要调用的服务提供者
5、使用HttpURLConnection发起请求,得到响应
3.3 初始化阶段源码分析
从@EnableFeignClient@入口看起,@Import注入的类是核心
FeignClientsRegistrar该类实现了ImportBeanDefinitionRegistrar接口,在该接口的registerBeanDefinitions方法中,spring向外暴露了BeanDefinitionRegistry注册器。用户如果需要手动创建或修改BeanDefinition,可以通过把BeanDefinition注册到BeanDefinitionRegistry的方式,之后spring会帮我们实例化bean并放在容器中
两个方法中:
- registerDefaultConfiguration方法主要用于读取配置信息
- 着重看registerFeignClients方法的实现
接下来向Spring容器注册代理类信息:
核心点:收集FeignClient的静态信息,每个Client会把他的基本信息,类名、方法、服务名等绑定到FactoryBean上,这样就就具备了生成一个动态代理类的基本条件。
FactoryBean的特殊之处在于它可以向容器中注册两个Bean:
- 一个是它本身,
- 一个是FactoryBean.getObject()方法返回值所代表的Bean
3.4、创建代理对象源码分析
上一阶段会将代理类FeignClientFactoryBean的定义注册到Spring容器,Spring容器refresh实例化Bean对象过程会中调用FeignClientFactory的getObject方法
接下来看loadBalance方法:
看一下这里的FeignBlockingLoadBalancerClient注入的配置
接下来调用target方法,可以看到一个是默认;一个是带有熔断器的目标器(这里我使用的是默认的)
调用feign的target方法:
最终调用了ReflectiveFeign类中的newInstance方法。其中名为nameToHandler的Map中存储了FeignClient接口中定义的方法
newInstace方法:
再来看一下InvocationHandler对象的创建:
将JDK代理的对象注入后
3.5 发起请求源码分析:
通过JDK动态代理我们知道,在InvocationHandler中,invoke方法对进行方法拦截和逻辑增强。看一下invoke如何工作的:
接下来进入executeAndDecode方法:
进入execute方法:
接下来进入选择服务:
接下来就是execute方法:
四、问题总结
1、问题1:Get请求传对象
直接报错:
2023-07-30 18:52:49.249 ERROR 4881 --- [nio-8081-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is feign.FeignException$MethodNotAllowed: [405] during [GET] to [http://FEIGN-DEMO-SERVER-SERVICE/feign/server/add1] [TestFeignClient#getAddResult1(AddReq)]: [{"timestamp":"2023-07-30T10:52:49.199+00:00","status":405,"error":"Method Not Allowed","path":"/feign/server/add1"}]] with root cause feign.FeignException$MethodNotAllowed: [405] during [GET] to [http://FEIGN-DEMO-SERVER-SERVICE/feign/server/add1] [TestFeignClient#getAddResult1(AddReq)]: [{"timestamp":"2023-07-30T10:52:49.199+00:00","status":405,"error":"Method Not Allowed","path":"/feign/server/add1"}] at feign.FeignException.clientErrorStatus(FeignException.java:221) ~[feign-core-11.8.jar:na] at feign.FeignException.errorStatus(FeignException.java:194) ~[feign-core-11.8.jar:na] at feign.FeignException.errorStatus(FeignException.java:185) ~[feign-core-11.8.jar:na] at feign.codec.ErrorDecoder$Default.decode(ErrorDecoder.java:92) ~[feign-core-11.8.jar:na] at feign.AsyncResponseHandler.handleResponse(AsyncResponseHandler.java:96) ~[feign-core-11.8.jar:na] at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:138) ~[feign-core-11.8.jar:na] at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:89) ~[feign-core-11.8.jar:na] at feign.ReflectiveFeign$FeignInvocationHandler.invoke(ReflectiveFeign.java:100) ~[feign-core-11.8.jar:na] at com.sun.proxy.$Proxy76.getAddResult1(Unknown Source) ~[na:na] at feign.demo.test.controller.FeignClientController.testFeign2(FeignClientController.java:37) ~[classes/:na] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_361] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_361] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_361] at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_361] at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205) ~[spring-web-5.3.22.jar:5.3.22] at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:150) ~[spring-web-5.3.22.jar:5.3.22] at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117) ~[spring-webmvc-5.3.22.jar:5.3.22] at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895) ~[spring-webmvc-5.3.22.jar:5.3.22] at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808) ~[spring-webmvc-5.3.22.jar:5.3.22] at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.3.22.jar:5.3.22] at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1070) ~[spring-webmvc-5.3.22.jar:5.3.22] at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963) ~[spring-webmvc-5.3.22.jar:5.3.22] at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.3.22.jar:5.3.22] at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) ~[spring-webmvc-5.3.22.jar:5.3.22] at javax.servlet.http.HttpServlet.service(HttpServlet.java:655) ~[tomcat-embed-core-9.0.65.jar:4.0.FR] at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.3.22.jar:5.3.22] at javax.servlet.http.HttpServlet.service(HttpServlet.java:764) ~[tomcat-embed-core-9.0.65.jar:4.0.FR] at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-embed-websocket-9.0.65.jar:9.0.65] at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.3.22.jar:5.3.22] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.22.jar:5.3.22] at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.3.22.jar:5.3.22] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.22.jar:5.3.22] at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.3.22.jar:5.3.22] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.22.jar:5.3.22] at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) [tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541) [tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135) [tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) [tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) [tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:360) [tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:399) [tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) [tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:890) [tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1789) [tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) [tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) [tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-9.0.65.jar:9.0.65] at java.lang.Thread.run(Thread.java:750) [na:1.8.0_361]
问题分析:
问题入口是在启动实例化时,生成Bean对象时解析生成方法元信息时定义的,往下看
contract解析:
解析生成方法元信息,也是问题根本原因所在:
后续在进行发起请求时如下:
继续跟进:
发现jackson转json的过程:
至此就结束了。
如何解决呢?OpenFeign官方也提供了对应的解决方案:
@SpringQueryMap注解
原理,绕开body处理方式
呢@SpringQueryMap又是如何处理的呢?这里不得不说到:AnnotatedParameterProcessor feign方法参数注解处理器,总两个方法:1.获取当前参数注解类型;2.处理当前参数
可以看到有一堆实现,其中QueryMapParameterProcessor 是用来解析处理@SpringQueryMap的
这里就一切对应上了,我们也可以自定义注解+自定义Processor来实现更加复杂参数的处理
再来看一下带有@SpringQueryMap的处理流程
五、扩展组件
几乎Builder中的配置都可以实现扩展
默认配置:
参考:
- Feign、OpenFeign、Spring Cloud Feign 的区别 - 掘金 (juejin.cn)
- 微服务精通之Feign原理解析 - 掘金 (juejin.cn)
- Spring Cloud Feign实现自定义复杂对象传参 - 简书 (jianshu.com)
- (259条消息) spring-cloud-openFeign源码深度解析_Jagger-Wang的博客-CSDN博客