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

1,bug现场还原

问题描述

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

报错信息


异常分析

thirdDemo是启动类web

TakeResourcesClient是@Component注解的类,里面经过 @Autowired调用ThirdFeignClientspring

@Component
public class TakeResourcesClient {
    @Autowired
    private ThirdFeignClient thirdFeignClient;

    @Autowired
    private ThirdProperties thirdProperties;
    
    ……
}复制代码

这个能解释循环依赖的依赖1和依赖2,SpringBoot在启动的时候自动加载@Component,分析其依赖的ThirdFeignClientbash

@FeignClient(path = PathConstant.CONTEXT_PATH + PathConstant.URL, name = PathConstant.NAME_APPLICATION)
public interface ThirdFeignClient {
 
}复制代码

这是ThirdFeignClient,是一个用@FeignClient注解的Feign客户端mvc

接着往下,依赖3没法解释,这里产生了app

问题1:ThirdFeignClient为何会依赖WebMvcAutoConfiguration$EnableWebMvcConfigurationide

继续往下,分析依赖4函数

ThirdInterceptorConfig是拦截器配置类,继承了WebMvcConfigurationSupport,构造器注入了ThirdFeignClient的依赖ui

@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)this

依赖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依赖有关,找到以下 img

这里验证了依赖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/…

相关文章
相关标签/搜索