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

6,542 阅读2分钟

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

2.2Debug

异常分析

异常第一现场如下


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

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


通过调用栈追溯,找到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


被直接调用的地方只有一处,也是在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(),把 mvcResourceUrlProvideradd进this.singletonsCurrentlyInCreation中,无异常


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

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

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



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


第二次执行beforeSingletonCreation(),把 mvcResourceUrlProvideradd进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,分析

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


在这里,这两个步骤相当于同时发生,并且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了。


5,结论

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

参考文档

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