跳到主要内容
Spring Bean 的作用域、生命周期与自动装配——从示例到源码 | 极客日志
Java java
Spring Bean 的作用域、生命周期与自动装配——从示例到源码 从实例出发,剖析Spring Bean的五种作用域及原型代理机制;详细走查Bean生命周期步骤,解释日志与源码顺序反直觉现象;讨论SpringBoot自动装配原理,包括@ComponentScan、@Import和自定义注解,并追踪@EnableAutoConfiguration源码加载自动配置类过程。
数字游民 发布于 2026/6/30 更新于 2026/7/7 4 浏览SpringBoot 版本:3.5.8
1. Bean 的作用域
Spring 提供了五种作用域,决定了 Bean 的可见范围和使用方式。下面这个配置类把它们都列出来了,配合控制器能直接观察行为。
public class Dog {}
@Configuration
public class DogConfig {
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public Dog singleDog () {
return new Dog ();
}
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Dog prototypeDog () {
return new Dog ();
}
@Bean
@RequestScope
public Dog requestDog () {
return new Dog ();
}
@Bean
@SessionScope
public Dog sessionDog () {
return new Dog ();
}
@Bean
Dog {
();
}
}
{
{
SpringApplication.run(SpringPrincipleApplication.class, args);
}
}
{
Dog single;
Dog prototype;
Dog request;
Dog session;
Dog application;
ApplicationContext context;
{
.single = single;
.prototype = prototype;
.request = request;
.session = session;
.application = application;
.context = context;
}
String {
(Dog) context.getBean( );
+ .single.toString() + + + singleDog;
}
String {
(Dog) context.getBean( );
+ .prototype.toString() + + + prototypeDog;
}
String {
(Dog) context.getBean( );
+ .request.toString() + + + requestDog;
}
String {
(Dog) context.getBean( );
+ .session.toString() + + + sessionDog;
}
String {
(Dog) context.getBean( );
+ .application.toString() + + + applicationDog;
}
}
@ApplicationScope
public
applicationDog
()
return
new
Dog
@SpringBootApplication
public
class
SpringPrincipleApplication
public
static
void
main
(String[] args)
@RequestMapping("/test")
@RestController
public
class
TestController
private
final
private
final
private
final
private
final
private
final
private
final
public
TestController
(@Qualifier("singleDog") Dog single,
@Qualifier("prototypeDog") Dog prototype,
@Qualifier("requestDog") Dog request,
@Qualifier("sessionDog") Dog session,
@Qualifier("applicationDog") Dog application,
ApplicationContext context)
this
this
this
this
this
this
@RequestMapping("/single")
public
single
()
Dog
singleDog
=
"singleDog"
return
"dog: "
this
"<br>"
"contextDog: "
@RequestMapping("/prototype")
public
prototype
()
Dog
prototypeDog
=
"prototypeDog"
return
"dog: "
this
"<br>"
"contextDog: "
@RequestMapping("/request")
public
request
()
Dog
requestDog
=
"requestDog"
return
"dog: "
this
"<br>"
"contextDog: "
@RequestMapping("/session")
public
session
()
Dog
sessionDog
=
"sessionDog"
return
"dog: "
this
"<br>"
"contextDog: "
@RequestMapping("/application")
public
application
()
Dog
applicationDog
=
"applicationDog"
return
"dog: "
this
"<br>"
"contextDog: "
直接通过 context.getBean() 获取会触发新实例创建(原型、请求等作用域)。
如果用 @Autowired 或 @Resource 注入原型 Bean,注入只发生一次——容器启动时把实例绑定到字段上,后面每次访问都是同一个对象,不会自动获取新实例。这是一个容易迷惑的地方。
代理注入 :对于请求作用域的 Bean,Spring 注入的是一个代理,不是真实实例。启功时就把代理给到单例 Bean 里,但真实实例等到 HTTP 请求来了才创建。当调用代理的方法时,它会从当前请求上下文中查找或创建真正的对象,这样每个请求都能拿到独立的实例。
Singleton(单例) :默认,整个容器只有一个实例。
Prototype(原型) :每次获取(getBean 或方法注入)都会创建新实例。
Request(请求) :每个 HTTP 请求一个实例,仅 Web 环境有效。
Session(会话) :每个用户会话一个实例,仅 Web 环境有效。
Application(应用) :整个 Web 应用共享一个实例,相当于 ServletContext 级别。
2. Bean 的生命周期 生命周期大致分五步:实例化 → 属性赋值 → 初始化 → 使用 → 销毁。初始化这一步里又细化了好几个回调,Spring 提供了不少扩展点。
实例化 :容器通过反射调用构造器创建对象。
属性赋值 :注入 @Autowired 之类的依赖。
初始化 :
通知方法(如 BeanNameAware):把框架资源或上下文传给 Bean。
前置处理(postProcessBeforeInitialization):校验、资源加载等。
初始化回调(afterPropertiesSet 或 @PostConstruct):属性都设好之后的定制逻辑。
后置处理(postProcessAfterInitialization):收尾、代理增强等。
使用 Bean :就绪,业务代码调用。
销毁 Bean :容器关闭时触发 @PreDestroy 或 DisposableBean。
2.1 示例 下面的 BeanLifeComponent 实现了几个关键接口,把生命周期每一步都打上日志。
public class Cat {}
@Configuration
public class CatConfig {
@Bean
public Cat cat () {
return new Cat ();
}
}
@Component
@Slf4j
public class BeanLifeComponent implements BeanNameAware , BeanPostProcessor, InitializingBean {
private Cat cat;
public BeanLifeComponent () {
log.info("1.实例化:执行构造方法" );
}
@Autowired
public void setCat (Cat cat) {
log.info("2.属性赋值:执行 setter 方法" );
this .cat = cat;
}
@Override
public void setBeanName (String name) {
log.info("3.1 通知方法调用,bean name is {}" , name);
}
@Override
public Object postProcessBeforeInitialization (Object bean, String beanName) throws BeansException {
log.info("3.3 前置处理,bean:{},beanName:{}" , bean, beanName);
return BeanPostProcessor.super .postProcessBeforeInitialization(bean, beanName);
}
@Override
public void afterPropertiesSet () {
log.info("3.2 初始化回调" );
}
@Override
public Object postProcessAfterInitialization (Object bean, String beanName) throws BeansException {
log.info("3.4 后置处理,bean:{},beanName:{}" , bean, beanName);
return BeanPostProcessor.super .postProcessAfterInitialization(bean, beanName);
}
public void use () {
log.info("4.使用 Bean" );
}
@PreDestroy
public void preDestroy () {
log.info("5.销毁 Bean" );
}
}
@SpringBootTest
class SpringPrincipleApplicationTests {
private final ApplicationContext context;
@Autowired
public SpringPrincipleApplicationTests (ApplicationContext context) {
this .context = context;
}
@Test
public void test () {
BeanLifeComponent beanLifeComponent = context.getBean(BeanLifeComponent.class);
beanLifeComponent.use();
}
}
运行后日志的顺序却是:invokeInitMethods(初始化回调)→ applyBeanPostProcessorsBeforeInitialization(前置处理)→ applyBeanPostProcessorsAfterInitialization(后置处理)。这跟前面列出的'前置→初始化→后置'流程对不上,原因在源码里藏着,下面细说。
2.2 源码解析 AbstractAutowireCapableBeanFactory 负责 Bean 的创建、注入和初始化。它的 createBean 会调用 doCreateBean,里面依次执行 createBeanInstance(实例化)、populateBean(属性赋值)、initializeBean(初始化)。
initializeBean 方法的调用顺序是:invokeAwareMethods → applyBeanPostProcessorsBeforeInitialization → invokeInitMethods → applyBeanPostProcessorsAfterInitialization。所以按源码逻辑,顺序应该是前置处理 → 初始化回调 → 后置处理 。
2.3 日志与源码'冲突'的原因 关键点:BeanPostProcessor 本身也是个 Bean 。当 Spring 初始化 BeanLifeComponent(它实现了 BeanPostProcessor)时,这个初始化过程是递归的。
对普通 Bean,事先注册好的 BeanPostProcessor 会按照'前置→初始化→后置'的顺序作用于它。
但对一个 BeanPostProcessor Bean 自己:
它必须先完成自身的 invokeInitMethods(即 afterPropertiesSet),然后才能被当作一个可用的 BeanPostProcessor 注册到容器里。
在它自己的创建过程中,它自己的 postProcessBeforeInitialization 和 postProcessAfterInitialization 方法不会被调用——因为它们只有在对象已经初始化完毕、成为容器里一个合格的 BeanPostProcessor 之后,才会在处理其他 Bean 时生效。
所以打印出来的日志里,'初始化回调'在前,而'前置处理/后置处理'其实是当这个 BeanPostProcessor 实例已经就绪后,又去处理其他 Bean(或者重新处理自己?实际上示例中 BeanLifeComponent 本身也会被自己这个 BeanPostProcessor 处理一遍,但那是发生在它已经初始化完成并被注册之后)的输出。日志看着像是先回调再前后置,其实就是 BeanPostProcessor 初始化与注册的先后关系导致的。
3. SpringBoot 自动装配 自动装配的核心思想是'约定优于配置',让开发者不用手动注册 Bean,而是通过引入 jar 包并遵守约定,实现自动发现和加载配置类。
SpringBoot 在启动时,会扫描 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件里声明的配置类,把它们统统加载到 IoC 容器里。如果自己的组件不在默认扫描路径下,也有几种方式让它被识别。
@Slf4j
@Component
public class TestConfig {
public void demo () {
log.info("demo" );
}
}
@SpringBootTest
public class SpringPrincipleApplicationTests {
private final ApplicationContext context;
@Autowired
public SpringPrincipleApplicationTests (ApplicationContext context) {
this .context = context;
}
@Test
public void demo () {
TestConfig testConfig = context.getBean(TestConfig.class);
testConfig.demo();
}
}
如果 TestConfig 不在启动类所在包或子包下,测试会报错。解决手段有三种。
3.1.1 @ComponentScan @SpringBootApplication
@ComponentScan("com.example.springprincicle.component")
public class SpringPrincipleApplication {
public static void main (String[] args) {
SpringApplication.run(SpringPrincipleApplication.class, args);
}
}
3.1.2 @Import 可以导入单个配置类,或者通过 ImportSelector 批量导入。
@SpringBootApplication
@Import(TestConfig.class)
public class SpringPrincipleApplication {
public static void main (String[] args) {
SpringApplication.run(SpringPrincipleApplication.class, args);
}
}
@Slf4j
@Component
public class DemoConfig {
public void demo () {
log.info("demo" );
}
}
public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String []{"com.example.springprincicle.component.TestConfig" , "com.example.springprincicle.component.DemoConfig" };
}
}
@SpringBootTest
public class SpringPrincipleApplicationTests {
private final ApplicationContext context;
@Autowired
public SpringPrincipleApplicationTests (ApplicationContext context) {
this .context = context;
}
@Test
public void demo () {
TestConfig testConfig = context.getBean(TestConfig.class);
testConfig.demo();
DemoConfig demoConfig = context.getBean(DemoConfig.class);
demoConfig.demo();
}
}
3.1.3 自定义注解 让第三方库的用户更舒服——库的开发者会提供一个注解,内部封装 @Import,使用时只需要加一个注解就行。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(MyImportSelector.class)
public @interface EnableTestConfig {}
@SpringBootApplication
@EnableTestConfig
public class SpringPrincipleApplication {
public static void main (String[] args) {
SpringApplication.run(SpringPrincipleApplication.class, args);
}
}
3.2 @SpringBootApplication 源码解析 @SpringBootApplication 是个组合注解,拆开来主要是三个。
3.2.1 @ComponentScan 自动扫描当前类所在的包及其子包。它排除了不需要的配置类,避免重复加载。因为在 @SpringBootApplication 里没有指定 basePackages,所以默认扫描路径就是启动类所在包。
3.2.2 @SpringBootConfiguration 本质上就是个 @Configuration,标记当前类是配置类。附带 @Indexed 注解,为启动阶段的组件扫描提供索引,提升速度。
3.2.3 @EnableAutoConfiguration 重头戏在这里。它通过 @Import 引入了 AutoConfigurationImportSelector,这个类实现了 DeferredImportSelector,负责在启动时动态加载所有自动配置类。
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
protected AutoConfigurationEntry getAutoConfigurationEntry (AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
configurations = removeDuplicates(configurations);
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = getConfigurationClassFilter().filter(configurations);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry (configurations, exclusions);
}
fireAutoConfigurationImportEvents 体现了向后兼容:它仍然会从 META-INF/spring.factories 加载监听器,尽管 Spring Boot 3.x 主推 .imports 文件,但老路径仍然生效。
private void fireAutoConfigurationImportEvents (List<String> configurations, Set<String> exclusions) {
List<AutoConfigurationImportListener> listeners = getAutoConfigurationImportListeners();
if (!listeners.isEmpty()) {
AutoConfigurationImportEvent event = new AutoConfigurationImportEvent (this , configurations, exclusions);
for (AutoConfigurationImportListener listener : listeners) {
invokeAwareMethods(listener);
listener.onAutoConfigurationImportEvent(event);
}
}
}
protected List<AutoConfigurationImportListener> getAutoConfigurationImportListeners () {
return SpringFactoriesLoader.loadFactories(AutoConfigurationImportListener.class, this .beanClassLoader);
}
public static <T> List<T> loadFactories (Class<T> factoryType, @Nullable ClassLoader classLoader) {
return forDefaultResourceLocation(classLoader).load(factoryType);
}
public static SpringFactoriesLoader forDefaultResourceLocation (@Nullable ClassLoader classLoader) {
return forResourceLocation(FACTORIES_RESOURCE_LOCATION, classLoader);
}
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories" ;
按约定,第三方依赖应该把自动配置类名写进 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,这样 SpringBoot 就能自动发现。getCandidateConfigurations 方法正是用来读取这些文件内容:
protected List<String> getCandidateConfigurations (AnnotationMetadata metadata, AnnotationAttributes attributes) {
ImportCandidates importCandidates = ImportCandidates.load(this .autoConfigurationAnnotation, getBeanClassLoader());
List<String> configurations = importCandidates.getCandidates();
Assert.state(!CollectionUtils.isEmpty(configurations), "No auto configuration classes found in " +
"META-INF/spring/" + this .autoConfigurationAnnotation.getName() + ".imports. If you " +
"are using a custom packaging, make sure that file is correct." );
return configurations;
}
这个方法不仅加载 SpringBoot 自带的自动配置,还会收集类路径下所有依赖的 .imports 文件里声明的配置类,这才是'自动装配'能够开箱即用的根本。
相关免费在线工具 Keycode 信息 查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
Escape 与 Native 编解码 JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
JavaScript / HTML 格式化 使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
JavaScript 压缩与混淆 Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online