SpringBoot31-ApplicationRunner 详细讲解

发布时间:2026/7/26 20:51:55
SpringBoot31-ApplicationRunner 详细讲解 一、什么是 ApplicationRunnerApplicationRunner是 Spring Boot 提供的一个接口用于在 Spring 应用启动完成后立即执行特定的初始化代码。它是实现应用启动后自动执行逻辑的标准方式之一。二、什么时候使用先问一个问题应用启动后我们想执行一些初始化逻辑该写在哪里假设你有一个需求项目启动后要从数据库加载一些配置到 Redis 缓存里。这段代码放在哪里在以下情况下应该使用ApplicationRunner应用启动完成后需要执行一次性的初始化操作比如数据加载、缓存预热、资源检查、定时任务启动等。关键是这些操作必须在应用完全启动后才能进行。三、为什么使用方案 1PostConstructComponent public class CacheWarmUp { Autowired private ConfigService configService; PostConstruct public void init() { // 加载配置到缓存 configService.loadToCache(); } }弊端PostConstruct是在 Bean 实例化并完成依赖注入后调用的。但此时Spring 上下文可能还没有全部刷新完成。如果CacheWarmUp依赖的某个 Bean 需要等上下文完全就绪才能正常工作比如某些后处理器还没执行完这里就会出问题。更关键的是如果有多个 Bean 都用PostConstruct它们的执行顺序不可控且分散在各个类里你不知道哪个先执行、哪个后执行。方案 2InitializingBean接口Component public class CacheWarmUp implements InitializingBean { Override public void afterPropertiesSet() { // 初始化逻辑 } }弊端和PostConstruct本质一样都是在 Bean 初始化阶段回调。它解决不了等整个应用完全准备好再执行的问题而且侵入性更强——你的类必须实现 Spring 的接口。方案 3写在main方法里SpringBootApplication public class DemoApplication { public static void main(String[] args) { ConfigurableApplicationContext context SpringApplication.run(DemoApplication.class, args); // 手动获取 Bean 执行 ConfigService service context.getBean(ConfigService.class); service.loadToCache(); } }弊端代码和 Spring Boot 的启动逻辑耦合在一起如果初始化逻辑很多main方法会变得臃肿无法利用 Spring 的依赖注入来管理这些初始化任务不方便写单元测试方案 4监听ContextRefreshedEventComponent public class StartupListener implements ApplicationListenerContextRefreshedEvent { Override public void onApplicationEvent(ContextRefreshedEvent event) { // 启动逻辑 } }弊端这确实是在上下文刷新完成后执行的但它是一个通用的事件监听机制。它的设计意图是监听事件而不是执行启动任务。用它来做启动初始化语义不清晰。而且如果上下文被多次刷新虽然很少见它会被多次触发。【小结】所以 Spring Boot 设计了ApplicationRunnerSpring Boot 在启动流程中专门留了一个阶段上下文已经完全刷新所有 Bean 都初始化好了但run()方法还没返回。在这个阶段Spring Boot 会查找所有实现了ApplicationRunner和CommandLineRunner接口的 Bean依次执行它们。为什么这样设计能解决上面的弊端问题ApplicationRunner如何解决执行时机太早它在ApplicationContext完全refresh之后才执行所有基础设施数据库连接池、事务管理器、Web 容器等都已就绪代码分散、不可控多个 Runner 可以通过Order注解明确指定执行顺序侵入业务类它是一个独立的接口你可以专门写一个启动任务类不需要污染业务 Beanmain方法臃肿启动逻辑交给 Spring 管理自动发现并执行语义不清接口名就叫ApplicationRunner一看就知道是应用启动后运行四、使用场景1. 缓存预热- 应用启动时加载热点数据到缓存Component public class CachePreloader implements ApplicationRunner { Autowired private UserService userService; Override public void run(ApplicationArguments args) throws Exception { System.out.println(开始预热用户缓存...); userService.preloadUserCache(); System.out.println(缓存预热完成); } }2. 数据库初始化- 创建必要的表或插入初始数据Component public class DatabaseInitializer implements ApplicationRunner { Autowired private JdbcTemplate jdbcTemplate; Override public void run(ApplicationArguments args) throws Exception { try { jdbcTemplate.execute(CREATE TABLE IF NOT EXISTS users (id INT, name VARCHAR(100))); System.out.println(数据库初始化成功); } catch (Exception e) { System.out.println(数据库已存在跳过初始化); } } }3. 定时任务启动- 启动后台定时处理任务Component public class ScheduledTaskStarter implements ApplicationRunner { Autowired private TaskScheduler taskScheduler; Override public void run(ApplicationArguments args) throws Exception { taskScheduler.scheduleAtFixedRate( () - System.out.println(执行后台任务), Duration.ofSeconds(10) ); System.out.println(定时任务已启动); } }4. 访问命令行参数- 根据启动参数进行不同的初始化Component public class ConfigLoader implements ApplicationRunner { Override public void run(ApplicationArguments args) throws Exception { // 获取选项参数如 --debugtrue if (args.containsOption(debug)) { System.out.println(debug 模式已启用); } // 获取非选项参数 ListString nonOptionArgs args.getNonOptionArgs(); System.out.println(传入的参数 nonOptionArgs); } }5. 外部系统连接检查- 验证必需的外部服务是否可用Component public class HealthChecker implements ApplicationRunner { Autowired private RedisTemplate redisTemplate; Autowired private MongoTemplate mongoTemplate; Override public void run(ApplicationArguments args) throws Exception { try { redisTemplate.getConnectionFactory().getConnection().ping(); System.out.println(Redis 连接正常); } catch (Exception e) { System.out.println(Redis 连接失败 e.getMessage()); } try { mongoTemplate.getDb().getName(); System.out.println(MongoDB 连接正常); } catch (Exception e) { System.out.println(MongoDB 连接失败 e.getMessage()); } } }五、怎么使用5-1、基础用法示例1Component public class MyApplicationRunner implements ApplicationRunner { Override public void run(ApplicationArguments args) throws Exception { // 执行初始化逻辑 System.out.println(应用启动完成执行初始化...); } }示例2Component Order(1) // 数字越小越先执行 public class CacheWarmUpRunner implements ApplicationRunner { Autowired private UserService userService; Autowired private RedisTemplateString, Object redisTemplate; Override public void run(ApplicationArguments args) { // 此时所有 Bean 都已就绪可以放心使用 ListUser users userService.findAll(); users.forEach(user - redisTemplate.opsForValue().set(user: user.getId(), user) ); System.out.println(缓存预热完成共加载 users.size() 条数据); } }多个 Runner 控制顺序Component Order(1) public class DataSourceCheckRunner implements ApplicationRunner { Override public void run(ApplicationArguments args) { // 先检查数据库连接 } } Component Order(2) public class CacheWarmUpRunner implements ApplicationRunner { Override public void run(ApplicationArguments args) { // 再预热缓存依赖数据库 } } Component Order(3) public class SchedulerStartRunner implements ApplicationRunner { Override public void run(ApplicationArguments args) { // 最后启动定时任务依赖缓存 } }条件化执行有时候你只想在特定环境下执行Component Profile(dev) // 只在开发环境执行 public class DevDataInitRunner implements ApplicationRunner { Override public void run(ApplicationArguments args) { // 插入一些测试数据 } }5-2、处理异常Component public class RobustApplicationRunner implements ApplicationRunner { Autowired private Logger logger; Override public void run(ApplicationArguments args) throws Exception { try { // 初始化逻辑 initializeApplication(); } catch (Exception e) { logger.error(应用初始化失败, e); // 可以选择抛出异常这会导致应用启动失败 throw new RuntimeException(应用启动异常, e); } } private void initializeApplication() { // 具体逻辑 } }ApplicationRunner里抛出的异常不会被吞掉它会直接向上传播导致整个应用启动失败。这是设计如此——如果初始化任务失败了应用就不应该对外提供服务。5-3、多个 ApplicationRunner 的执行顺序 - 使用Order注解Component Order(1) // 最先执行 public class FirstRunner implements ApplicationRunner { Override public void run(ApplicationArguments args) throws Exception { System.out.println(这是第一个执行的); } } Component Order(2) // 其次执行 public class SecondRunner implements ApplicationRunner { Override public void run(ApplicationArguments args) throws Exception { System.out.println(这是第二个执行的); } }5-4、不要阻塞启动线程ApplicationRunner是在主线程里同步执行的。如果你的逻辑需要执行很长时间比如同步处理大量数据会延迟应用启动导致健康检查接口迟迟不能响应。错误示范Component public class BadRunner implements ApplicationRunner { Override public void run(ApplicationArguments args) { // 同步处理 10 万条数据主线程被阻塞 5 分钟 processHugeData(); } }正确做法Component public class GoodRunner implements ApplicationRunner { Autowired private ExecutorService executorService; // 注入线程池 Override public void run(ApplicationArguments args) { // 提交到线程池异步执行主线程立即返回 executorService.submit(this::processHugeData); } }5-5、参数ApplicationArguments 详解1、是什么ApplicationArguments是 Spring Boot 提供的一个接口用来封装和解析应用启动时传入的命令行参数。它提供了便捷的方法来访问这些参数。2、里面装的是什么ApplicationArguments包含的是应用启动时在命令行传入的所有参数。3、参数分类命令行参数被分为两种1. 选项参数Option Arguments- 形如--keyvalue或--key的参数java -jar myapp.jar --server.port8081 --debugtrue --enable-feature2. 非选项参数Non-Option Arguments-不带--前缀的参数java -jar myapp.jar file1.txt file2.txt4、怎么使用创建一个完整示例来演示ApplicationArguments的各种用法## 实际运行示例import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.stereotype.Component; import java.util.List; Component public class ArgumentParserRunner implements ApplicationRunner { Override public void run(ApplicationArguments args) throws Exception { System.out.println( ApplicationArguments 演示 \n); // 1. 获取所有选项参数名称 System.out.println(【1】所有选项参数的名称); String[] optionNames args.getOptionNames().toArray(new String[0]); System.out.println( java.util.Arrays.toString(optionNames)); System.out.println(); // 2. 检查某个选项是否存在 System.out.println(【2】检查特定选项是否存在); System.out.println( 是否包含 debug 选项 args.containsOption(debug)); System.out.println( 是否包含 server.port 选项 args.containsOption(server.port)); System.out.println(); // 3. 获取单个选项的值 System.out.println(【3】获取单个选项的值); ListString portValues args.getOptionValues(server.port); if (portValues ! null !portValues.isEmpty()) { System.out.println( server.port 的值 portValues.get(0)); } else { System.out.println( server.port 未设置); } System.out.println(); // 4. 获取某个选项的所有值如果该选项指定了多次 System.out.println(【4】获取选项的所有值可能有多个); ListString debugValues args.getOptionValues(debug); if (debugValues ! null) { System.out.println( debug 的所有值 debugValues); } else { System.out.println( debug 未设置); } System.out.println(); // 5. 获取所有非选项参数 System.out.println(【5】所有非选项参数); ListString nonOptionArgs args.getNonOptionArgs(); System.out.println( 非选项参数列表 nonOptionArgs); System.out.println( 非选项参数个数 nonOptionArgs.size()); System.out.println(); // 6. 获取原始参数数组 System.out.println(【6】原始参数数组); String[] sourceArgs args.getSourceArgs(); System.out.println( 原始参数 java.util.Arrays.toString(sourceArgs)); System.out.println(); // 7. 实际应用例子解析参数进行配置 System.out.println(【7】实际应用示例 - 根据参数配置应用); parseApplicationConfig(args); } private void parseApplicationConfig(ApplicationArguments args) { // 解析端口 String port 8080; // 默认值 ListString portValues args.getOptionValues(server.port); if (portValues ! null !portValues.isEmpty()) { port portValues.get(0); } System.out.println( 服务器端口 port); // 解析调试模式 boolean debugMode args.containsOption(debug); System.out.println( 调试模式 (debugMode ? 启用 : 禁用)); // 解析输入文件 ListString files args.getNonOptionArgs(); if (!files.isEmpty()) { System.out.println( 输入文件 String.join(, , files)); } else { System.out.println( 未指定输入文件); } } }5、启动命令# 场景 1只有选项参数 java -jar myapp.jar --server.port9090 --debugtrue --enable-cache # 场景 2混合选项参数和非选项参数 java -jar myapp.jar --server.port8081 --debug file1.txt file2.txt # 场景 3选项参数有多个值 java -jar myapp.jar --include*.txt --include*.log data/6、对应的输出场景 1 输出【1】所有选项参数的名称 [server.port, debug, enable-cache] 【2】检查特定选项是否存在 是否包含 debug 选项true 是否包含 server.port 选项true 【3】获取单个选项的值 server.port 的值9090 【4】获取选项的所有值 debug 的所有值[true] 【5】所有非选项参数 非选项参数列表[] 非选项参数个数0 【7】实际应用示例 - 根据参数配置应用 服务器端口9090 调试模式启用 未指定输入文件场景 2 输出【5】所有非选项参数 非选项参数列表[file1.txt, file2.txt] 非选项参数个数2 【7】实际应用示例 - 根据参数配置应用 服务器端口8081 调试模式启用 输入文件file1.txt, file2.txt7、常用方法总览方法说明示例getOptionNames()获取所有选项参数的名称args.getOptionNames()返回[debug, port]containsOption(String)检查是否包含某个选项args.containsOption(debug)返回true/falsegetOptionValues(String)获取某个选项的所有值args.getOptionValues(port)返回[8080]getNonOptionArgs()获取所有非选项参数args.getNonOptionArgs()返回[file1.txt]getSourceArgs()获取原始参数数组args.getSourceArgs()返回原始数组8、核心概念图解启动命令 java -jar app.jar --debugtrue --port8080 file1.txt file2.txt ^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^ 选项参数 非选项参数 ApplicationArguments 内部结构 ├── optionNames: [debug, port] ├── optionValues: {debug: [true], port: [8080]} └── nonOptionArgs: [file1.txt, file2.txt]9、实际应用场景Component public class DataImportRunner implements ApplicationRunner { Autowired private DataService dataService; Override public void run(ApplicationArguments args) throws Exception { // 获取导入模式 boolean dryRun args.containsOption(dry-run); // 获取并发数 String threads 1; ListString threadValues args.getOptionValues(threads); if (threadValues ! null !threadValues.isEmpty()) { threads threadValues.get(0); } // 获取要导入的文件 ListString files args.getNonOptionArgs(); System.out.println(导入配置 - 干运行 dryRun 线程数 threads 文件数 files.size()); dataService.importData(files, Integer.parseInt(threads), dryRun); } }总结ApplicationArguments就是把命令行参数进行了包装和分类让你可以更方便地访问和处理这些参数。六、ApplicationRunnervsCommandLineRunner功能类似但参数形式不同Spring Boot 实际上提供了两个几乎一样的接口CommandLineRunner和ApplicationRunner的用法和效果几乎完全一样。它们都属于 Spring Boot 的“启动回调接口”用于在应用容器完全初始化后执行一次性的代码。示例Component public class MyCommandLineRunner implements CommandLineRunner { Override public void run(String... args) throws Exception { // args 是 String 数组而不是 ApplicationArguments 对象 System.out.println(参数个数 args.length); for (String arg : args) { System.out.println(参数 arg); } } }// 1. CommandLineRunner - 接收原始参数 public interface CommandLineRunner { void run(String... args) throws Exception; } // 2. ApplicationRunner - 接收封装后的参数 public interface ApplicationRunner { void run(ApplicationArguments args) throws Exception; }唯一的区别在于它们接收命令行参数的方式特性CommandLineRunnerApplicationRunner方法签名void run(String... args) throws Exceptionvoid run(ApplicationArguments args) throws Exception参数类型原始字符串数组(String[])封装对象(ApplicationArguments)参数处理需要手动解析例如解析--namevalueApplicationArguments提供了便捷的方法来区分非选项参数和选项参数。ApplicationRunner比CommandLineRunner强在哪里CommandLineRunner接收的是String... args也就是main方法传进来的原始字符串数组。如果你的启动命令是java -jar app.jar --envprod --debug --server.port8081在CommandLineRunner里你只能拿到[--envprod, --debug, --server.port8081]需要自己解析哪些是选项、哪些是参数。而ApplicationRunner接收的ApplicationArguments已经帮你解析好了Component public class MyStartupTask implements ApplicationRunner { Override public void run(ApplicationArguments args) throws Exception { // 获取所有选项名称env, debug, server.port SetString optionNames args.getOptionNames(); // 判断是否有 --debug boolean debug args.containsOption(debug); // 获取 --envprod 的值 ListString envValues args.getOptionValues(env); // [prod] // 获取非选项参数没有 -- 前缀的 ListString nonOptionArgs args.getNonOptionArgs(); } }结论ApplicationRunner是对CommandLineRunner的增强它提供了更友好的命令行参数 API。如果你不需要解析参数两者完全等价如果需要ApplicationRunner更省事。结论日常开发中如何选择在绝大多数企业级 Web 应用的初始化场景中我们通常不需要关心命令行参数。应用配置大多通过application.yml或配置中心获取很少直接依赖命令行参数。因此当您不处理命令行参数时使用CommandLineRunner或ApplicationRunner效果是完全一样的选哪个都可以。但如果您的应用例如一个批处理应用或一个特殊的启动工具需要复杂地解析和使用命令行参数那么推荐使用ApplicationRunner因为它提供的ApplicationArguments对象能更结构化、更便捷地处理和读取参数。七、和ApplicationReadyEvent的区别Spring Boot 启动完成后还会发布一个ApplicationReadyEvent。它和ApplicationRunner的区别ApplicationRunner上下文刷新后run()返回前执行。此时应用还没完全就绪事件还没发。ApplicationReadyEventrun()已经返回应用完全启动完毕。如果你做的是必须阻塞启动流程的初始化比如加载配置后面的逻辑依赖它用ApplicationRunner。如果你只是想在启动完成后发一条通知用ApplicationReadyEvent。八、总结特性说明用途应用启动完成后执行初始化逻辑优势保证所有 Bean 初始化完成支持依赖注入可访问命令行参数注意事项如果抛出异常应用启动会失败可用 Order 控制多个 Runner 的执行顺序替代方案CommandLineRunner、PostConstruct、InitializingBean