Loading

记录一次因为 FactoryBean 导致组件提前加载的问题

概述

在前段时间,笔者的开源项目的用户反映项目在配置某个功能后,会在启动时候出现 "No servlet set" 的错误,这个问题具体可以参见 Crane4j isse#268
问题的原因其实在标题已经剧透了,是因为 FactoryBean 被提前加载,进而间接造成 SpringMVC 组件被提前加载导致的。
虽然最后解决方案没什么好说的,不过整个排查的过程很好加深了笔者对 Bean 生命周期,以及 FactoryBean 使用方式的理解,总的来说还是蛮有意思的,故写此文章用于记录。

1.问题定位

1.1.出现 No ServletContext set

跳过前情提要,简而言之,当笔者在本地启动项目以后,出现 “No ServletContext set” 错误:
image.png
根据堆栈,我们可以直接确认是 WebMvcAutoConfiguration#EnableWebMvcConfiguration
中创建 HandleMapping 的时候,因为找不到 ServletContext 而导致的:
image.png
ServletContext 又来自于 WebMvcAutoConfiguration 实现的 ServletContextAware 回调接口:

public WebMvcAutoConfiguration implements ServletContextAware {

    @Nullable
    private ServletContext servletContext;
    
    public void setServletContext(@Nullable ServletContext servletContext) {
        this.servletContext = servletContext;
    }

    @Nullable
    public final ServletContext getServletContext() {
        return this.servletContext;
    }
}

1.2.ApplicationContextAware 为什么没生效?

显然,这个 setServletContext 方法要么没调用,要么调用了但是 set 了一个空值。那么,setServletContext 又是谁调用的?
看过 Spring 处理各种 Aware 接口源码的同学可能立刻会敏感的意识到,这个接口的处理,要么是基于 AbstractApplicationContext 的回调接口完成,要么和 ApplicationContextAware 等接口一样,是基于某个特定的后处理器完成。
如果下载了 Spring 源码,你可以直接在 idea 中寻找 ServletContextAware#setServletContext 方法在源码中的引用,或者你可以直接双击 shift 在源码中寻找与其同名或部分同名的组件,如此我们便找到了 ServletContextAwareProcessor 这个后处理器 —— 显然ApplicationContextAware 等接口一样, ServletContextAware 接口是通过 ServletContextAwareProcessor 这个特定的后处理器调用的

public class ServletContextAwareProcessor implements BeanPostProcessor {

	@Nullable
	private ServletContext servletContext;

	@Nullable
	private ServletConfig servletConfig;
    
	@Nullable
	protected ServletContext getServletContext() {
		if (this.servletContext == null && getServletConfig() != null) {
			return getServletConfig().getServletContext();
		}
		return this.servletContext;
	}
    
	@Nullable
	protected ServletConfig getServletConfig() {
		return this.servletConfig;
	}

	@Override
	public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
		if (getServletContext() != null && bean instanceof ServletContextAware) {
			((ServletContextAware) bean).setServletContext(getServletContext());
		}
		if (getServletConfig() != null && bean instanceof ServletConfigAware) {
			((ServletConfigAware) bean).setServletConfig(getServletConfig());
		}
		return bean;
	}
}

非常明显,ServletContextAwareProcessor 是一个 BeanPostProcessor,这个后处理器在 Bean 实例化后、初始化前调用,结合简单的代码,我们可以推测原因无外乎两种:

  1. WebMvcAutoConfiguration 根本没被它处理,所以没有设置上 ServletContext
  2. WebMvcAutoConfiguration 被后处理器时,后处理器并没有持有一个可用的 ServletContext,同时从 ServletConfig 中也无法获得一个可用的 ServletContext

不过静态的代码分析到这边基本就到头了,具体什么情况,还得要跑起来才知道。

2.问题复现

2.1.梳理依赖链

我们在 ServletContextAware#setServletContext 方法上打上断点,然后重新启动项目,看看到底是怎么一回事:
image.png
好吧,启动时根本没有进入 ServletContextAware#setServletContext 方法,说明是我们之前推迟的第一种情况,即 WebMvcAutoConfiguration 根本没有被 ServletContextAwareProcessor 进行处理
那么,这种情况唯一的解释,就是该配置类 WebMvcAutoConfiguration#EnableWebMvcConfiguration本身因为某种原因被过早的实例化,此时 ServletContextAwareProcessor 可能都还没有生效。
为了验证我们的猜想,我们可以直接在 WebMvcAutoConfiguration#EnableWebMvcConfiguration 的构造函数里面打上断点:
image.png
进入断点后,我们检查 doCreateBean 方法调用时,BeanFactory 中是否有 ServletContextAwareProcessor:
image.png
虽然没有 ServletContextAwareProcessor,不过有一个 WebApplicationContextServletContextAwareProcessor,它是 ServletContextAwareProcessor 的子类,我们可以注意到,里面 ServletContext 与 ServletConfig 确实都是空的。

2.2.为什么会提前加载?

到现在问题基本明确了,就是 WebMvcAutoConfiguration#EnableWebMvcConfiguration 过早加载的问题,那么,为什么它会提前加载?
根据上面的堆栈信息, Idea 已经告诉了我们,截止调用 WebMvcAutoConfiguration#EnableWebMvcConfiguration 构造函数时,整个上下文中 doGetBean 调用了 8 次,这意味着在 WebMvcAutoConfiguration#EnableWebMvcConfiguration 之前,整条依赖链上还有 7 个创建中的 Bean,顺着这个堆栈信息我们向上溯源,整条依赖链大概是这样的:

  1. org.springframework.context.annotation.internalAsyncAnnotationProcessor
  2. org.springframework.scheduling.annotation.ProxyAsyncConfiguration
  3. operatorBeanDefinitionRegistrar.OperatorProxyFactoryBean#cn.example.ExampleOperator
  4. operatorProxyFactory
  5. operationAnnotationProxyMethodFactory
  6. springConverterManager
  7. mvcConversionService
  8. org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration

分析这个依赖链,笔者可以意识到了问题所在,因为 3、4、5 都是 Crane4j 提供的组件,按理说这种用户自己的组件不应该这么早的进行初始化,因此说明创建 org.springframework.scheduling.annotation.ProxyAsyncConfiguration 这一步肯定有问题。
笔者在这一步纠结了很久,因为 ProxyAsyncConfiguration 看起来就是一个普通的配置类,直到在里面看到了这一段代码:

@Autowired(required = false)
void setConfigurers(Collection<AsyncConfigurer> configurers) {
    if (CollectionUtils.isEmpty(configurers)) {
        return;
    }
    if (configurers.size() > 1) {
        throw new IllegalStateException("Only one AsyncConfigurer may exist");
    }
    AsyncConfigurer configurer = configurers.iterator().next();
    this.executor = configurer::getAsyncExecutor;
    this.exceptionHandler = configurer::getAsyncUncaughtExceptionHandler;
}

Spring 在此处进行了一次 setter 方法注入。熟悉 Spring 的同学应该都知道,这种批量注入最终会调用 ListableBeanFactory 的 getXXXForType 或者 getXXXOfType 去批量从容器中获取 Bean。有意思的是,如果容器中存在 FactoryBean,由于 Spring 只能通过 FactoryBean#getObjectType 方法去推断类型,因此会提前创建 FactoryBean 以便获取其类型。
根据这个思路,我们沿着堆栈从 ProxyAsyncConfiguration 的创建向下找,找到进行依赖注入的地方,接着就发现确实是这个问题导致的:
image.png
简单的来说,ProxyAsyncConfiguration 进行依赖注入时,调用了 getBeanNamesForType 方法,而这个方法会去检查容器中所有的 Bean 的类型。此时由于我们的 operatorBeanDefinitionRegistrar.OperatorProxyFactoryBean#cn.example.ExampleOperator 是一个 FactoryBean,因此 Spring 直接 FactoryBean 创建了出来以获取其类型,而 FactoryBean 的创建又触发了其他组件的创建,最终导致了整条依赖链上所有组件的提前加载!

3.解决方案

现在我们已经明确的知道了问题所在,那么该怎么解决?
首先,getTypeForFactoryBean 这个地方提供了一个参数 allowInit 用于决定是否要初始化 FactoryBean,然而顺着堆栈一路找上去,会发现这个参数最初已经在 getBeanNamesForType 就定死了是 true,这意味着我们不可能通过轻易的改变 Spring 的初始化流程来避免这个问题。
既然如此,那就只能从这个 FactoryBean 下手了,以下是其代码:

@Setter
public static class OperatorProxyFactoryBean<T> implements FactoryBean<T> {

    private OperatorProxyFactory operatorProxyFactory;
    private Class<T> operatorType;

    @Override
    public T getObject() {
        return operatorProxyFactory.get(operatorType);
    }

    @Override
    public Class<?> getObjectType() {
        return operatorType;
    }
}

后续的问题其实就是由于它依赖的 OperatorProxyFactory 被初始化导致的,因此我们需要想办法在把它的加载延迟到调用 getObject 的时候。
要延迟一个属性的注入,第一种办法是直接在属性或者 setter 方法上添加 @Lazy 注解,此后当进行依赖注入时,Spring 将会生成一个代理对象,等到使用代理对象时才会真正的从 Spring 容器获取对应的 Bean。不过由于这个 Bean 是在代码中通过手动构造 BeanDefinition 的方式创建的,依赖注入的参数在一开始就已经指定,因此无法通过加注解的方式实现。
因此我们只能采用第二种,即使用 ObjectProvider 对其进行包裹,改成这样:

public static class OperatorProxyFactoryBean<T> implements FactoryBean<T> {

    private ObjectProvider<OperatorProxyFactory> operatorProxyFactory;
    private Class<T> operatorType;

    @Override
    public T getObject() {
        return operatorProxyFactory.getObject().get(operatorType);
    }

    @Override
    public Class<?> getObjectType() {
        return operatorType;
    }
}

重新编译打包顺利启动,至此,这个问题彻底解决了。

总结

总的来说,这种因为 Spring 内置组件的初始化时机被打乱,导致出现各种奇奇怪怪的问题倒是蛮常见的。
当已经出现此类问题的时候,可以考虑直接在构造函数或者某些回调接口上打断点,通过堆栈倒推 Bean 的依赖关系来排查问题。
不过,最理想的肯定还是从一开始就避免出现这类问题。因此,在基于 Spring 生命周期开发一些组件时,我们需要特别注意它们的初始化时机。比如 BeanPostProcessor、Advice/Advisor 或者 FactoryBean 这些组件,要特别注意不要依赖到了正常的业务组件,否则它们可能就会因为被过早初始化而无法正常使用。如果一定要使用的话,最好使用懒加载的方式去获取依赖。

posted @ 2024-04-29 14:10  Createsequence  阅读(14)  评论(0编辑  收藏  举报