我在写拦截器的时候,多个类都是通过构造器注入,并且也在拦截器中通过构造器显示声明了依赖FeignClient,在项目启动后,Spring依赖分析显示,这些类产生了循环依赖
thirdDemo
是启动类
TakeResourcesClient
是@Component注解的类,里面通过 @Autowired调用 ThirdFeignClient
@Component public class TakeResourcesClient { @Autowired private ThirdFeignClient thirdFeignClient; @Autowired private ThirdProperties thirdProperties; …… }复制代码
这个能解释循环依赖的依赖1和依赖2,SpringBoot在启动的时候自动加载@Component,分析其依赖的 ThirdFeignClient
@FeignClient(path = PathConstant.CONTEXT_PATH + PathConstant.URL, name = PathConstant.NAME_APPLICATION) public interface ThirdFeignClient { }复制代码
这是 ThirdFeignClient
,是一个用@FeignClient注解的Feign客户端
接着往下,依赖3无法解释,这里产生了
问题1: ThirdFeignClient
为什么会依赖 WebMvcAutoConfiguration$EnableWebMvcConfiguration
?
继续往下,分析依赖4
ThirdInterceptorConfig
是拦截器配置类,继承了 WebMvcConfigurationSupport
,构造器注入了 ThirdFeignClient
的依赖
@Component public class ThirdInterceptorConfig extends WebMvcConfigurationSupport { private final List<AuthHandle> authHandles; private final ThirdProperties thirdProperties; private final ThirdFeignClient thirdFeignClient; @Autowired public ThirdInterceptorConfig(List<AuthHandle> authHandles, ThirdProperties thirdProperties, ThirdFeignClient thirdFeignClient) { this.authHandles = authHandles; this.thirdProperties = thirdProperties; this.thirdFeignClient = thirdFeignClient; } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new ThirdInterceptor(authHandles, thirdProperties, thirdFeignClient)) …… }复制代码
但是这里会有断层,依赖2是 TakeResourcesClient
--> ThirdFeignClient
(通过 @Autowired调用ThirdFeignClient)
依赖4通过 构造器注入 ThirdFeignClient
,应该也是 ThirdInterceptorConfig
--> ThirdFeignClien
最后看一下拦截器的配置,也是通过构造器注入 ThirdFeignClient
,其实 ThirdInterceptorConfig
要注入 ThirdFeignClient
,目的就是为了在生成 ThirdInterceptor
对象的时候,注入 ThirdFeignClient
拦截器
public class ThirdInterceptor extends HandlerInterceptorAdapter { private final List<AuthHandle> authHandles; private final ThirdProperties thirdProperties; private ThirdFeignClient thirdFeignClient; public ThirdInterceptor(List<AuthHandle> authHandles, ThirdProperties thirdProperties, ThirdFeignClient thirdFeignClient) { this.authHandles = authHandles; this.thirdProperties = thirdProperties; this.thirdFeignClient = thirdFeignClient; } ……复制代码
继续往下,依赖5和依赖6也无法解释,那么产生了如下几个问题
问题2: mvcResourceUrlProvider
是什么?为什么 ThirdInterceptorConfig
依赖 mvcResourceUrlProvider
?
问题3:为什么 mvcResourceUrlProvider
又依赖 ThirdFeignClient
?
依赖分析的结果可能并不是真正的依赖关系,而是在执行依赖分析的时候出发了某种异常,这个异常的核心是 mvcResourceUrlProvider
,而 mvcResourceUrlProvider
和 FeignClient
加载和拦截器的加载顺序有关,那么要debug找到throw异常的第一现场,看看和 mvcResourceUrlProvider
有没有关系。
异常第一现场如下
分析这段代码的意思应该是: org.springframework.beans.factory.support.DefaultSingletonBeanRegistry
的 getSingleton()
函数在创建 mvcResourceUrlProvider
之前,先调用 beforeSingletonCreation()
函数来校验 mvcResourceUrlProvider
在 this.singletonsCurrentlyInCreation
中是否已经存在,如果存在则抛异常
继续关注 mvcResourceUrlProvider
是在哪里被初始化加载的
通过调用栈追溯,找到 org.springframework.context.event.AbstractApplicationEventMulticaster
的 retrieveApplicationListeners()
函数, mvcResourceUrlProvider
在这里第一次出现,是 listenerBeans
中的一个元素,而 listenerBeans
是
listenerBeans = new LinkedHashSet<>(this.defaultRetriever.applicationListenerBeans);复制代码
初始化赋值出来的, listenerBeans
的全部对象有22个,看起来像是SpringBoot默认初始化的实例。
搜了一下这个类,确实是缺省配置,是Springboot Web应用启动过程中定义的Bean。参考 blog.csdn.net/andy_zhang2…
继续追问:为什么 this.singletonsCurrentlyInCreation
中已经存在了 mvcResourceUrlProvider
,肯定是有其他地方加载的,先全局搜一下 mvcResourceUrlProvider
,在 org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport
中
被直接调用的地方只有一处,也是在 org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport
中
这里应该是 WebMvcConfigurationSuppor
在添加完拦截器之后,通过@Bean注解去调用 mvcResourceUrlProvider
注册成为默认拦截器,而 mvcResourceUrlProvider
已经作为缺省配置被预先加载好了。
( mvcResourceUrlProvider
提供 ResourceUrlProvider
实例, ResourceUrlProvider
是获取外部URL路径的转换的核心组件,其内部定了 Map<String, ResourceHttpRequestHandler> handlerMap
用来进行链式的解析。)
至此,要先解决的问题是
为什么 this.singletonsCurrentlyInCreation
中已经存在了 mvcResourceUrlProvider
?
在 beforeSingletonCreation()
打断点发现,此函数会被执行两次,第一次执行时, this.singletonsCurrentlyInCreation
中没有 mvcResourceUrlProvider
,不会触发异常,第二次才会触发异常
第一次执行时, this.singletonsCurrentlyInCreation
中没有 mvcResourceUrlProvider
,然后把 mvcResourceUrlProvider
加进去,这样第二次执行的时候就会触发异常
现在不知道为什么 beforeSingletonCreation()
函数会执行两次,看这个函数和相关命名,是不应该被加载两次的。通过观察调用栈,发现跟refresh事件发布有关,看一下调用栈中的 refresh()
函数,
位于 org.springframework.context.support.AbstractApplicationContext
中,这应该是context创建阶段的一个步骤。
refresh()
调用栈的后面紧接着就是 createContext()
,位于 org.springframework.cloud.context.named.NamedContextFactory
中,这个函数里面执行了 context.refresh()
,那么 context 为什么会创建,通过调用栈和 context 的属性,判断这应该是 FeignContext
,如下
现在提出一个假说: 在解析自动配置的时候,Spring分析依赖,扫描到了跟Feign相关的依赖,认为有必要创建FeignContext,创建过程中执行了 context.refresh()
根据beanName相关信息,追溯堆栈到feign相关函数之前,找到跟Feign相关的依赖,如下
通过函数名和相关变量就能看出来,这是从 FeignClientFactoryBean
这个工厂Bean中获取 ThirdFeignClient
实例,参考 spring-cloud-openfeign原理分析 ,确认FeignClientFactoryBean 创建feign客户端的工厂。
追溯调用栈,继续分析是什么自动配置会跟Feign依赖有关,找到如下
这里验证了依赖2,和上面假说的前半段,Spring装载自动配置类 TakeResourcesClient
,找到它依赖 ThirdFeignClient
。
这里继续关注一下 doGetObjectFromFactoryBean()
,看看FeignClient创建过程
Feign.Builder builder = feign(context);复制代码
这段代码的执行会调用其他函数,创建FeignContext,位于 org.springframework.cloud.context.named.NamedContextFactory
如下,这里创建FeignContext时候执行了 context.refresh()
,和前面的 refresh()
函数执行match上了,并且 refresh()
之后, 会第一次执行 beforeSingletonCreation()
,把 mvcResourceUrlProvider
add进 this.singletonsCurrentlyInCreation
中,无异常
有了第一次分析,debug第二次的时候,先关注是有什么依赖引发 FeignContext 创建,以及为什么 FeignContext 需要再次创建
相同的追溯调用栈方式,找到依赖
如上两图,可以得到 ThirdFeignClient
--> thirdInterceptorConfig
--> WebMvcAutoConfiguration$EnableWebMvcConfiguration
这样的依赖关系,同样的,会走到创建 FeignContext 的步骤
第二次执行 beforeSingletonCreation()
,把 mvcResourceUrlProvider
add进 this.singletonsCurrentlyInCreation
中 ,触发异常,也就是异常的第一现场。
分析: WebMvcAutoConfiguration$EnableWebMvcConfiguration
应当是拦截器配置类,即 ThirdInterceptorConfig
,构造器显示声明了 ThirdFeignClient
依赖,导致第二次创建 FeignContext
那么为什么为什么FeignContext需要再次创建?
FeignContext 用于隔离配置的, 继承 org.springframework.cloud.context.named.NamedContextFactory
, 就是上面的 createContext
, createContext
为每个命名空间独立创建 ApplicationContext
,设置parent为外部传入的Context,这样就可以共用外部的Context中的Bean。
关注创建 FeignContext 前对于命名空间的判断,每次执行 getContext()
的时候, 命令空间都是platform-3rd而已有的命名空间this.contexts数量都是0,这直接导致么FeignContext创建两次 ,每次都进去 createContext()
阶段,应该是第一次执行之后 FeignContext 并没有真正存在this.contexts中。
下图时根据上面的分析,勾勒出的执行步骤触发异常的流程图
在这里,这两个步骤相当于同时发生,并且 ThirdFeignClient
都是被其他自动装配类通过构造器显示声明应用,导致两次加载,我想, ThirdFeignClient
是Feign的客户端, 不要显示地通过构造器来注入,让Spring容器去管理它的生成, 其他地方要调用就可以了,不需要通过显示声明去初始化而导致创建 FeignContext 。
采取措施,在调用 ThirdFeignClient
的类中通过@Autowired注解来调用
回答问题1:
第二次执行 beforeSingletonCreation()
的时候,应该是 WebMvcAutoConfiguration$EnableWebMvcConfiguration
依赖 ThirdFeignClient
回答问题2:
ThirdInterceptorConfig
显示依赖了 ThirdFeignClient
,导致创建 FeignContext , context.refresh()
又加载了 mvcResourceUrlProvider
回答问题3:
mvcResourceUrlProvider
不依赖 ThirdFeignClient
,是两次加载 FeignContext 触发的异常
改动后代码如下
public class ThirdInterceptor extends HandlerInterceptorAdapter { private static final Logger logger = LoggerFactory.getLogger(ThirdInterceptor.class); private final List<AuthHandle> authHandles; private final ThirdProperties thirdProperties; @Autowired private ThirdFeignClient thirdFeignClient; public ThirdInterceptor(List<AuthHandle> authHandles, ThirdProperties thirdProperties) { this.authHandles = authHandles; this.thirdProperties = thirdProperties; } } 复制代码
@Component public class TakeResourcesClient { @Autowired private ThirdFeignClient thirdFeignClient; @Autowired private ThirdProperties thirdProperties; }复制代码
@Configuration public class ThirdInterceptorConfig extends WebMvcConfigurationSupport { private final List<AuthHandle> authHandles; private final ThirdProperties thirdProperties; @Autowired public ThirdInterceptorConfig(List<AuthHandle> authHandles, ThirdProperties thirdProperties) { this.authHandles = authHandles; this.thirdProperties = thirdProperties; } @Bean public ThirdInterceptor getThirdInterceptor() { return new ThirdInterceptor(authHandles, thirdProperties); } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(getThirdInterceptor()) …… }复制代码
改过之后,项目正常启动,是可行的。
并且观察加载顺序,在第一次加载 takeResourcesClient
实例的时候,已经加载了 thirdFeignClient
实例,在加载 thirdInterceptorConfig
,执行
ConstructorResolver.setCurrentInjectionPoint(descriptor)复制代码
拿到 previousInjectionPoint 先前注入点,里面 thirdFeignClient
,不会再创建 FeignContext 了。
Feign客户端Spring去分析依赖,不要通过构造器注入,在调用的时候通过@Autowired注解来调用。
参考文档
techblog.ppdai.com/2018/05/28/…