文章目录
-
- [一、 源头:并非 Spring 原生](#一、 源头:并非 Spring 原生)
- [二、 核心作用:填补"构造函数"的短板](#二、 核心作用:填补“构造函数”的短板)
-
- [1. 构造函数的局限性](#1. 构造函数的局限性)
- [2. @PostConstruct 的时机](#2. @PostConstruct 的时机)
- [三、 执行时机:Spring Bean 生命周期](#三、 执行时机:Spring Bean 生命周期)
- [四、 底层原理:Spring 是如何发现并执行它的?](#四、 底层原理:Spring 是如何发现并执行它的?)
-
- [1. 底层核心:CommonAnnotationBeanPostProcessor](#1. 底层核心:CommonAnnotationBeanPostProcessor)
- [2. 源码级解析(简化版逻辑)](#2. 源码级解析(简化版逻辑))
- [五、 使用规范与注意事项](#五、 使用规范与注意事项)
- [六、 横向对比:初始化的三种方式](#六、 横向对比:初始化的三种方式)
- [七、 总结](#七、 总结)
在 Java 开发(尤其是 Spring 开发)中,@PostConstruct 是一个出现频率极高的注解。我们都知道它用于"初始化",但:
- 它和构造函数(Constructor)有什么区别?
- 为什么在构造函数里不能使用
@Autowired注入的 Bean,而在@PostConstruct里却可以? - Spring 到底是在什么时候、通过什么机制调用它的?
- 它是 Spring 的原生注解吗?
本文将从 Spring Bean 的生命周期开始剖析,彻底弄清 @PostConstruct 背后的知识。
一、 源头:并非 Spring 原生
首先要纠正一个常见的认知误区:@PostConstruct 并不是 Spring 框架定义的注解。

它是 JSR-250 (Java Specification Requests) 规范的一部分,最初位于 javax.annotation 包下(Java EE 规范)。
- Java 9 之前:它包含在 JDK 的核心库中。
- Java 9 及以后:它被移出了 JDK 核心库。
- 现在的归宿 :随着 Java EE 转型为 Jakarta EE(javax -> jakarta ),现在通常位于
jakarta.annotation包中。
Spring 只是遵循并实现了这个规范 ,就像 Spring 支持 @Resource(也是 JSR-250)一样。这使得我们的代码具有更好的移植性,不完全绑定在 Spring 框架上。
二、 核心作用:填补"构造函数"的短板
要理解 @PostConstruct 的价值,必须先理解 Java 对象的创建过程与 Spring Bean 的创建过程的区别。
1. 构造函数的局限性
在普通的 Java 类中,构造函数是对象生的地方。但在 Spring 容器中,Bean 的创建过程被拉长了。
看下面这个反例:
注意,下例是字段注入,而不是构造器注入
java
@Component
public class UserService {
@Autowired
private UserDao userDao;
public UserService() {
// 错误!此时 userDao 还是 null!
// 因为构造函数执行时,Spring 还没来得及把依赖注入进来
userDao.queryUserList();
}
}
为什么会空指针(NPE)?
因为 Java 语言规定,构造函数是对象实例化的第一步(先执行构造函数 new,再初始化对象属性,即成员变量)。
而 Spring 的依赖注入逻辑是:先通过构造函数创建 Bean 实例,再通过反射给 @Autowired 标记的字段赋值 ------ 因此构造函数执行时,注入的属性还未被赋值,必然为 null。
2. @PostConstruct 的时机
我们需要一个时机:对象已经实例化了,且所有的依赖(属性)都已经注入完毕了,但 Bean 还没真正对外提供服务之前。
这就是 @PostConstruct 的执行时机。
java
@Component
public class UserService {
@Autowired
private UserDao userDao;
public UserService() {
System.out.println("1. 构造函数执行");
}
@PostConstruct
public void init() {
System.out.println("2. 依赖注入已完成,可以调用 userDao");
// 正确!此时 userDao 已经被 Spring 注入了
userDao.queryUserList();
}
}
三、 执行时机:Spring Bean 生命周期
参考文章:Spring的Bean的生命周期
这是理解原理的重中之重。一个 Bean 从无到有,经历了以下关键步骤:
text
Spring 容器启动
↓
1. 【Instantiation】实例化 Bean
(执行构造函数 Constructor)
↓
2. 【Populate Properties】属性赋值
(处理 @Autowired, @Value, Setter 注入)
↓
3. 【Initialization】初始化阶段
↓
3.1 执行 BeanNameAware, BeanFactoryAware 等接口方法
↓
3.2 执行 BeanPostProcessor 的 postProcessBeforeInitialization 方法
@PostConstruct 由 CommonAnnotationBeanPostProcessor 的 postProcessBeforeInitialization 方法触发执行
↓
3.3 执行 InitializingBean 接口的 afterPropertiesSet() 方法
↓
3.4 执行 XML 中配置的 init-method 方法
↓
3.5 执行 BeanPostProcessor 的 postProcessAfterInitialization 方法
↓
4. Bean 创建完成,放入单例池 (SingletonObjects)
↓
5. Bean 销毁 (执行 @PreDestroy)
总结执行顺序:
Constructor (构造函数) -> @Autowired (依赖注入) -> @PostConstruct (初始化方法)
四、 底层原理:Spring 是如何发现并执行它的?
Spring 是怎么知道你写了一个 @PostConstruct 方法的?这归功于 Spring 强大的 BeanPostProcessor(Bean 后置处理器) 机制。
1. 底层核心:CommonAnnotationBeanPostProcessor
在 Spring 容器启动时,会注册一系列的后置处理器。其中有一个非常重要的类叫做:
org.springframework.context.annotation.CommonAnnotationBeanPostProcessor
这个类专门负责处理 JSR-250 规范的注解(包括 @PostConstruct、@PreDestroy、@Resource)。
2. 源码级解析(简化版逻辑)
当 Spring 创建 Bean 走到 初始化阶段 时,CommonAnnotationBeanPostProcessor 会介入:
- 扫描:它会利用 Java 反射机制,扫描当前 Bean 的所有方法。
- 识别 :检查方法上是否有
@PostConstruct注解。 - 调用 :如果发现了,就通过反射(
Method.invoke())来执行这个方法。
伪代码模拟流程:
java
// 这是 Spring 内部的大致逻辑
public Object postProcessBeforeInitialization(Object bean, String beanName) {
// 1. 获取这个 Bean 的 Class
Class<?> clazz = bean.getClass();
// 2. 扫描所有方法
for (Method method : clazz.getDeclaredMethods()) {
// 3. 判断是否有 @PostConstruct 注解
if (method.isAnnotationPresent(PostConstruct.class)) {
// 4. 强制设置可访问(因为通常是 private 的)
method.setAccessible(true);
// 5. 执行方法
method.invoke(bean);
}
}
return bean;
}
实际代码:



注:Spring 内部实际使用了缓存机制(InjectionMetadata)来避免重复扫描,提高性能,但核心逻辑就是反射无疑。
五、 使用规范与注意事项
虽然使用简单,但有几个点必须注意,否则可能导致报错或行为异常:
- 返回值必须为 void :
@PostConstruct标记的方法不能有返回值。因为这是初始化过程,Spring 不会处理你的返回值。 - 不能有参数 :
方法参数列表必须为空。Spring 调用它时是无参调用的。 - 访问权限 :
可以是public、protected、package-private甚至private。Spring 会通过反射暴力访问,但不能是static的。 - 只执行一次 :
对于单例 Bean(Singleton),该方法在容器启动时只执行一次。对于多例 Bean(Prototype),每次创建 Bean 实例时都会执行。 - 耗时操作警告 :
正如之前的文章提到,该方法由main线程同步执行 。千万不要在这里写死循环或者长耗时的 IO 操作,否则会卡死 Spring 启动过程!(如需耗时操作,请移步ApplicationRunner)。
六、 横向对比:初始化的三种方式
Spring 提供了三种 Bean 初始化的方式,它们的执行顺序如下:
@PostConstruct(注解方式,推荐)- 优点:符合 Java 标准,代码侵入性小,直观。
InitializingBean接口 (实现afterPropertiesSet方法)- 优点:Spring 强类型约束。
- 缺点:代码耦合了 Spring 接口。
init-method(XML 配置或 @Bean(initMethod="..."))- 优点:完全解耦,类中不需要引入任何 Spring 代码。
- 缺点:配置繁琐,容易忘记。
执行顺序优先级:
@PostConstruct > InitializingBean#afterPropertiesSet() > init-method
七、 总结
@PostConstruct 是连接 Java 对象实例化与 Spring 容器管理的桥梁。
- 它解决了什么? 解决了构造函数无法使用依赖注入属性的问题。
- 它在哪里? 位于依赖注入之后,Bean 正式投入使用之前。
- 它是怎么运行的? 依靠
CommonAnnotationBeanPostProcessor通过反射机制在 Bean 初始化阶段回调。
掌握了这个注解,即掌握了 Spring Bean 生命周期的核心一环,无论是写业务初始化代码,还是阅读源码,都将更加得心应手。