记录一次因为 FactoryBean 导致组件提前加载的问题
概述#
在前段时间,笔者的开源项目的用户反映项目在配置某个功能后,会在启动时候出现 "No servlet set" 的错误,这个问题具体可以参见 Crane4j isse#268。
问题的原因其实在标题已经剧透了,是因为 FactoryBean 被提前加载,进而间接造成 SpringMVC 组件被提前加载导致的。
虽然最后解决方案没什么好说的,不过整个排查的过程很好加深了笔者对 Bean 生命周期,以及 FactoryBean 使用方式的理解,总的来说还是蛮有意思的,故写此文章用于记录。
1.问题定位#
1.1.出现 No ServletContext set#
跳过前情提要,简而言之,当笔者在本地启动项目以后,出现 “No ServletContext set” 错误:
根据堆栈,我们可以直接确认是 WebMvcAutoConfiguration#EnableWebMvcConfiguration
中创建 HandleMapping
的时候,因为找不到 ServletContext
而导致的:
而 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 实例化后、初始化前调用,结合简单的代码,我们可以推测原因无外乎两种:
WebMvcAutoConfiguration
根本没被它处理,所以没有设置上ServletContext
;WebMvcAutoConfiguration
被后处理器时,后处理器并没有持有一个可用的ServletContext
,同时从ServletConfig
中也无法获得一个可用的ServletContext
;
不过静态的代码分析到这边基本就到头了,具体什么情况,还得要跑起来才知道。
2.问题复现#
2.1.梳理依赖链#
我们在 ServletContextAware#setServletContext
方法上打上断点,然后重新启动项目,看看到底是怎么一回事:
好吧,启动时根本没有进入 ServletContextAware#setServletContext
方法,说明是我们之前推迟的第一种情况,即 WebMvcAutoConfiguration 根本没有被 ServletContextAwareProcessor 进行处理。
那么,这种情况唯一的解释,就是该配置类 WebMvcAutoConfiguration#EnableWebMvcConfiguration
本身因为某种原因被过早的实例化,此时 ServletContextAwareProcessor
可能都还没有生效。
为了验证我们的猜想,我们可以直接在 WebMvcAutoConfiguration#EnableWebMvcConfiguration
的构造函数里面打上断点:
进入断点后,我们检查 doCreateBean 方法调用时,BeanFactory 中是否有 ServletContextAwareProcessor:
虽然没有 ServletContextAwareProcessor,不过有一个 WebApplicationContextServletContextAwareProcessor,它是 ServletContextAwareProcessor 的子类,我们可以注意到,里面 ServletContext 与 ServletConfig 确实都是空的。
2.2.为什么会提前加载?#
到现在问题基本明确了,就是 WebMvcAutoConfiguration#EnableWebMvcConfiguration
过早加载的问题,那么,为什么它会提前加载?
根据上面的堆栈信息, Idea 已经告诉了我们,截止调用 WebMvcAutoConfiguration#EnableWebMvcConfiguration
构造函数时,整个上下文中 doGetBean
调用了 8 次,这意味着在 WebMvcAutoConfiguration#EnableWebMvcConfiguration
之前,整条依赖链上还有 7 个创建中的 Bean,顺着这个堆栈信息我们向上溯源,整条依赖链大概是这样的:
- org.springframework.context.annotation.internalAsyncAnnotationProcessor
- org.springframework.scheduling.annotation.ProxyAsyncConfiguration
- operatorBeanDefinitionRegistrar.OperatorProxyFactoryBean#cn.example.ExampleOperator
- operatorProxyFactory
- operationAnnotationProxyMethodFactory
- springConverterManager
- mvcConversionService
- 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
的创建向下找,找到进行依赖注入的地方,接着就发现确实是这个问题导致的:
简单的来说,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 这些组件,要特别注意不要依赖到了正常的业务组件,否则它们可能就会因为被过早初始化而无法正常使用。如果一定要使用的话,最好使用懒加载的方式去获取依赖。
作者:Createsequence
出处:https://www.cnblogs.com/Createsequence/p/18165609
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?