转载

Spring Interceptor 自动注入FeignClient导致循环依赖2.0

1,bug现场还原

问题描述

我在写拦截器的时候,多个类都是通过构造器注入,并且也在拦截器中通过构造器显示声明了依赖FeignClient,在项目启动后,Spring依赖分析显示,这些类产生了循环依赖

报错信息

Spring Interceptor 自动注入FeignClient导致循环依赖2.0

异常分析

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 ,而 mvcResourceUrlProviderFeignClient 加载和拦截器的加载顺序有关,那么要debug找到throw异常的第一现场,看看和 mvcResourceUrlProvider 有没有关系。

2.2Debug

异常分析

异常第一现场如下

Spring Interceptor 自动注入FeignClient导致循环依赖2.0

分析这段代码的意思应该是: org.springframework.beans.factory.support.DefaultSingletonBeanRegistrygetSingleton() 函数在创建 mvcResourceUrlProvider 之前,先调用 beforeSingletonCreation() 函数来校验 mvcResourceUrlProviderthis.singletonsCurrentlyInCreation 中是否已经存在,如果存在则抛异常

继续关注 mvcResourceUrlProvider 是在哪里被初始化加载的

Spring Interceptor 自动注入FeignClient导致循环依赖2.0

通过调用栈追溯,找到 org.springframework.context.event.AbstractApplicationEventMulticasterretrieveApplicationListeners() 函数, 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

Spring Interceptor 自动注入FeignClient导致循环依赖2.0

被直接调用的地方只有一处,也是在 org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport

Spring Interceptor 自动注入FeignClient导致循环依赖2.0

这里应该是 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 加进去,这样第二次执行的时候就会触发异常

Spring Interceptor 自动注入FeignClient导致循环依赖2.0

现在不知道为什么 beforeSingletonCreation() 函数会执行两次,看这个函数和相关命名,是不应该被加载两次的。通过观察调用栈,发现跟refresh事件发布有关,看一下调用栈中的 refresh() 函数,

Spring Interceptor 自动注入FeignClient导致循环依赖2.0

位于 org.springframework.context.support.AbstractApplicationContext 中,这应该是context创建阶段的一个步骤。

refresh() 调用栈的后面紧接着就是 createContext() ,位于 org.springframework.cloud.context.named.NamedContextFactory 中,这个函数里面执行了 context.refresh() ,那么 context 为什么会创建,通过调用栈和 context 的属性,判断这应该是 FeignContext ,如下

Spring Interceptor 自动注入FeignClient导致循环依赖2.0

现在提出一个假说: 在解析自动配置的时候,Spring分析依赖,扫描到了跟Feign相关的依赖,认为有必要创建FeignContext,创建过程中执行了 context.refresh()

根据beanName相关信息,追溯堆栈到feign相关函数之前,找到跟Feign相关的依赖,如下

Spring Interceptor 自动注入FeignClient导致循环依赖2.0

通过函数名和相关变量就能看出来,这是从 FeignClientFactoryBean 这个工厂Bean中获取 ThirdFeignClient 实例,参考 spring-cloud-openfeign原理分析 ,确认FeignClientFactoryBean 创建feign客户端的工厂。

追溯调用栈,继续分析是什么自动配置会跟Feign依赖有关,找到如下

这里验证了依赖2,和上面假说的前半段,Spring装载自动配置类 TakeResourcesClient ,找到它依赖 ThirdFeignClient

这里继续关注一下 doGetObjectFromFactoryBean() ,看看FeignClient创建过程

Spring Interceptor 自动注入FeignClient导致循环依赖2.0

Feign.Builder builder = feign(context);复制代码

这段代码的执行会调用其他函数,创建FeignContext,位于 org.springframework.cloud.context.named.NamedContextFactory

如下,这里创建FeignContext时候执行了 context.refresh() ,和前面的 refresh() 函数执行match上了,并且 refresh() 之后, 会第一次执行 beforeSingletonCreation() ,把 mvcResourceUrlProvider add进 this.singletonsCurrentlyInCreation 中,无异常

Spring Interceptor 自动注入FeignClient导致循环依赖2.0

第二次执行this.singletonsCurrentlyInCreation()函数调用过程分析

有了第一次分析,debug第二次的时候,先关注是有什么依赖引发 FeignContext 创建,以及为什么 FeignContext 需要再次创建

相同的追溯调用栈方式,找到依赖

Spring Interceptor 自动注入FeignClient导致循环依赖2.0

Spring Interceptor 自动注入FeignClient导致循环依赖2.0

如上两图,可以得到 ThirdFeignClient --> thirdInterceptorConfig --> WebMvcAutoConfiguration$EnableWebMvcConfiguration 这样的依赖关系,同样的,会走到创建 FeignContext 的步骤

Spring Interceptor 自动注入FeignClient导致循环依赖2.0

第二次执行 beforeSingletonCreation() ,把 mvcResourceUrlProvider add进 this.singletonsCurrentlyInCreation ,触发异常,也就是异常的第一现场。

分析: WebMvcAutoConfiguration$EnableWebMvcConfiguration 应当是拦截器配置类,即 ThirdInterceptorConfig ,构造器显示声明了 ThirdFeignClient 依赖,导致第二次创建 FeignContext

那么为什么为什么FeignContext需要再次创建?

FeignContext 用于隔离配置的, 继承 org.springframework.cloud.context.named.NamedContextFactory , 就是上面的 createContextcreateContext 为每个命名空间独立创建 ApplicationContext ,设置parent为外部传入的Context,这样就可以共用外部的Context中的Bean。

关注创建 FeignContext 前对于命名空间的判断,每次执行 getContext() 的时候, 命令空间都是platform-3rd而已有的命名空间this.contexts数量都是0,这直接导致么FeignContext创建两次 ,每次都进去 createContext() 阶段,应该是第一次执行之后 FeignContext 并没有真正存在this.contexts中。

3,分析

下图时根据上面的分析,勾勒出的执行步骤触发异常的流程图

Spring Interceptor 自动注入FeignClient导致循环依赖2.0

在这里,这两个步骤相当于同时发生,并且 ThirdFeignClient 都是被其他自动装配类通过构造器显示声明应用,导致两次加载,我想, ThirdFeignClient 是Feign的客户端, 不要显示地通过构造器来注入,让Spring容器去管理它的生成, 其他地方要调用就可以了,不需要通过显示声明去初始化而导致创建 FeignContext

采取措施,在调用 ThirdFeignClient 的类中通过@Autowired注解来调用

回答问题1:

第二次执行 beforeSingletonCreation() 的时候,应该是 WebMvcAutoConfiguration$EnableWebMvcConfiguration 依赖 ThirdFeignClient

回答问题2:

ThirdInterceptorConfig 显示依赖了 ThirdFeignClient ,导致创建 FeignContextcontext.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 了。

Spring Interceptor 自动注入FeignClient导致循环依赖2.0

5,结论

Feign客户端Spring去分析依赖,不要通过构造器注入,在调用的时候通过@Autowired注解来调用。

参考文档

techblog.ppdai.com/2018/05/28/…

原文  https://juejin.im/post/5dd4f6156fb9a05a79378c3b
正文到此结束
Loading...