SpringBoot:基于注解 @PostConstruct 和 ApplicationRunner 进行初始化的区别

文章目录

    • [一、 共同点:都是"主线程"在干活](#一、 共同点:都是“主线程”在干活)
    • [二、 核心区别:生命周期的"错位"](#二、 核心区别:生命周期的“错位”)
      • [1. @PostConstruct:装修单间(Bean 初始化阶段)](#1. @PostConstruct:装修单间(Bean 初始化阶段))
      • [2. ApplicationRunner:交房仪式(容器启动完成阶段)](#2. ApplicationRunner:交房仪式(容器启动完成阶段))
      • [3. 时间轴对比](#3. 时间轴对比)
    • [三、 为什么不全用 @PostConstruct ?](#三、 为什么不全用 @PostConstruct ?)
      • [1. 事务与 AOP 的失效陷阱](#1. 事务与 AOP 的失效陷阱)
      • [2. 无法获取命令行参数](#2. 无法获取命令行参数)
      • [3. 容错性与容器崩溃风险](#3. 容错性与容器崩溃风险)
      • [4. 循环依赖异常](#4. 循环依赖异常)
    • [四、 选择考量](#四、 选择考量)
    • [五、 代码示例](#五、 代码示例)
      • [不推荐写法:在 Bean 初始化中做重逻辑](#不推荐写法:在 Bean 初始化中做重逻辑)
      • 职责分离写法
    • [六、 总结](#六、 总结)

关于初始化逻辑:为什么有了 @PostConstruct,还需要 ApplicationRunner?

在 Spring Boot 开发中,我们经常需要在项目启动时执行一些初始化逻辑,比如:加载缓存、启动 Netty 服务、初始化配置等。

涉及到初始化Bean等,可以使用注解 @PostConstruct

关于 @PostConstruct注解详解,参考文章:【深度剖析】:结合 Spring Bean 的生命周期理解 @PostConstruct 的原理

但随着对 Spring Boot 了解的深入,还有一个很好用的接口 ApplicationRunnerCommandLineRunner 来处理启动后的业务逻辑。

不过,既然 @PostConstructApplicationRunnerrun 方法最终都是由 main 主线程执行的,那 Spring Boot 为什么要多此一举搞出个新接口呢?

本文将深入剖析这两者的核心区别,旨在写出更稳健的启动代码。

一、 共同点:都是"主线程"在干活

首先要纠正一个误区:不要以为 ApplicationRunner 是异步执行的。

除非在方法内部显式开启了新线程,否则 @PostConstructApplicationRunner 默认都是由 main 线程(即启动 Spring 应用的主线程)同步执行的

这意味着:无论我用哪一个,如果在里面执行了死循环或超长耗时的操作,都会导致 Spring Boot 应用启动卡死,无法对外提供服务。

既然线程模型一样,那区别到底在哪?答案在于:时机状态

二、 核心区别:生命周期的"错位"

我们可以把 Spring Boot 的启动过程比作 "盖房子"

1. @PostConstruct:装修单间(Bean 初始化阶段)

  • 触发时机:当 Spring 正在创建某个具体的 Bean,完成了构造函数调用和属性注入(Autowired)之后,立刻执行。
  • 当前状态 :此时房子还在盖,Spring 容器还没完全准备好。可能还有很多其他的 Bean 还没被创建,AOP 代理还没生成。
  • 定位 :它关注的是 "Bean 自身" 的完整性。

2. ApplicationRunner:交房仪式(容器启动完成阶段)

  • 触发时机:当所有的 Bean 都初始化完毕,Spring 容器完全 refresh 结束,准备对外服务之前。
  • 当前状态 :房子盖好了,水通了电通了,钥匙交到我手上了。Spring 容器已完全就绪
  • 定位 :它关注的是 "整个应用" 的业务逻辑。

3. 时间轴对比

text 复制代码
Spring Boot 启动开始 (main 线程)
      ↓
1. 扫描并创建 Bean A
      ↓
2. Bean A 属性注入
      ↓
3. 执行 Bean A 的 @PostConstruct  <--- 【此处是装修】
      ↓
4. Bean A 创建完成
      ↓
... (循环创建其他 Bean) ...
      ↓
5. Spring 容器刷新完成 (Context Refreshed)
      ↓
6. 执行 ApplicationRunner / CommandLineRunner <--- 【此处是交房】
      ↓
7. 应用启动结束 (Started),端口对外开放

三、 为什么不全用 @PostConstruct ?

虽然 @PostConstruct 用起来简单(加个注解就行),但在以下复杂场景下,它存在致命缺陷:

1. 事务与 AOP 的失效陷阱

这是最容易踩的坑。Spring 的事务(@Transactional)和切面(AOP)是基于动态代理 实现的。

@PostConstruct 执行时,Bean 的后置处理器(BeanPostProcessor)可能还没有机会为当前 Bean 生成代理对象。
结果就是:@PostConstruct 方法里调用的事务方法,可能只是个普通方法调用,事务根本没生效。(直接被this调用了,而非被代理对象,也就是Bean调用)

ApplicationRunner 执行时,所有代理对象均已生成,安全无虞。

2. 无法获取命令行参数

@PostConstruct 只是一个标准的 Java 方法,它无法感知 Spring Boot 启动时的参数。

如果想根据 java -jar app.jar --server.port=9090 --my.config=123 中的参数来做初始化,@PostConstruct 做不到。

ApplicationRunner 的接口定义如下:

java 复制代码
void run(ApplicationArguments args) throws Exception;

它可以轻松获取所有命令行参数。

3. 容错性与容器崩溃风险

以"我们想要在SpringBoot的服务器(处理HTTP)启动完毕后,再单独启动一个 Netty 服务器(处理WebSocket)"的场景为例:

  • 如果在 @PostConstruct 中启动 :Spring 刚把我的 Bean 实例化,还没把它放入容器。如果 Netty 启动抛出异常,异常会直接中断 Bean 的创建流程,导致 整个 Spring 容器启动失败,进程退出
  • 如果在 ApplicationRunner 中启动 :Spring 容器已经启动成功了 。即使 Netty 报错,仍可以选择 catch 异常并打印日志,甚至降级服务,而不会导致整个 Web 服务直接挂掉。

4. 循环依赖异常

如果 Bean A 的 @PostConstruct 依赖了 Bean B,而 Bean B 正在创建中,很容易引发复杂的加载顺序问题,出现循环依赖,导致程序异常等问题

四、 选择考量

为了让代码更清晰、更稳健,可以遵循以下原则:

场景 推荐方案 理由
轻量级数据校验 @PostConstruct 例如:检查注入的 API Key 是否为空。这是 Bean 自身合法性的检查。
内部数据初始化 @PostConstruct 例如:将注入的 List 转为 Map 以便快速查找。这是 Bean 内部逻辑。
启动对外服务 ApplicationRunner 例如:开启 Netty、gRPC 服务、启动消费者线程。
复杂业务预热 ApplicationRunner 例如:从数据库查配置加载到 Redis。此时容器完全就绪,数据库连接池和 AOP 均可用。
依赖命令行参数 ApplicationRunner 只有它能拿到 ApplicationArguments

五、 代码示例

不推荐写法:在 Bean 初始化中做重逻辑

java 复制代码
@Component
public class MyNettyServer {
    
    @PostConstruct
    public void init() {
        // 风险:如果端口被占用导致异常,整个 Spring Boot 都会启动失败
        // 且此时 AOP 可能未生效
        this.startServer(); 
    }
}

职责分离写法

此处为了不阻塞 main 线程,甚至可以使用单独 new 一个 Thread 来干run方法里面的活

java 复制代码
@Component
@Slf4j
public class MyNettyStarter implements ApplicationRunner {

    @Autowired
    private MyNettyServer nettyServer;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        // 此时 Spring 容器已就绪
        try {
            nettyServer.start();
            log.info("Netty 启动成功,参数:" + args.getOptionNames());
        } catch (Exception e) {
            // 可以选择记录日志,不影响主容器运行
            log.error("Netty 启动失败,请检查配置");
        }
    }
}

六、 总结

@PostConstruct 是管好"自己",ApplicationRunner 是管好"大家"。

  • 如果只是想让这个 Bean 在用之前把自己"洗干净"(赋值、校验),用 @PostConstruct
  • 如果是想在应用启动后搞点"大动作"(启动服务、预热数据、通过网络交互),则务必使用 ApplicationRunner
相关推荐
瑞雪兆丰年兮1 小时前
[从0开始学Java|第一天]Java入门
java·开发语言
我爱娃哈哈1 小时前
SpringBoot 实现 RSA+AES 自动接口解密
java·spring boot·后端
沈雅馨1 小时前
SQL语言的云计算
开发语言·后端·golang
东东最爱敲键盘2 小时前
第7天 进程间通信
java·服务器·前端
九皇叔叔2 小时前
【04】SpringBoot3 MybatisPlus 查询(Mapper)
java·mybatis·mybatis plus
人道领域2 小时前
javaWeb从入门到进阶(SpringBoot基础案例)
java·开发语言·spring
u0104058362 小时前
利用Java CompletableFuture优化企业微信批量消息发送的异步编排
java·开发语言·企业微信
yangminlei2 小时前
SpringSecurity核心源码剖析+jwt+OAuth(一):SpringSecurity的初次邂逅(概念、认证、授权)
java·开发语言·python
小张快跑。2 小时前
【SpringBoot进阶指南(一)】SpringBoot整合MyBatis实战、Bean管理、自动配置原理、自定义starter
java·开发语言·spring boot