面试复盘:单例 Bean 和 Request Bean 注解注入,程序启动会报错吗?
最近在复习 Spring Boot 时,突发奇想一个问题:如果有两个 Bean,一个是 singleton
作用域,另一个是 request
作用域,通过注解(比如 @Autowired
)注入,程序启动时会报错吗?这个问题虽然没在面试中遇到,但感觉是个不错的考察点,既涉及 Bean 的作用域,又考验对 Spring 容器和依赖注入的理解。下面我就模拟一次"自我面试",复盘分析这个场景。
问题背景
在 Spring Boot 中,Bean 的作用域决定了它的生命周期和实例化方式:
singleton
:全局唯一,容器启动时创建,适用于无状态对象。request
:每个 HTTP 请求一个实例,仅在 Web 环境下有效,请求结束后销毁。
假设有如下代码:
java
@Component
@Scope("singleton")
public class SingletonBean {
@Autowired
private RequestBean requestBean;
public void print() {
System.out.println("RequestBean: " + requestBean);
}
}
@Component
@Scope("request")
public class RequestBean {
public String getMessage() {
return "I am a request-scoped bean";
}
}
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
问题来了:SingletonBean
是单例,生命周期跟容器绑定,而 RequestBean
是请求级别,只有在 HTTP 请求时才创建。程序启动时,Spring 容器会初始化所有单例 Bean,那这种依赖注入会报错吗?
分析过程
-
Spring 容器启动流程
- 程序启动时,Spring 容器会加载所有
singleton
作用域的 Bean。 SingletonBean
是单例,Spring 会尝试实例化它,并通过@Autowired
注入依赖RequestBean
。- 但
RequestBean
是request
作用域,启动时没有 HTTP 请求上下文,Spring 无法直接创建它的实例。
- 程序启动时,Spring 容器会加载所有
-
默认行为
-
Spring 的依赖注入是"急切"(eager)的,容器启动时会解析所有单例 Bean 的依赖。
-
由于
RequestBean
在非 Web 请求环境下不可用,Spring 会抛出异常。 -
可能的错误信息类似:
dartorg.springframework.beans.factory.BeanCreationException: Error creating bean with name 'singletonBean': Injection of autowired dependencies failed; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'RequestBean' available
-
-
为什么会报错
singleton
和request
的生命周期不匹配。单例 Bean 在容器初始化时就固定,而request
Bean 需要动态创建。- Spring 容器启动时没有 Web 请求上下文,无法提供
RequestBean
的实例。
验证实验
我在本地写了个 demo 测试了一下,果然启动报错了。日志显示 Spring 找不到 RequestBean
的定义,因为它期待一个现成的 Bean,而 request
作用域的 Bean 在启动时根本不存在。这让我想到,Spring 肯定有办法处理这种场景,不然 Web 开发中这种需求太常见了。
解决方案
复盘时查了资料,发现 Spring 提供了几种方法来解决这种依赖注入问题:
-
使用代理(
@Autowired
+ 代理模式)-
Spring 可以为
request
作用域的 Bean 创建一个代理对象(CGLIB 动态代理)。 -
修改代码,在
RequestBean
上加@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
:java@Component @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) public class RequestBean { public String getMessage() { return "I am a request-scoped bean"; } }
-
原理 :代理对象在容器启动时注入到
SingletonBean
中,实际调用时才动态解析到真实的RequestBean
实例。 -
结果 :程序启动不会报错,但在非 Web 环境下调用
requestBean
会抛出IllegalStateException
,提示缺少请求上下文。
-
-
使用
Provider
或ObjectFactory
-
通过
javax.inject.Provider
或 Spring 的ObjectFactory
延迟获取RequestBean
:java@Component @Scope("singleton") public class SingletonBean { @Autowired private Provider<RequestBean> requestBeanProvider; public void print() { RequestBean requestBean = requestBeanProvider.get(); System.out.println("RequestBean: " + requestBean); } }
-
原理 :
Provider
是一个工厂接口,每次调用get()
时才会创建RequestBean
,避免启动时依赖解析。 -
结果:启动正常,只有在实际调用时才会检查请求上下文。
-
-
避免直接注入
-
如果业务允许,可以通过
ApplicationContext
手动获取RequestBean
:java@Component @Scope("singleton") public class SingletonBean { @Autowired private ApplicationContext context; public void print() { RequestBean requestBean = context.getBean(RequestBean.class); System.out.println("RequestBean: " + requestBean); } }
-
原理:运行时动态获取 Bean,绕过了启动时的依赖注入。
-
结果:启动没问题,但需要在请求时调用。
-
复盘感想
这个问题让我意识到,Bean 作用域的差异会直接影响依赖注入的行为。如果直接用 @Autowired
把 request
作用域的 Bean 注入到单例 Bean,程序启动会报错,因为生命周期不兼容。
- 面试应对 :如果被问到,我可以先说"默认会报错,因为容器启动时没有请求上下文",然后补充"可以用代理模式或
Provider
解决"。 - 加分点 :提到代理模式的实现(CGLIB)和异常类型(
BeanCreationException
),显得更专业。
总结
单例 Bean 和 request
Bean 的注解注入,程序启动会报错,除非采取特殊处理:
- 代理模式 :加
proxyMode
,最常用。 - 延迟加载 :用
Provider
或ObjectFactory
。 - 手动获取 :通过
ApplicationContext
。