0%

Runner使用

Spring Boot Runner 详解:ApplicationRunner 与 CommandLineRunner 实战指南

在 Spring Boot 应用中,若需在 SpringApplication.run() 启动完成后自动执行初始化逻辑(如加载配置、初始化缓存、校验依赖服务),可通过 ApplicationRunnerCommandLineRunner 接口实现。这两个接口均为 Spring Boot 提供的 “启动后回调” 扩展点,核心作用是在 Spring 上下文初始化完成后、应用对外提供服务前执行自定义逻辑。从 “接口差异→实现方式→执行顺序→实战场景” 四个维度,系统讲解 Runner 的使用方法与底层原理。

Runner 接口核心作用与差异

ApplicationRunnerCommandLineRunner 功能高度相似,均用于 “启动后执行逻辑”,但在参数接收方式上存在关键差异,适用于不同场景。

1. 接口定义对比

(1)ApplicationRunner 接口
1
2
3
4
5
6
7
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;

public interface ApplicationRunner {
// 参数为 ApplicationArguments 对象,支持解析命令行参数(含选项参数和非选项参数)
void run(ApplicationArguments args) throws Exception;
}
(2)CommandLineRunner 接口
1
2
3
4
5
6
import org.springframework.boot.CommandLineRunner;

public interface CommandLineRunner {
// 参数为 String 数组,直接接收原始命令行参数(不解析,按输入顺序存储)
void run(String... args) throws Exception;
}

2. 核心差异:参数处理方式

对比维度 ApplicationRunner CommandLineRunner
参数类型 ApplicationArguments 对象 String... 原始数组
参数解析能力 支持解析 “选项参数”(如 --name=张三)和 “非选项参数”(如 dev 不解析,直接返回原始输入(如 --name=张三 会原样存储为字符串)
参数获取方式 通过 getOptionNames()/getOptionValues() 获取选项参数,getNonOptionArgs() 获取非选项参数 遍历字符串数组,需手动解析参数格式
适用场景 需解析命令行选项参数(如 --port=8081 仅需接收简单非选项参数(如环境标识 dev

3. ApplicationArguments 类核心方法

ApplicationArgumentsApplicationRunner 的核心参数类,提供了强大的命令行参数解析能力,关键方法如下:

方法名 作用描述 示例(命令行输入:java -jar app.jar --name=张三 dev 8081
getOptionNames() 获取所有 “选项参数名”(以 -- 开头的参数名) 返回 ["name"]
getOptionValues(String name) 根据选项名获取参数值(支持多值,如 --tag=java --tag=spring getOptionValues("name") 返回 ["张三"]
containsOption(String name) 判断是否包含指定选项参数 containsOption("name") 返回 true
getNonOptionArgs() 获取所有 “非选项参数”(不以 -- 开头的参数) 返回 ["dev", "8081"]
getSourceArgs() 获取原始命令行参数数组(与 CommandLineRunner 接收的参数一致) 返回 ["--name=张三", "dev", "8081"]

Runner 接口的实现与使用

无论是 ApplicationRunner 还是 CommandLineRunner,实现步骤均为 “实现接口→重写 run 方法→注册为 Spring Bean”,核心是通过 @Component@Bean 确保 Runner 被 Spring 扫描并管理。

1. ApplicationRunner 实现示例(解析选项参数)

场景:启动时通过命令行参数指定 “环境” 和 “端口”,并初始化对应配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import java.util.List;

// 1. 标注 @Component,注册为 Spring Bean
@Component
public class ConfigInitRunner implements ApplicationRunner {

@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("=== ConfigInitRunner 执行初始化 ===");

// 2. 解析选项参数(--env=dev --port=8081)
// 判断是否包含 "env" 选项
if (args.containsOption("env")) {
List<String> envValues = args.getOptionValues("env");
String env = envValues.get(0); // 获取第一个 "env" 参数值(如 "dev")
System.out.println("当前环境:" + env);
// 执行环境相关初始化(如加载 dev 环境配置)
initConfigByEnv(env);
}

// 3. 解析非选项参数(如 "init-cache")
List<String> nonOptionArgs = args.getNonOptionArgs();
if (nonOptionArgs.contains("init-cache")) {
System.out.println("开始初始化缓存...");
initCache(); // 调用缓存初始化方法
}

System.out.println("=== ConfigInitRunner 初始化完成 ===");
}

// 模拟:根据环境加载配置
private void initConfigByEnv(String env) {
if ("dev".equals(env)) {
System.out.println("加载开发环境配置...");
} else if ("prod".equals(env)) {
System.out.println("加载生产环境配置...");
}
}

// 模拟:初始化缓存
private void initCache() {
// 实际逻辑:如从数据库加载热点数据到 Redis
System.out.println("缓存初始化完成(热点数据加载成功)");
}
}
启动命令与执行结果:
1
2
# 命令行输入:指定选项参数 --env=dev 和非选项参数 init-cache
java -jar app.jar --env=dev init-cache

执行结果

1
2
3
4
5
6
=== ConfigInitRunner 执行初始化 ===
当前环境:dev
加载开发环境配置...
开始初始化缓存...
缓存初始化完成(热点数据加载成功)
=== ConfigInitRunner 初始化完成 ===

2. CommandLineRunner 实现示例(处理原始参数)

场景:启动时接收原始命令行参数(如 “数据初始化标识”),执行数据导入逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class DataImportRunner implements CommandLineRunner {

@Override
public void run(String... args) throws Exception {
System.out.println("=== DataImportRunner 执行数据初始化 ===");

// 遍历原始命令行参数(不解析,直接处理)
for (String arg : args) {
if ("import-data".equals(arg)) {
System.out.println("开始导入初始数据...");
importInitData(); // 执行数据导入
break;
}
}

System.out.println("=== DataImportRunner 执行完成 ===");
}

// 模拟:导入初始数据(如用户、角色数据)
private void importInitData() {
// 实际逻辑:如调用 MyBatis 插入初始数据到数据库
System.out.println("初始数据导入完成(10条用户数据、3条角色数据)");
}
}
启动命令与执行结果:
1
2
# 命令行输入:原始参数 import-data
java -jar app.jar import-data

执行结果

1
2
3
4
=== DataImportRunner 执行数据初始化 ===
开始导入初始数据...
初始数据导入完成(10条用户数据、3条角色数据)
=== DataImportRunner 执行完成 ===

Runner 的执行顺序控制

若项目中存在多个 Runner(如 ConfigInitRunnerDataImportRunner),默认执行顺序由 Spring 扫描 Bean 的顺序决定(不确定)。若需指定执行顺序,可通过以下两种方式实现:

方式一:实现 Ordered 接口

通过 Ordered 接口的 getOrder() 方法指定顺序(数字越小,执行优先级越高):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import org.springframework.core.Ordered;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

@Component
public class FirstRunner implements ApplicationRunner, Ordered {

@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("1. FirstRunner 执行(优先级最高)");
}

// 指定顺序为 1(最小,最先执行)
@Override
public int getOrder() {
return 1;
}
}

@Component
public class SecondRunner implements ApplicationRunner, Ordered {

@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("2. SecondRunner 执行(优先级次之)");
}

// 指定顺序为 2(后于 FirstRunner 执行)
@Override
public int getOrder() {
return 2;
}
}

执行结果

1
2
1. FirstRunner 执行(优先级最高)
2. SecondRunner 执行(优先级次之)

方式二:使用 @Order 注解

通过 @Order 注解直接指定顺序(效果与 Ordered 接口一致,更简洁):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import org.springframework.core.annotation.Order;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

// 顺序 1:最先执行
@Order(1)
@Component
public class HighPriorityRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("高优先级 Runner 执行");
}
}

// 顺序 2:后执行
@Order(2)
@Component
public class LowPriorityRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("低优先级 Runner 执行");
}
}

执行结果

1
2
高优先级 Runner 执行
低优先级 Runner 执行

Runner 的执行时机与底层原理

Runner 的执行时机是在 SpringApplication.run() 方法的最后阶段——afterRefresh() 方法中,具体流程如下:

1. 核心源码拆解(SpringApplication 类)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public ConfigurableApplicationContext run(String... args) {
// ... 省略前面的上下文初始化、自动配置等步骤 ...

try {
// 初始化 ApplicationArguments 对象(封装命令行参数)
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);

// 准备上下文(加载 Bean、初始化 Spring 容器)
prepareContext(context, environment, listeners, applicationArguments, printedBanner);

// 刷新上下文(关键:完成 Bean 的初始化和依赖注入)
refreshContext(context);

// 上下文刷新后执行(Runner 就在这里调用)
afterRefresh(context, applicationArguments);

// ... 省略后续日志、监听器通知等步骤 ...
}
// ... 省略异常处理 ...
}

// afterRefresh 方法:调用 Runner
protected void afterRefresh(ConfigurableApplicationContext context, ApplicationArguments args) {
callRunners(context, args);
}

// 核心方法:收集并执行所有 Runner
private void callRunners(ApplicationContext context, ApplicationArguments args) {
List<Object> runners = new ArrayList<>();
// 1. 收集所有 ApplicationRunner 类型的 Bean
runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
// 2. 收集所有 CommandLineRunner 类型的 Bean
runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
// 3. 排序(按 Ordered 接口或 @Order 注解指定的顺序)
AnnotationAwareOrderComparator.sort(runners);

// 4. 遍历执行所有 Runner
for (Object runner : new LinkedHashSet<>(runners)) {
if (runner instanceof ApplicationRunner) {
// 执行 ApplicationRunner 的 run 方法
callRunner((ApplicationRunner) runner, args);
}
if (runner instanceof CommandLineRunner) {
// 执行 CommandLineRunner 的 run 方法
callRunner((CommandLineRunner) runner, args);
}
}
}

2. 执行时机总结

Runner 的执行时机处于 “Spring 上下文完全初始化后”“应用对外提供服务前”,具体流程为:

  1. SpringApplication.run() 启动 → 初始化环境、加载配置;
  2. 刷新 Spring 上下文(refreshContext)→ 完成所有 Bean 的创建和依赖注入;
  3. 调用 afterRefresh() → 收集所有 ApplicationRunnerCommandLineRunner Bean;
  4. 按顺序执行 Runner 的 run 方法 → 执行初始化逻辑;
  5. Runner 执行完成 → 应用启动完成,开始监听端口(如 Tomcat 启动)。

关键结论:Runner 中可安全使用所有 Spring 管理的 Bean(如 @Autowired 注入的 Service、Repository),因为此时 Bean 已完全初始化。

Runner 实战场景与注意事项

1. 典型应用场景

  • 初始化缓存:启动后加载热点数据到 Redis 或本地缓存;
  • 校验依赖服务:检查数据库、Redis、消息队列等依赖服务是否可用;
  • 加载配置:从配置中心(如 Nacos、Apollo)拉取最新配置并生效;
  • 数据初始化:导入初始数据(如管理员账号、基础字典数据);
  • 注册服务:将应用信息注册到服务发现中心(如 Eureka、Nacos)。

2. 注意事项

(1)避免阻塞操作,防止应用启动超时

Runner 的 run 方法在应用启动主线程中执行,若包含耗时操作(如大文件导入、长时间网络请求),会导致应用启动超时。
解决方案

  • 将耗时操作放入异步线程(如 @Async 标注的方法);

  • 示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    @Component
    public class AsyncInitRunner implements ApplicationRunner {

    @Autowired
    private AsyncService asyncService;

    @Override
    public void run(ApplicationArguments args) throws Exception {
    // 主线程触发异步操作,不阻塞启动
    asyncService.initLargeData();
    }
    }

    @Service
    public class AsyncService {
    // 异步执行耗时操作
    @Async
    public void initLargeData() {
    // 模拟耗时数据导入(10分钟)
    try {
    Thread.sleep(600000);
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    }
    System.out.println("大文件数据导入完成");
    }
    }
(2)异常处理:Runner 执行失败会导致应用启动失败

若 Runner 的 run 方法抛出未捕获异常,Spring Boot 会终止应用启动(因为初始化逻辑失败,应用无法正常提供服务)。
解决方案

  • run 方法中捕获异常,根据业务决定是否终止启动;

  • 示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @Component
    public class SafeInitRunner implements ApplicationRunner {

    @Override
    public void run(ApplicationArguments args) throws Exception {
    try {
    // 可能抛出异常的初始化逻辑(如依赖服务校验)
    checkRedisConnection();
    } catch (Exception e) {
    System.err.println("Redis 连接校验失败:" + e.getMessage());
    // 业务决定:若 Redis 是核心依赖,抛出异常终止启动;否则仅日志告警
    throw new RuntimeException("核心依赖 Redis 不可用,应用启动失败", e);
    }
    }

    private void checkRedisConnection() {
    // 模拟 Redis 连接校验(失败时抛出异常)
    throw new RuntimeException("Redis 服务器未响应");
    }
    }
(3)区分环境:避免生产环境执行测试逻辑

Runner 会在所有环境(dev/test/prod)启动时执行,需通过环境判断避免生产环境执行测试逻辑。
解决方案

  • 注入 Environment 对象,判断当前环境;

  • 示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    @Component
    public class EnvAwareRunner implements ApplicationRunner {

    @Autowired
    private Environment environment;

    @Override
    public void run(ApplicationArguments args) throws Exception {
    // 获取当前激活的环境(如 "dev"、"prod")
    String[] activeProfiles = environment.getActiveProfiles();
    boolean isDevEnv = Arrays.asList(activeProfiles).contains("dev");

    // 仅开发环境执行测试数据初始化
    if (isDevEnv) {
    System.out.println("开发环境:初始化测试数据...");
    initTestData();
    }
    }

    private void initTestData() {
    // 开发环境专属测试数据
    }
    }

总结

ApplicationRunnerCommandLineRunner 是 Spring Boot 启动后初始化逻辑的核心扩展点,关键要点可概括为:

  1. 接口选择:需解析命令行选项参数(如 --env=dev)用 ApplicationRunner;仅需原始参数用 CommandLineRunner
  2. 执行顺序:通过 Ordered 接口或 @Order 注解指定,数字越小优先级越高;
  3. 执行时机:Spring 上下文初始化完成后执行,可安全使用所有 Spring Bean;
  4. 实战注意:避免阻塞操作、处理异常、区分环境,确保应用稳定启动

欢迎关注我的其它发布渠道

表情 | 预览
快来做第一个评论的人吧~
Powered By Valine
v1.3.10