1,bug现场还原
问题描述
我在写拦截器的时候,多个类都是通过构造器注入,并且也在拦截器中通过构造器显示声明了依赖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
?
2,bug分析
2.1假说
依赖分析的结果可能并不是真正的依赖关系,而是在执行依赖分析的时候出发了某种异常,这个异常的核心是mvcResourceUrlProvider
,而mvcResourceUrlProvider
和FeignClient
加载和拦截器的加载顺序有关,那么要debug找到throw异常的第一现场,看看和mvcResourceUrlProvider
有没有关系。
2.2Debug
异常分析
异常第一现场如下
分析这段代码的意思应该是: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()函数调用过程分析
第一次执行时,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
中,无异常
第二次执行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中。
3,分析
下图时根据上面的分析,勾勒出的执行步骤触发异常的流程图
在这里,这两个步骤相当于同时发生,并且ThirdFeignClient
都是被其他自动装配类通过构造器显示声明应用,导致两次加载,我想,ThirdFeignClient
是Feign的客户端,不要显示地通过构造器来注入,让Spring容器去管理它的生成,其他地方要调用就可以了,不需要通过显示声明去初始化而导致创建FeignContext。
采取措施,在调用ThirdFeignClient
的类中通过@Autowired注解来调用
回答问题1:
第二次执行beforeSingletonCreation()
的时候,应该是WebMvcAutoConfiguration$EnableWebMvcConfiguration
依赖 ThirdFeignClient
回答问题2:
ThirdInterceptorConfig
显示依赖了ThirdFeignClient
,导致创建FeignContext,context.refresh()
又加载了 mvcResourceUrlProvider
回答问题3:
mvcResourceUrlProvider
不依赖ThirdFeignClient
,是两次加载 FeignContext触发的异常
4,实现
改动后代码如下
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了。
5,结论
Feign客户端Spring去分析依赖,不要通过构造器注入,在调用的时候通过@Autowired注解来调用。
参考文档
今天的文章Spring Interceptor 自动注入FeignClient导致循环依赖2.0分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/16722.html