我在写拦截器的时候,多个类都是通过构造器注入,并且也在拦截器中通过构造器显示声明了依赖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/…