Feign原理记录

背景:使用feign将参数封装为对象后,报错405,困惑了很久,所以有必要了解一下feign原理

一、Feign、OpenFeign、Spring Cloud Feign发布历史

  发布历史  maven坐标
Feign
  • 2013年6月 Netflix Feign第一个版本:1.0.0发布
  • 2016年7月 Netflix Feign发布最后一个版本:8.18.0
<dependency>
    <groupId>com.netflix.feign</groupId>
    <artifactId>feign-core</artifactId>
</dependency>
OpenFeign
  • 2016年,Netflix将feign捐献给社区,并改名OpenFeign

  • 2016年7月,OpenFeign首个版本发布:9.0.0,之后一直持续发布到现在

     
<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 也有两个 starter

  • spring-cloud-starter-feign
  • spring-cloud-starter-openfeign

Spring Cloud Feign 发版历史

  • 2015年3月 spring-cloud-starter-feign 发布了 1.0.0.RELEASE 版本
  • 2016年9月,spring-cloud-starter-feign 将依赖从 Netflix Feign 改为 OpenFeign,并发布了 1.2.0.RELEASE
  • 2017年11月,Spring Cloud 团队将两个 feign starter 进行"合并",发布了 spring-cloud-starter-openfeign首个版本 1.4.0.RELEASE,和 spring-cloud-starter-feign1.4.0.RELEASE 版本
  • 2019年5月23日 spring-cloud-starter-feign 发布了最后一个换皮版本 1.4.7.RELEASE,到此,过渡期也就结束了
<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核心流程

 

  1. 基于面向接口的动态代理方式生成实现类
  2. 根据 Contract 协议规则,解析接口类的注解信息,解析成内部表现
  3. 基于 RequestBean,动态生成 Request
  4. 使用 Encoder 将 Bean 转换成 Http 报文正文
  5. 拦截器负责对请求和返回进行装饰处理
  6. 日志记录器记录请求日志
  7. 基于重试器发送 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>
    pom依赖

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中的配置都可以实现扩展

 默认配置:

 

 

 

 

 

 

 

 

 

 

参考:

 

posted @ 2023-07-28 22:56  coder、  阅读(48)  评论(0编辑  收藏  举报