当前位置: 首页 > article >正文

CommandLineRunner详细教程

文章目录

    • 1. CommandLineRunner基础概念和背景
      • 1.1 什么是CommandLineRunner?
        • 1.1.1 核心概念
        • 1.1.2 接口定义
      • 1.2 为什么需要CommandLineRunner?
      • 1.3 CommandLineRunner的特点
        • 1.3.1 执行时机
        • 1.3.2 与ApplicationRunner的区别
    • 2. 环境搭建和项目结构
      • 2.1 Maven项目配置
        • 2.1.1 基础依赖
        • 2.1.2 Gradle项目配置
      • 2.2 项目结构
        • 2.2.1 推荐的目录结构
      • 2.3 基础配置文件
        • 2.3.1 application.yml配置
        • 2.3.2 环境特定配置
    • 3. CommandLineRunner基本用法
      • 3.1 简单实现方式
        • 3.1.1 实现接口方式
        • 3.1.2 Lambda表达式方式
      • 3.2 依赖注入和服务调用
        • 3.2.1 注入Spring服务
        • 3.2.2 使用@Value注入配置
      • 3.3 多个Runner的执行顺序
        • 3.3.1 使用@Order注解控制顺序
    • 4. 实际应用场景详解
      • 4.1 数据库初始化场景
        • 4.1.1 基础数据初始化
        • 4.1.2 数据迁移场景
      • 4.2 缓存预热场景
        • 4.2.1 Redis缓存预热
      • 4.3 外部服务检查场景
        • 4.3.1 第三方服务连接检查
    • 5. 测试CommandLineRunner
      • 5.1 单元测试
        • 5.1.1 基础测试方法
      • 5.2 集成测试
        • 5.2.1 完整应用启动测试
    • 6. 最佳实践
      • 6.1 设计原则
        • 6.1.1 单一职责原则
        • 6.1.2 异常处理策略
      • 6.2 性能优化
        • 6.2.1 异步执行
      • 6.3 配置管理
    • 7. 常见问题和解决方案
      • 7.1 常见错误
        • 7.1.1 依赖注入问题
        • 7.1.2 执行时间过长
      • 7.2 调试技巧
        • 7.2.1 添加详细日志
    • 8. 总结和建议
      • 8.1 CommandLineRunner适用场景
      • 8.2 与其他初始化方式对比
      • 8.3 最佳实践总结
      • 8.4 技术选型建议
      • 8.5 未来发展趋势

1. CommandLineRunner基础概念和背景

1.1 什么是CommandLineRunner?

CommandLineRunner是Spring Boot框架提供的一个功能接口,用于在Spring Boot应用启动完成后立即执行特定的代码逻辑。它允许开发者在应用程序完全启动并准备好接收请求之前执行一些初始化任务。

1.1.1 核心概念
  • 启动时执行:在Spring Boot应用程序完全启动后自动执行
  • 单次执行:每次应用启动时只执行一次
  • 访问命令行参数:可以获取应用启动时的命令行参数
  • 异常处理:如果执行过程中出现异常,会阻止应用正常启动
1.1.2 接口定义
@FunctionalInterface
public interface CommandLineRunner {/*** 应用启动后执行的回调方法* @param args 命令行参数数组* @throws Exception 如果执行过程中出现错误*/void run(String... args) throws Exception;
}

1.2 为什么需要CommandLineRunner?

在实际开发中,我们经常需要在应用启动后执行一些初始化工作:

/*** 常见的应用启动初始化需求*/
public class InitializationNeeds {/*** 1. 数据库初始化* - 创建默认管理员账户* - 初始化基础数据* - 执行数据迁移脚本*/public void databaseInitialization() {// 传统做法的问题:// ❌ 在@PostConstruct中执行 - 可能依赖项还未完全初始化// ❌ 在控制器中执行 - 需要手动调用,不够自动化// ❌ 在main方法中执行 - Spring容器可能还未准备好// ✅ 使用CommandLineRunner的优势:// - Spring容器完全启动完成// - 所有Bean都已初始化// - 数据库连接池已准备就绪}/*** 2. 缓存预热* - 预加载热点数据到Redis* - 初始化本地缓存*/public void cacheWarmup() {// 在应用启动时预加载数据,提升首次访问性能}/*** 3. 定时任务启动* - 启动后台清理任务* - 初始化定时数据同步*/public void scheduleTasksInitialization() {// 启动各种后台任务}/*** 4. 外部服务连接检查* - 验证第三方API连接* - 检查消息队列连接*/public void externalServiceCheck() {// 确保外部依赖服务可用}
}

1.3 CommandLineRunner的特点

1.3.1 执行时机
/*** Spring Boot应用启动流程中CommandLineRunner的位置*/
public class SpringBootStartupFlow {public void startupSequence() {// 1. 创建SpringApplication// 2. 准备Environment// 3. 创建ApplicationContext// 4. 准备ApplicationContext// 5. 刷新ApplicationContext//    - 实例化所有单例Bean//    - 执行@PostConstruct方法//    - 发布ContextRefreshedEvent事件// 6. 调用ApplicationRunner和CommandLineRunner ⬅️ 这里!// 7. 发布ApplicationReadyEvent事件// 8. 应用启动完成,开始接收请求}
}
1.3.2 与ApplicationRunner的区别
/*** CommandLineRunner vs ApplicationRunner*/
public class RunnerComparison {/*** CommandLineRunner接口*/public class MyCommandLineRunner implements CommandLineRunner {@Overridepublic void run(String... args) throws Exception {// 参数:原始字符串数组// 例如:["--server.port=8080", "--spring.profiles.active=dev"]System.out.println("命令行参数:" + Arrays.toString(args));}}/*** ApplicationRunner接口*/public class MyApplicationRunner implements ApplicationRunner {@Overridepublic void run(ApplicationArguments args) throws Exception {// 参数:解析后的ApplicationArguments对象// 提供更方便的参数访问方法System.out.println("选项参数:" + args.getOptionNames());System.out.println("非选项参数:" + args.getNonOptionArgs());}}
}

2. 环境搭建和项目结构

2.1 Maven项目配置

2.1.1 基础依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.example</groupId><artifactId>commandlinerunner-demo</artifactId><version>1.0.0</version><packaging>jar</packaging><name>CommandLineRunner Demo</name><description>CommandLineRunner功能演示项目</description><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.2.0</version><relativePath/></parent><properties><java.version>17</java.version><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><!-- Spring Boot核心依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><!-- Web功能(如果需要) --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- 数据库相关(如果需要) --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><!-- MySQL驱动 --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><!-- Redis支持 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- 测试依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!-- Lombok(简化代码) --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><!-- JSON处理 --><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin></plugins></build>
</project>
2.1.2 Gradle项目配置
plugins {id 'java'id 'org.springframework.boot' version '3.2.0'id 'io.spring.dependency-management' version '1.1.4'
}group = 'com.example'
version = '1.0.0'
sourceCompatibility = '17'configurations {compileOnly {extendsFrom annotationProcessor}
}repositories {mavenCentral()
}dependencies {// Spring Boot核心implementation 'org.springframework.boot:spring-boot-starter'implementation 'org.springframework.boot:spring-boot-starter-web'implementation 'org.springframework.boot:spring-boot-starter-data-jpa'implementation 'org.springframework.boot:spring-boot-starter-data-redis'// 数据库runtimeOnly 'mysql:mysql-connector-java'// 工具类compileOnly 'org.projectlombok:lombok'annotationProcessor 'org.projectlombok:lombok'// 测试testImplementation 'org.springframework.boot:spring-boot-starter-test'
}tasks.named('test') {useJUnitPlatform()
}

2.2 项目结构

2.2.1 推荐的目录结构
src/
├── main/
│   ├── java/
│   │   └── com/
│   │       └── example/
│   │           ├── CommandLineRunnerDemoApplication.java
│   │           ├── runner/                    # CommandLineRunner实现
│   │           │   ├── DatabaseInitRunner.java
│   │           │   ├── CacheWarmupRunner.java
│   │           │   ├── SystemCheckRunner.java
│   │           │   └── DataMigrationRunner.java
│   │           ├── config/                    # 配置类
│   │           │   ├── DatabaseConfig.java
│   │           │   ├── RedisConfig.java
│   │           │   └── RunnerConfig.java
│   │           ├── service/                   # 业务服务
│   │           │   ├── UserService.java
│   │           │   ├── DataService.java
│   │           │   └── CacheService.java
│   │           ├── entity/                    # 实体类
│   │           │   ├── User.java
│   │           │   ├── Role.java
│   │           │   └── SystemConfig.java
│   │           ├── repository/                # 数据访问层
│   │           │   ├── UserRepository.java
│   │           │   └── SystemConfigRepository.java
│   │           └── util/                      # 工具类
│   │               ├── CommandLineUtils.java
│   │               └── InitializationUtils.java
│   └── resources/
│       ├── application.yml                    # 主配置文件
│       ├── application-dev.yml               # 开发环境配置
│       ├── application-prod.yml              # 生产环境配置
│       ├── data/                             # 初始化数据
│       │   ├── init-data.sql
│       │   └── sample-data.json
│       └── static/                           # 静态资源
└── test/└── java/└── com/└── example/├── runner/                   # Runner测试│   ├── DatabaseInitRunnerTest.java│   └── CacheWarmupRunnerTest.java└── integration/              # 集成测试└── ApplicationStartupTest.java

2.3 基础配置文件

2.3.1 application.yml配置
# 应用基础配置
spring:application:name: commandlinerunner-demo# 数据源配置datasource:url: jdbc:mysql://localhost:3306/demo_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghaiusername: rootpassword: passworddriver-class-name: com.mysql.cj.jdbc.Driver# JPA配置jpa:hibernate:ddl-auto: updateshow-sql: trueproperties:hibernate:dialect: org.hibernate.dialect.MySQL8Dialectformat_sql: true# Redis配置data:redis:host: localhostport: 6379database: 0timeout: 2000msjedis:pool:max-active: 8max-wait: -1msmax-idle: 8min-idle: 0# 服务器配置
server:port: 8080servlet:context-path: /api# 日志配置
logging:level:com.example: DEBUGorg.springframework: INFOorg.hibernate: INFOpattern:console: '%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n'file: '%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n'file:name: logs/application.log# 自定义配置
app:runner:enabled: truedatabase-init: truecache-warmup: truesystem-check: trueinitialization:admin-username: adminadmin-password: admin123admin-email: admin@example.com
2.3.2 环境特定配置
# application-dev.yml (开发环境)
spring:datasource:url: jdbc:mysql://localhost:3306/demo_dev?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghaijpa:hibernate:ddl-auto: create-dropshow-sql: trueapp:runner:database-init: truecache-warmup: false  # 开发环境跳过缓存预热system-check: false  # 开发环境跳过系统检查logging:level:com.example: DEBUG---
# application-prod.yml (生产环境)
spring:datasource:url: jdbc:mysql://prod-db:3306/demo_prod?useUnicode=true&characterEncoding=utf8&useSSL=true&serverTimezone=Asia/Shanghaijpa:hibernate:ddl-auto: validateshow-sql: falseapp:runner:database-init: false  # 生产环境通常不自动初始化cache-warmup: truesystem-check: truelogging:level:com.example: INFOroot: WARN

3. CommandLineRunner基本用法

3.1 简单实现方式

3.1.1 实现接口方式
package com.example.runner;import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;/*** 基础CommandLineRunner实现示例*/
@Slf4j
@Component
public class BasicCommandLineRunner implements CommandLineRunner {@Overridepublic void run(String... args) throws Exception {log.info("=== BasicCommandLineRunner 开始执行 ===");// 1. 打印启动信息log.info("应用程序启动完成,开始执行初始化任务");// 2. 处理命令行参数if (args.length > 0) {log.info("接收到命令行参数:");for (int i = 0; i < args.length; i++) {log.info("  参数[{}]: {}", i, args[i]);}} else {log.info("没有接收到命令行参数");}// 3. 执行简单的初始化逻辑performBasicInitialization();log.info("=== BasicCommandLineRunner 执行完成 ===");}private void performBasicInitialization() {try {// 模拟一些初始化工作log.info("正在执行基础初始化...");Thread.sleep(1000); // 模拟耗时操作log.info("基础初始化完成");} catch (InterruptedException e) {log.error("初始化过程被中断", e);Thread.currentThread().interrupt();}}
}
3.1.2 Lambda表达式方式
package com.example.config;import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** 使用@Bean和Lambda表达式创建CommandLineRunner*/
@Slf4j
@Configuration
public class RunnerConfig {/*** 简单的Lambda方式*/@Beanpublic CommandLineRunner simpleRunner() {return args -> {log.info("=== Lambda CommandLineRunner 执行 ===");log.info("这是通过Lambda表达式创建的CommandLineRunner");// 执行简单任务printWelcomeMessage();};}/*** 带参数处理的Lambda方式*/@Beanpublic CommandLineRunner parameterProcessorRunner() {return args -> {log.info("=== 参数处理 CommandLineRunner ===");// 解析和处理命令行参数processCommandLineArguments(args);};}/*** 条件执行的Runner*/@Beanpublic CommandLineRunner conditionalRunner() {return args -> {// 根据参数决定是否执行if (shouldExecuteConditionalLogic(args)) {log.info("=== 条件 CommandLineRunner 执行 ===");executeConditionalLogic();} else {log.info("跳过条件执行逻辑");}};}private void printWelcomeMessage() {log.info("欢迎使用CommandLineRunner演示应用!");log.info("应用已经启动并准备就绪");}private void processCommandLineArguments(String[] args) {log.info("处理命令行参数,共{}个参数", args.length);for (String arg : args) {if (arg.startsWith("--")) {// 处理选项参数handleOptionArgument(arg);} else {// 处理普通参数handleNormalArgument(arg);}}}private void handleOptionArgument(String arg) {log.info("处理选项参数: {}", arg);if (arg.contains("=")) {String[] parts = arg.substring(2).split("=", 2);String key = parts[0];String value = parts.length > 1 ? parts[1] : "";log.info("选项: {} = {}", key, value);} else {log.info("布尔选项: {}", arg.substring(2));}}private void handleNormalArgument(String arg) {log.info("处理普通参数: {}", arg);}private boolean shouldExecuteConditionalLogic(String[] args) {// 检查是否有特定参数for (String arg : args) {if ("--skip-conditional".equals(arg)) {return false;}}return true;}private void executeConditionalLogic() {log.info("执行条件逻辑...");// 执行一些条件性的初始化工作}
}

3.2 依赖注入和服务调用

3.2.1 注入Spring服务
package com.example.runner;import com.example.service.UserService;
import com.example.service.DataService;
import com.example.service.CacheService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;/*** 演示依赖注入的CommandLineRunner*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ServiceAwareRunner implements CommandLineRunner {// 通过构造函数注入依赖服务private final UserService userService;private final DataService dataService;private final CacheService cacheService;@Overridepublic void run(String... args) throws Exception {log.info("=== ServiceAwareRunner 开始执行 ===");try {// 1. 用户服务初始化initializeUserService();// 2. 数据服务初始化initializeDataService();// 3. 缓存服务初始化initializeCacheService();log.info("所有服务初始化完成");} catch (Exception e) {log.error("服务初始化过程中出现错误", e);throw e; // 重新抛出异常,阻止应用启动}log.info("=== ServiceAwareRunner 执行完成 ===");}private void initializeUserService() {log.info("初始化用户服务...");// 检查是否存在管理员用户if (!userService.existsAdminUser()) {log.info("创建默认管理员用户");userService.createDefaultAdminUser();} else {log.info("管理员用户已存在");}// 获取用户统计信息long userCount = userService.getUserCount();log.info("当前系统用户数量: {}", userCount);}private void initializeDataService() {log.info("初始化数据服务...");// 检查数据库连接if (dataService.isDatabaseConnected()) {log.info("数据库连接正常");// 执行数据迁移(如果需要)if (dataService.needsMigration()) {log.info("执行数据迁移...");dataService.performMigration();log.info("数据迁移完成");}} else {log.error("数据库连接失败");throw new RuntimeException("无法连接到数据库");}}private void initializeCacheService() {log.info("初始化缓存服务...");// 检查Redis连接if (cacheService.isRedisConnected()) {log.info("Redis连接正常");// 清理过期缓存cacheService.clearExpiredCache();// 预热重要缓存cacheService.preloadImportantData();} else {log.warn("Redis连接失败,缓存功能将不可用");// 注意:这里只是警告,不阻止应用启动}}
}
3.2.2 使用@Value注入配置
package com.example.runner;import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;/*** 演示配置注入的CommandLineRunner*/
@Slf4j
@Component
public class ConfigAwareRunner implements CommandLineRunner {// 注入应用配置@Value("${spring.application.name}")private String applicationName;@Value("${server.port:8080}")private int serverPort;@Value("${app.runner.enabled:true}")private boolean runnerEnabled;@Value("${app.initialization.admin-username:admin}")private String adminUsername;@Value("${app.initialization.admin-password:}")private String adminPassword;@Value("${app.initialization.admin-email:admin@example.com}")private String adminEmail;// 注入环境变量@Value("${JAVA_HOME:#{null}}")private String javaHome;@Overridepublic void run(String... args) throws Exception {if (!runnerEnabled) {log.info("ConfigAwareRunner已禁用,跳过执行");return;}log.info("=== ConfigAwareRunner 开始执行 ===");// 打印应用配置信息printApplicationInfo();// 打印初始化配置printInitializationConfig();// 打印环境信息printEnvironmentInfo();log.info("=== ConfigAwareRunner 执行完成 ===");}private void printApplicationInfo() {log.info("应用信息:");log.info("  应用名称: {}", applicationName);log.info("  服务端口: {}", serverPort);log.info("  Runner启用状态: {}", runnerEnabled);}private void printInitializationConfig() {log.info("初始化配置:");log.info("  管理员用户名: {}", adminUsername);log.info("  管理员密码: {}", maskPassword(adminPassword));log.info("  管理员邮箱: {}", adminEmail);}private void printEnvironmentInfo() {log.info("环境信息:");log.info("  Java Home: {}", javaHome != null ? javaHome : "未设置");log.info("  工作目录: {}", System.getProperty("user.dir"));log.info("  Java版本: {}", System.getProperty("java.version"));log.info("  操作系统: {} {}", System.getProperty("os.name"), System.getProperty("os.version"));}private String maskPassword(String password) {if (password == null || password.isEmpty()) {return "未设置";}return "*".repeat(password.length());}
}

3.3 多个Runner的执行顺序

3.3.1 使用@Order注解控制顺序
package com.example.runner;import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;/*** 第一个执行的Runner - 系统检查*/
@Slf4j
@Component
@Order(1)
public class SystemCheckRunner implements CommandLineRunner {@Overridepublic void run(String... args) throws Exception {log.info("=== [ORDER 1] SystemCheckRunner 开始执行 ===");// 执行系统检查performSystemCheck();log.info("=== [ORDER 1] SystemCheckRunner 执行完成 ===");}private void performSystemCheck() {log.info("执行系统健康检查...");// 检查磁盘空间checkDiskSpace();// 检查内存使用checkMemoryUsage();// 检查网络连接checkNetworkConnectivity();}private void checkDiskSpace() {long freeSpace = new java.io.File("/").getFreeSpace();long totalSpace = new java.io.File("/").getTotalSpace();double usagePercent = ((double) (totalSpace - freeSpace) / totalSpace) * 100;log.info("磁盘使用率: {:.2f}%", usagePercent);if (usagePercent > 90) {log.warn("磁盘空间不足,使用率超过90%");}}private void checkMemoryUsage() {Runtime runtime = Runtime.getRuntime();long maxMemory = runtime.maxMemory();long totalMemory = runtime.totalMemory();long freeMemory = runtime.freeMemory();long usedMemory = totalMemory - freeMemory;log.info("内存使用情况:");log.info("  最大内存: {} MB", maxMemory / 1024 / 1024);log.info("  已分配内存: {} MB", totalMemory / 1024 / 1024);log.info("  已使用内存: {} MB", usedMemory / 1024 / 1024);log.info("  空闲内存: {} MB", freeMemory / 1024 / 1024);}private void checkNetworkConnectivity() {// 简单的网络连接检查try {java.net.InetAddress.getByName("www.google.com").isReachable(5000);log.info("网络连接正常");} catch (Exception e) {log.warn("网络连接检查失败: {}", e.getMessage());}}
}/*** 第二个执行的Runner - 数据库初始化*/
@Slf4j
@Component
@Order(2)
public class DatabaseInitRunner implements CommandLineRunner {@Overridepublic void run(String... args) throws Exception {log.info("=== [ORDER 2] DatabaseInitRunner 开始执行 ===");// 数据库初始化逻辑initializeDatabase();log.info("=== [ORDER 2] DatabaseInitRunner 执行完成 ===");}private void initializeDatabase() {log.info("初始化数据库...");// 创建基础数据表(如果不存在)createBaseTables();// 插入初始数据insertInitialData();// 创建索引createIndexes();}private void createBaseTables() {log.info("检查并创建基础数据表...");// 实际的表创建逻辑}private void insertInitialData() {log.info("插入初始数据...");// 实际的数据插入逻辑}private void createIndexes() {log.info("创建数据库索引...");// 实际的索引创建逻辑}
}/*** 第三个执行的Runner - 缓存预热*/
@Slf4j
@Component
@Order(3)
public class CacheWarmupRunner implements CommandLineRunner {@Overridepublic void run(String... args) throws Exception {log.info("=== [ORDER 3] CacheWarmupRunner 开始执行 ===");// 缓存预热逻辑warmupCache();log.info("=== [ORDER 3] CacheWarmupRunner 执行完成 ===");}private void warmupCache() {log.info("开始缓存预热...");// 预热用户数据缓存warmupUserCache();// 预热配置数据缓存warmupConfigCache();// 预热统计数据缓存warmupStatsCache();}private void warmupUserCache() {log.info("预热用户数据缓存...");// 实际的用户缓存预热逻辑}private void warmupConfigCache() {log.info("预热配置数据缓存...");// 实际的配置缓存预热逻辑}private void warmupStatsCache() {log.info("预热统计数据缓存...");// 实际的统计缓存预热逻辑}
}

4. 实际应用场景详解

4.1 数据库初始化场景

4.1.1 基础数据初始化
package com.example.runner;import com.example.entity.User;
import com.example.entity.Role;
import com.example.entity.SystemConfig;
import com.example.repository.UserRepository;
import com.example.repository.RoleRepository;
import com.example.repository.SystemConfigRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;/*** 数据库基础数据初始化Runner*/
@Slf4j
@Component
@Order(10) // 确保在系统检查之后执行
@RequiredArgsConstructor
public class DatabaseInitializationRunner implements CommandLineRunner {private final UserRepository userRepository;private final RoleRepository roleRepository;private final SystemConfigRepository systemConfigRepository;private final PasswordEncoder passwordEncoder;@Value("${app.initialization.admin-username:admin}")private String adminUsername;@Value("${app.initialization.admin-password:admin123}")private String adminPassword;@Value("${app.initialization.admin-email:admin@example.com}")private String adminEmail;@Value("${app.runner.database-init:true}")private boolean enableDatabaseInit;@Overridepublic void run(String... args) throws Exception {if (!enableDatabaseInit) {log.info("数据库初始化已禁用,跳过执行");return;}log.info("=== 数据库初始化开始 ===");try {// 1. 初始化角色数据initializeRoles();// 2. 初始化管理员用户initializeAdminUser();// 3. 初始化系统配置initializeSystemConfigs();// 4. 执行数据验证validateInitializedData();log.info("数据库初始化完成");} catch (Exception e) {log.error("数据库初始化失败", e);throw new RuntimeException("数据库初始化失败", e);}log.info("=== 数据库初始化结束 ===");}@Transactionalprivate void initializeRoles() {log.info("初始化角色数据...");// 定义基础角色String[][] baseRoles = {{"ADMIN", "系统管理员", "拥有系统所有权限"},{"USER", "普通用户", "基础用户权限"},{"MODERATOR", "版主", "内容管理权限"},{"VIEWER", "访客", "只读权限"}};for (String[] roleData : baseRoles) {String roleName = roleData[0];String displayName = roleData[1];String description = roleData[2];if (!roleRepository.existsByName(roleName)) {Role role = new Role();role.setName(roleName);role.setDisplayName(displayName);role.setDescription(description);role.setCreatedAt(java.time.LocalDateTime.now());roleRepository.save(role);log.info("创建角色: {} - {}", roleName, displayName);} else {log.debug("角色已存在: {}", roleName);}}log.info("角色数据初始化完成");}@Transactionalprivate void initializeAdminUser() {log.info("初始化管理员用户...");// 检查管理员用户是否存在if (!userRepository.existsByUsername(adminUsername)) {// 获取管理员角色Role adminRole = roleRepository.findByName("ADMIN").orElseThrow(() -> new RuntimeException("管理员角色不存在"));// 创建管理员用户User adminUser = new User();adminUser.setUsername(adminUsername);adminUser.setEmail(adminEmail);adminUser.setPassword(passwordEncoder.encode(adminPassword));adminUser.setEnabled(true);adminUser.setAccountNonExpired(true);adminUser.setAccountNonLocked(true);adminUser.setCredentialsNonExpired(true);adminUser.setCreatedAt(java.time.LocalDateTime.now());adminUser.getRoles().add(adminRole);userRepository.save(adminUser);log.info("创建管理员用户: {} ({})", adminUsername, adminEmail);// 安全起见,不在日志中显示密码log.info("管理员用户创建完成,请及时修改默认密码");} else {log.info("管理员用户已存在: {}", adminUsername);}}@Transactionalprivate void initializeSystemConfigs() {log.info("初始化系统配置...");// 定义系统配置项String[][] configs = {{"system.name", "系统名称", "CommandLineRunner演示系统"},{"system.version", "系统版本", "1.0.0"},{"system.maintenance", "维护模式", "false"},{"user.registration.enabled", "用户注册开关", "true"},{"user.email.verification.required", "邮箱验证要求", "true"},{"cache.expiry.user", "用户缓存过期时间(秒)", "3600"},{"cache.expiry.config", "配置缓存过期时间(秒)", "1800"},{"file.upload.max-size", "文件上传最大大小(MB)", "10"},{"session.timeout", "会话超时时间(分钟)", "30"}};for (String[] configData : configs) {String key = configData[0];String description = configData[1];String defaultValue = configData[2];if (!systemConfigRepository.existsByConfigKey(key)) {SystemConfig config = new SystemConfig();config.setConfigKey(key);config.setConfigValue(defaultValue);config.setDescription(description);config.setCreatedAt(java.time.LocalDateTime.now());config.setUpdatedAt(java.time.LocalDateTime.now());systemConfigRepository.save(config);log.debug("创建系统配置: {} = {}", key, defaultValue);}}log.info("系统配置初始化完成");}private void validateInitializedData() {log.info("验证初始化数据...");// 验证角色数据long roleCount = roleRepository.count();log.info("系统角色数量: {}", roleCount);// 验证用户数据long userCount = userRepository.count();log.info("系统用户数量: {}", userCount);// 验证管理员用户boolean adminExists = userRepository.existsByUsername(adminUsername);log.info("管理员用户存在: {}", adminExists);// 验证系统配置long configCount = systemConfigRepository.count();log.info("系统配置项数量: {}", configCount);// 检查关键配置validateEssentialConfigs();log.info("数据验证完成");}private void validateEssentialConfigs() {String[] essentialKeys = {"system.name","system.version", "user.registration.enabled"};for (String key : essentialKeys) {boolean exists = systemConfigRepository.existsByConfigKey(key);if (!exists) {log.error("关键配置项缺失: {}", key);throw new RuntimeException("关键配置项缺失: " + key);}}log.debug("关键配置项验证通过");}
}
4.1.2 数据迁移场景
package com.example.runner;import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.ClassPathResource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.FileCopyUtils;import javax.sql.DataSource;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;/*** 数据库迁移Runner*/
@Slf4j
@Component
@Order(5) // 在数据库初始化之前执行
@RequiredArgsConstructor
public class DataMigrationRunner implements CommandLineRunner {private final DataSource dataSource;private final JdbcTemplate jdbcTemplate;@Overridepublic void run(String... args) throws Exception {log.info("=== 数据库迁移开始 ===");try {// 1. 检查数据库版本String currentVersion = getCurrentDatabaseVersion();log.info("当前数据库版本: {}", currentVersion);// 2. 执行迁移脚本executemigrations(currentVersion);// 3. 更新版本信息updateDatabaseVersion();log.info("数据库迁移完成");} catch (Exception e) {log.error("数据库迁移失败", e);throw e;}log.info("=== 数据库迁移结束 ===");}private String getCurrentDatabaseVersion() {try {// 检查版本表是否存在if (!tableExists("schema_version")) {log.info("版本表不存在,创建版本表");createVersionTable();return "0.0.0";}// 查询当前版本String version = jdbcTemplate.queryForObject("SELECT version FROM schema_version ORDER BY applied_at DESC LIMIT 1",String.class);return version != null ? version : "0.0.0";} catch (Exception e) {log.warn("获取数据库版本失败,假设为初始版本", e);return "0.0.0";}}private boolean tableExists(String tableName) {try (Connection connection = dataSource.getConnection()) {DatabaseMetaData metaData = connection.getMetaData();ResultSet tables = metaData.getTables(null, null, tableName.toUpperCase(), null);return tables.next();} catch (Exception e) {log.error("检查表存在性失败: {}", tableName, e);return false;}}private void createVersionTable() {String sql = """CREATE TABLE schema_version (id BIGINT AUTO_INCREMENT PRIMARY KEY,version VARCHAR(20) NOT NULL,description VARCHAR(255),script_name VARCHAR(100),applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,INDEX idx_version (version),INDEX idx_applied_at (applied_at))""";jdbcTemplate.execute(sql);log.info("版本表创建完成");}private void executemigrations(String currentVersion) {// 定义迁移脚本Migration[] migrations = {new Migration("1.0.0", "初始数据库结构", "V1_0_0__initial_schema.sql"),new Migration("1.1.0", "添加用户扩展信息表", "V1_1_0__add_user_profile.sql"),new Migration("1.2.0", "添加日志记录表", "V1_2_0__add_audit_log.sql"),new Migration("1.3.0", "优化索引结构", "V1_3_0__optimize_indexes.sql")};for (Migration migration : migrations) {if (shouldExecuteMigration(currentVersion, migration.getVersion())) {executeMigration(migration);}}}private boolean shouldExecuteMigration(String currentVersion, String migrationVersion) {// 简单的版本比较逻辑return compareVersions(migrationVersion, currentVersion) > 0;}private int compareVersions(String version1, String version2) {String[] v1Parts = version1.split("\\.");String[] v2Parts = version2.split("\\.");int maxLength = Math.max(v1Parts.length, v2Parts.length);for (int i = 0; i < maxLength; i++) {int v1Part = i < v1Parts.length ? Integer.parseInt(v1Parts[i]) : 0;int v2Part = i < v2Parts.length ? Integer.parseInt(v2Parts[i]) : 0;if (v1Part != v2Part) {return Integer.compare(v1Part, v2Part);}}return 0;}private void executeMigration(Migration migration) {log.info("执行迁移: {} - {}", migration.getVersion(), migration.getDescription());try {// 读取迁移脚本String script = loadMigrationScript(migration.getScriptName());// 执行脚本String[] statements = script.split(";");for (String statement : statements) {statement = statement.trim();if (!statement.isEmpty()) {jdbcTemplate.execute(statement);}}// 记录迁移历史recordMigration(migration);log.info("迁移完成: {}", migration.getVersion());} catch (Exception e) {log.error("迁移失败: {}", migration.getVersion(), e);throw new RuntimeException("迁移失败: " + migration.getVersion(), e);}}private String loadMigrationScript(String scriptName) throws IOException {ClassPathResource resource = new ClassPathResource("db/migration/" + scriptName);if (!resource.exists()) {throw new RuntimeException("迁移脚本不存在: " + scriptName);}byte[] bytes = FileCopyUtils.copyToByteArray(resource.getInputStream());return new String(bytes, StandardCharsets.UTF_8);}private void recordMigration(Migration migration) {jdbcTemplate.update("INSERT INTO schema_version (version, description, script_name) VALUES (?, ?, ?)",migration.getVersion(),migration.getDescription(),migration.getScriptName());}private void updateDatabaseVersion() {// 获取最新版本String latestVersion = jdbcTemplate.queryForObject("SELECT version FROM schema_version ORDER BY applied_at DESC LIMIT 1",String.class);log.info("数据库版本已更新至: {}", latestVersion);}/*** 迁移信息类*/private static class Migration {private final String version;private final String description;private final String scriptName;public Migration(String version, String description, String scriptName) {this.version = version;this.description = description;this.scriptName = scriptName;}public String getVersion() { return version; }public String getDescription() { return description; }public String getScriptName() { return scriptName; }}
}

4.2 缓存预热场景

4.2.1 Redis缓存预热
package com.example.runner;import com.example.service.CacheService;
import com.example.service.UserService;
import com.example.service.ConfigService;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;/*** Redis缓存预热Runner*/
@Slf4j
@Component
@Order(20) // 在数据库初始化后执行
@RequiredArgsConstructor
public class CacheWarmupRunner implements CommandLineRunner {private final RedisTemplate<String, Object> redisTemplate;private final UserService userService;private final ConfigService configService;private final CacheService cacheService;private final ObjectMapper objectMapper;// 用于异步预热的线程池private final Executor warmupExecutor = Executors.newFixedThreadPool(5);@Overridepublic void run(String... args) throws Exception {log.info("=== 缓存预热开始 ===");try {// 1. 检查Redis连接if (!checkRedisConnection()) {log.warn("Redis连接失败,跳过缓存预热");return;}// 2. 清理过期缓存cleanupExpiredCache();// 3. 并行预热各种缓存CompletableFuture<Void> userCacheWarmup = warmupUserCache();CompletableFuture<Void> configCacheWarmup = warmupConfigCache();CompletableFuture<Void> staticDataWarmup = warmupStaticData();// 4. 等待所有预热任务完成CompletableFuture.allOf(userCacheWarmup, configCacheWarmup, staticDataWarmup).get(); // 等待完成// 5. 验证缓存预热结果validateWarmupResults();log.info("缓存预热完成");} catch (Exception e) {log.error("缓存预热失败", e);// 缓存预热失败不应该阻止应用启动log.warn("缓存预热失败,应用将在没有缓存的情况下启动");}log.info("=== 缓存预热结束 ===");}private boolean checkRedisConnection() {try {redisTemplate.getConnectionFactory().getConnection().ping();log.info("Redis连接正常");return true;} catch (Exception e) {log.error("Redis连接检查失败", e);return false;}}private void cleanupExpiredCache() {log.info("清理过期缓存...");try {// 获取所有缓存键var keys = redisTemplate.keys("cache:*");if (keys != null && !keys.isEmpty()) {log.info("发现{}个缓存键", keys.size());// 检查并删除过期键int expiredCount = 0;for (String key : keys) {Long expire = redisTemplate.getExpire(key);if (expire != null && expire == -2) { // -2表示键不存在或已过期redisTemplate.delete(key);expiredCount++;}}log.info("清理了{}个过期缓存", expiredCount);}} catch (Exception e) {log.warn("清理过期缓存失败", e);}}private CompletableFuture<Void> warmupUserCache() {return CompletableFuture.runAsync(() -> {log.info("开始预热用户缓存...");try {// 1. 预热活跃用户数据List<Long> activeUserIds = userService.getActiveUserIds();log.info("预热{}个活跃用户缓存", activeUserIds.size());for (Long userId : activeUserIds) {try {var user = userService.getUserById(userId);if (user != null) {String cacheKey = "cache:user:" + userId;redisTemplate.opsForValue().set(cacheKey, user, Duration.ofHours(1));}} catch (Exception e) {log.warn("预热用户缓存失败: userId={}", userId, e);}}// 2. 预热用户统计数据warmupUserStatistics();log.info("用户缓存预热完成");} catch (Exception e) {log.error("用户缓存预热失败", e);}}, warmupExecutor);}private void warmupUserStatistics() {try {// 预热用户统计信息Map<String, Object> userStats = userService.getUserStatistics();redisTemplate.opsForValue().set("cache:user:statistics", userStats, Duration.ofMinutes(30));// 预热在线用户数量Long onlineUserCount = userService.getOnlineUserCount();redisTemplate.opsForValue().set("cache:user:online-count", onlineUserCount, Duration.ofMinutes(5));log.debug("用户统计缓存预热完成");} catch (Exception e) {log.warn("用户统计缓存预热失败", e);}}private CompletableFuture<Void> warmupConfigCache() {return CompletableFuture.runAsync(() -> {log.info("开始预热配置缓存...");try {// 1. 预热系统配置Map<String, String> systemConfigs = configService.getAllSystemConfigs();redisTemplate.opsForValue().set("cache:config:system", systemConfigs, Duration.ofHours(2));// 2. 预热应用配置Map<String, Object> appConfigs = configService.getApplicationConfigs();redisTemplate.opsForValue().set("cache:config:application", appConfigs, Duration.ofHours(1));// 3. 预热特性开关配置Map<String, Boolean> featureFlags = configService.getFeatureFlags();redisTemplate.opsForValue().set("cache:config:features", featureFlags, Duration.ofMinutes(30));log.info("配置缓存预热完成");} catch (Exception e) {log.error("配置缓存预热失败", e);}}, warmupExecutor);}private CompletableFuture<Void> warmupStaticData() {return CompletableFuture.runAsync(() -> {log.info("开始预热静态数据缓存...");try {// 1. 预热地区数据warmupRegionData();// 2. 预热字典数据warmupDictionaryData();// 3. 预热菜单数据warmupMenuData();log.info("静态数据缓存预热完成");} catch (Exception e) {log.error("静态数据缓存预热失败", e);}}, warmupExecutor);}private void warmupRegionData() {try {// 预热省市区数据var regions = configService.getAllRegions();redisTemplate.opsForValue().set("cache:static:regions", regions, Duration.ofDays(1) // 地区数据变化较少,缓存1天);log.debug("地区数据缓存预热完成");} catch (Exception e) {log.warn("地区数据缓存预热失败", e);}}private void warmupDictionaryData() {try {// 预热数据字典var dictionaries = configService.getAllDictionaries();for (Map.Entry<String, Object> entry : dictionaries.entrySet()) {String cacheKey = "cache:dict:" + entry.getKey();redisTemplate.opsForValue().set(cacheKey, entry.getValue(), Duration.ofHours(4));}log.debug("字典数据缓存预热完成,预热{}项", dictionaries.size());} catch (Exception e) {log.warn("字典数据缓存预热失败", e);}}private void warmupMenuData() {try {// 预热菜单数据var menus = configService.getSystemMenus();redisTemplate.opsForValue().set("cache:static:menus", menus, Duration.ofHours(2));log.debug("菜单数据缓存预热完成");} catch (Exception e) {log.warn("菜单数据缓存预热失败", e);}}private void validateWarmupResults() {log.info("验证缓存预热结果...");int successCount = 0;int totalCount = 0;// 检查关键缓存是否存在String[] keysToCheck = {"cache:config:system","cache:config:application","cache:user:statistics","cache:static:regions"};for (String key : keysToCheck) {totalCount++;if (Boolean.TRUE.equals(redisTemplate.hasKey(key))) {successCount++;log.debug("缓存键存在: {}", key);} else {log.warn("缓存键不存在: {}", key);}}double successRate = (double) successCount / totalCount * 100;log.info("缓存预热成功率: {:.1f}% ({}/{})", successRate, successCount, totalCount);if (successRate < 50) {log.warn("缓存预热成功率过低,可能影响应用性能");}}
}

4.3 外部服务检查场景

4.3.1 第三方服务连接检查
package com.example.runner;import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.client.ResourceAccessException;import javax.sql.DataSource;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.sql.Connection;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;/*** 外部服务连接检查Runner*/
@Slf4j
@Component
@Order(1) // 最先执行,确保基础服务可用
@RequiredArgsConstructor
public class ExternalServiceCheckRunner implements CommandLineRunner {private final DataSource dataSource;private final RestTemplate restTemplate;@Value("${app.external-services.payment-api.url:}")private String paymentApiUrl;@Value("${app.external-services.email-service.url:}")private String emailServiceUrl;@Value("${app.external-services.redis.host:localhost}")private String redisHost;@Value("${app.external-services.redis.port:6379}")private int redisPort;@Value("${app.runner.system-check:true}")private boolean enableSystemCheck;@Overridepublic void run(String... args) throws Exception {if (!enableSystemCheck) {log.info("系统检查已禁用,跳过执行");return;}log.info("=== 外部服务连接检查开始 ===");List<ServiceCheckResult> results = new ArrayList<>();try {// 1. 数据库连接检查results.add(checkDatabaseConnection());// 2. Redis连接检查results.add(checkRedisConnection());// 3. 第三方API检查results.addAll(checkExternalAPIs());// 4. 分析检查结果analyzeCheckResults(results);} catch (Exception e) {log.error("服务检查过程中出现异常", e);throw e;}log.info("=== 外部服务连接检查结束 ===");}private ServiceCheckResult checkDatabaseConnection() {log.info("检查数据库连接...");ServiceCheckResult result = new ServiceCheckResult("数据库", "Database");LocalDateTime startTime = LocalDateTime.now();try {// 尝试获取数据库连接try (Connection connection = dataSource.getConnection()) {if (connection.isValid(5)) {Duration responseTime = Duration.between(startTime, LocalDateTime.now());result.setSuccess(true);result.setResponseTime(responseTime);result.setMessage("数据库连接正常");// 获取数据库信息String dbUrl = connection.getMetaData().getURL();String dbProduct = connection.getMetaData().getDatabaseProductName();String dbVersion = connection.getMetaData().getDatabaseProductVersion();result.setDetails(String.format("URL: %s, 产品: %s, 版本: %s", dbUrl, dbProduct, dbVersion));log.info("数据库连接成功 - {} ({}ms)", dbProduct, responseTime.toMillis());} else {result.setSuccess(false);result.setMessage("数据库连接无效");log.error("数据库连接无效");}}} catch (Exception e) {result.setSuccess(false);result.setMessage("数据库连接失败: " + e.getMessage());result.setError(e);log.error("数据库连接失败", e);}return result;}private ServiceCheckResult checkRedisConnection() {log.info("检查Redis连接...");ServiceCheckResult result = new ServiceCheckResult("Redis缓存", "Redis");LocalDateTime startTime = LocalDateTime.now();try {// 使用Socket测试Redis连接try (Socket socket = new Socket()) {socket.connect(new InetSocketAddress(redisHost, redisPort), 5000);Duration responseTime = Duration.between(startTime, LocalDateTime.now());result.setSuccess(true);result.setResponseTime(responseTime);result.setMessage("Redis连接正常");result.setDetails(String.format("主机: %s, 端口: %d", redisHost, redisPort));log.info("Redis连接成功 - {}:{} ({}ms)", redisHost, redisPort, responseTime.toMillis());}} catch (Exception e) {result.setSuccess(false);result.setMessage("Redis连接失败: " + e.getMessage());result.setError(e);log.error("Redis连接失败", e);}return result;}private List<ServiceCheckResult> checkExternalAPIs() {log.info("检查外部API服务...");List<ServiceCheckResult> results = new ArrayList<>();// 并行检查多个API服务CompletableFuture<ServiceCheckResult> paymentCheck = checkApiService("支付服务", "PaymentAPI", paymentApiUrl + "/health");CompletableFuture<ServiceCheckResult> emailCheck = checkApiService("邮件服务", "EmailService", emailServiceUrl + "/status");try {// 等待所有检查完成,设置超时时间results.add(paymentCheck.get(10, TimeUnit.SECONDS));results.add(emailCheck.get(10, TimeUnit.SECONDS));} catch (Exception e) {log.error("API服务检查超时或失败", e);}return results;}private CompletableFuture<ServiceCheckResult> checkApiService(String serviceName, String serviceType, String url) {return CompletableFuture.supplyAsync(() -> {if (url == null || url.isEmpty()) {ServiceCheckResult result = new ServiceCheckResult(serviceName, serviceType);result.setSuccess(false);result.setMessage("服务URL未配置");log.warn("{} URL未配置,跳过检查", serviceName);return result;}log.debug("检查{}服务: {}", serviceName, url);ServiceCheckResult result = new ServiceCheckResult(serviceName, serviceType);LocalDateTime startTime = LocalDateTime.now();try {ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);Duration responseTime = Duration.between(startTime, LocalDateTime.now());if (response.getStatusCode() == HttpStatus.OK) {result.setSuccess(true);result.setResponseTime(responseTime);result.setMessage("服务响应正常");result.setDetails(String.format("状态码: %s, 响应时间: %dms", response.getStatusCode(), responseTime.toMillis()));log.info("{}服务连接成功 ({}ms)", serviceName, responseTime.toMillis());} else {result.setSuccess(false);result.setMessage("服务响应异常: " + response.getStatusCode());log.warn("{}服务响应异常: {}", serviceName, response.getStatusCode());}} catch (ResourceAccessException e) {result.setSuccess(false);result.setMessage("服务连接超时或拒绝: " + e.getMessage());result.setError(e);log.error("{}服务连接失败", serviceName, e);} catch (Exception e) {result.setSuccess(false);result.setMessage("服务检查失败: " + e.getMessage());result.setError(e);log.error("{}服务检查失败", serviceName, e);}return result;});}private void analyzeCheckResults(List<ServiceCheckResult> results) {log.info("=== 服务检查结果分析 ===");int totalServices = results.size();int successfulServices = 0;int criticalFailures = 0;for (ServiceCheckResult result : results) {if (result.isSuccess()) {successfulServices++;log.info("✓ {} - {} ({}ms)", result.getServiceName(), result.getMessage(),result.getResponseTime() != null ? result.getResponseTime().toMillis() : 0);} else {log.error("✗ {} - {}", result.getServiceName(), result.getMessage());// 检查是否为关键服务if (isCriticalService(result.getServiceType())) {criticalFailures++;}}}double successRate = (double) successfulServices / totalServices * 100;log.info("服务检查完成: 成功率 {:.1f}% ({}/{})", successRate, successfulServices, totalServices);// 处理关键服务失败if (criticalFailures > 0) {String errorMessage = String.format("关键服务检查失败,共%d个服务不可用", criticalFailures);log.error(errorMessage);// 根据配置决定是否阻止应用启动boolean failOnCriticalError = true; // 可以通过配置控制if (failOnCriticalError) {throw new RuntimeException(errorMessage);}} else if (successRate < 50) {log.warn("服务可用性较低,应用可能无法正常工作");}}private boolean isCriticalService(String serviceType) {// 定义关键服务类型return "Database".equals(serviceType) || "Redis".equals(serviceType);}/*** 服务检查结果类*/private static class ServiceCheckResult {private final String serviceName;private final String serviceType;private boolean success;private Duration responseTime;private String message;private String details;private Exception error;public ServiceCheckResult(String serviceName, String serviceType) {this.serviceName = serviceName;this.serviceType = serviceType;}// Getters and Setterspublic String getServiceName() { return serviceName; }public String getServiceType() { return serviceType; }public boolean isSuccess() { return success; }public void setSuccess(boolean success) { this.success = success; }public Duration getResponseTime() { return responseTime; }public void setResponseTime(Duration responseTime) { this.responseTime = responseTime; }public String getMessage() { return message; }public void setMessage(String message) { this.message = message; }public String getDetails() { return details; }public void setDetails(String details) { this.details = details; }public Exception getError() { return error; }public void setError(Exception error) { this.error = error; }}
}

5. 测试CommandLineRunner

5.1 单元测试

5.1.1 基础测试方法
package com.example.runner;import com.example.service.UserService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;/*** CommandLineRunner单元测试示例*/
@ExtendWith(MockitoExtension.class)
@TestPropertySource(properties = {"app.runner.database-init=true","app.initialization.admin-username=testadmin"
})
class DatabaseInitializationRunnerTest {@Mockprivate UserService userService;private DatabaseInitializationRunner runner;@BeforeEachvoid setUp() {runner = new DatabaseInitializationRunner(userService);}@Testvoid testRunWithEnabledInitialization() throws Exception {// 模拟管理员用户不存在when(userService.existsAdminUser()).thenReturn(false);// 执行Runnerrunner.run("--spring.profiles.active=test");// 验证是否创建了管理员用户verify(userService, times(1)).createDefaultAdminUser();}@Testvoid testRunWithExistingAdmin() throws Exception {// 模拟管理员用户已存在when(userService.existsAdminUser()).thenReturn(true);// 执行Runnerrunner.run();// 验证没有创建新的管理员用户verify(userService, never()).createDefaultAdminUser();}@Testvoid testRunWithServiceException() {// 模拟服务异常when(userService.existsAdminUser()).thenThrow(new RuntimeException("数据库连接失败"));// 验证异常被正确抛出assertThrows(RuntimeException.class, () -> runner.run());}
}

5.2 集成测试

5.2.1 完整应用启动测试
package com.example.integration;import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.TestPropertySource;/*** 应用启动集成测试*/
@SpringBootTest
@ActiveProfiles("test")
@TestPropertySource(properties = {"app.runner.enabled=true","app.runner.database-init=false", // 测试时禁用数据库初始化"app.runner.cache-warmup=false"   // 测试时禁用缓存预热
})
class ApplicationStartupIntegrationTest {@Testvoid contextLoads() {// 测试应用能够正常启动// Spring Boot会自动执行所有CommandLineRunner}
}

6. 最佳实践

6.1 设计原则

6.1.1 单一职责原则
// ✅ 好的做法:每个Runner负责单一职责
@Component
@Order(1)
public class DatabaseInitRunner implements CommandLineRunner {// 只负责数据库初始化
}@Component  
@Order(2)
public class CacheWarmupRunner implements CommandLineRunner {// 只负责缓存预热
}// ❌ 坏的做法:一个Runner做太多事情
@Component
public class MegaRunner implements CommandLineRunner {public void run(String... args) {initDatabase();     // 数据库初始化warmupCache();      // 缓存预热  checkServices();    // 服务检查sendNotifications(); // 发送通知// ... 更多职责}
}
6.1.2 异常处理策略
@Component
public class RobustRunner implements CommandLineRunner {@Overridepublic void run(String... args) throws Exception {try {// 关键操作:失败应该阻止应用启动performCriticalInitialization();} catch (Exception e) {log.error("关键初始化失败", e);throw e; // 重新抛出异常}try {// 非关键操作:失败不应该阻止应用启动performOptionalInitialization();} catch (Exception e) {log.warn("可选初始化失败,继续启动", e);// 不重新抛出异常}}
}

6.2 性能优化

6.2.1 异步执行
@Component
public class AsyncInitRunner implements CommandLineRunner {@Async("taskExecutor")public CompletableFuture<Void> asyncInitialization() {return CompletableFuture.runAsync(() -> {// 异步执行的初始化逻辑log.info("异步初始化开始");performHeavyInitialization();log.info("异步初始化完成");});}@Overridepublic void run(String... args) throws Exception {// 启动异步任务但不等待完成asyncInitialization();log.info("主初始化完成,异步任务在后台继续执行");}
}

6.3 配置管理

@Component
@ConditionalOnProperty(name = "app.runner.data-init.enabled", havingValue = "true", matchIfMissing = true
)
public class ConditionalRunner implements CommandLineRunner {@Value("${app.runner.data-init.batch-size:1000}")private int batchSize;@Overridepublic void run(String... args) throws Exception {// 根据配置执行初始化}
}

7. 常见问题和解决方案

7.1 常见错误

7.1.1 依赖注入问题
// ❌ 问题:依赖项可能未完全初始化
@Component
public class EarlyRunner implements CommandLineRunner {@Autowiredprivate SomeService someService; // 可能还未准备好@Overridepublic void run(String... args) throws Exception {someService.doSomething(); // 可能失败}
}// ✅ 解决方案:使用构造函数注入和检查
@Component
@RequiredArgsConstructor
public class SafeRunner implements CommandLineRunner {private final SomeService someService;@Overridepublic void run(String... args) throws Exception {if (someService == null) {log.error("SomeService未注入");return;}someService.doSomething();}
}
7.1.2 执行时间过长
// ✅ 解决方案:添加超时控制和进度监控
@Component
public class TimeoutAwareRunner implements CommandLineRunner {@Overridepublic void run(String... args) throws Exception {long startTime = System.currentTimeMillis();long timeoutMs = 60000; // 60秒超时try {CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {performLongRunningTask();});future.get(timeoutMs, TimeUnit.MILLISECONDS);} catch (TimeoutException e) {log.error("初始化超时,耗时超过{}ms", timeoutMs);throw new RuntimeException("初始化超时", e);} finally {long duration = System.currentTimeMillis() - startTime;log.info("初始化耗时: {}ms", duration);}}
}

7.2 调试技巧

7.2.1 添加详细日志
@Component
public class DebuggableRunner implements CommandLineRunner {@Overridepublic void run(String... args) throws Exception {log.info("=== Runner开始执行 ===");log.info("命令行参数: {}", Arrays.toString(args));log.info("当前时间: {}", LocalDateTime.now());log.info("JVM内存信息: 最大{}MB, 已用{}MB", Runtime.getRuntime().maxMemory() / 1024 / 1024,(Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / 1024 / 1024);try {performInitialization();log.info("初始化成功完成");} catch (Exception e) {log.error("初始化失败: {}", e.getMessage(), e);throw e;} finally {log.info("=== Runner执行结束 ===");}}
}

8. 总结和建议

8.1 CommandLineRunner适用场景

✅ 适合使用CommandLineRunner的场景:
- 应用启动后的一次性初始化任务
- 数据库基础数据初始化
- 缓存预热
- 系统健康检查
- 配置验证
- 外部服务连接测试
- 数据迁移任务❌ 不适合使用CommandLineRunner的场景:
- 需要在Bean初始化过程中执行的逻辑(应使用@PostConstruct)
- 定期执行的任务(应使用@Scheduled)
- 请求处理逻辑(应在Controller中处理)
- 复杂的业务流程(应在Service中实现)

8.2 与其他初始化方式对比

初始化方式对比:方式                    | 执行时机              | 适用场景
-----------------------|---------------------|------------------------
@PostConstruct         | Bean初始化后         | 单个Bean的初始化
CommandLineRunner      | 应用完全启动后        | 全局初始化任务
ApplicationRunner      | 应用完全启动后        | 需要解析命令行参数
InitializingBean       | Bean属性设置后        | Bean级别的初始化验证
ApplicationListener    | 特定事件发生时        | 事件驱动的初始化
@EventListener         | 特定事件发生时        | 注解方式的事件监听

8.3 最佳实践总结

  1. 职责分离:每个Runner只负责一个特定的初始化任务
  2. 顺序控制:使用@Order注解明确执行顺序
  3. 异常处理:区分关键和非关键操作的异常处理策略
  4. 配置驱动:通过配置控制Runner的启用和行为
  5. 日志记录:添加详细的执行日志便于调试
  6. 性能考虑:对于耗时操作考虑异步执行
  7. 测试覆盖:编写单元测试和集成测试
  8. 监控告警:对关键初始化任务添加监控

8.4 技术选型建议

// 新项目推荐使用ApplicationRunner(参数处理更友好)
@Component
public class ModernRunner implements ApplicationRunner {@Overridepublic void run(ApplicationArguments args) throws Exception {// 更方便的参数处理if (args.containsOption("debug")) {enableDebugMode();}}
}// 但CommandLineRunner仍然适用于简单场景
@Component  
public class SimpleRunner implements CommandLineRunner {@Overridepublic void run(String... args) throws Exception {// 简单直接的初始化逻辑performBasicInitialization();}
}

8.5 未来发展趋势

随着Spring Boot的发展,CommandLineRunner的使用趋势:

  1. 云原生支持:更好地支持容器化部署场景
  2. 健康检查集成:与Spring Boot Actuator更紧密集成
  3. 监控指标:提供更丰富的执行指标
  4. 异步优化:更好的异步执行支持
  5. 配置管理:更灵活的条件执行机制

相关文章:

CommandLineRunner详细教程

文章目录 1. CommandLineRunner基础概念和背景1.1 什么是CommandLineRunner&#xff1f;1.1.1 核心概念1.1.2 接口定义 1.2 为什么需要CommandLineRunner&#xff1f;1.3 CommandLineRunner的特点1.3.1 执行时机1.3.2 与ApplicationRunner的区别 2. 环境搭建和项目结构2.1 Mave…...

Github 2025-06-05 Go开源项目日报 Top10

根据Github Trendings的统计,今日(2025-06-05统计)共有10个项目上榜。根据开发语言中项目的数量,汇总情况如下: 开发语言项目数量Go项目10TypeScript项目1Go编程语言:构建简单、可靠和高效的软件 创建周期:3474 天开发语言:Go协议类型:BSD 3-Clause “New” or “Revise…...

C++进阶--C++11--智能指针(重点)

文章目录 C进阶--智能指针&#xff08;重点&#xff09;智能指针使用的场景RAII和智能指针的设计思路C标准库智能指针的使用定制删除器智能指针的原理shared_ptr和weak_ptr循环引用&#xff08;容易考&#xff09;weak_ptr 其他知识扩展(类型转换)总结个人学习心得结语 很高兴和…...

CSP-38th

目录 1.正态分布 2.走马 3.信息传输 4.字符串可能性个数 5.最多访问节点个数 1.正态分布 本来是很简单的一道模拟题&#xff0c;根据 (n-u) /a 的整数位、十分位确定是在第几行&#xff0c;根据百分位确定是在第几列&#xff0c;但是我直接将 (n-u)/a 乘以100后进行 // 和…...

企业私有化部署DeepSeek实战指南:从硬件选型到安全运维——基于国产大模型的安全可控落地实践

一、部署前的战略评估与规划 私有化部署不仅是技术工程&#xff0c;更是企业数据战略的核心环节。需重点评估三方面&#xff1a; 1、业务场景适配性​ 适用场景&#xff1a;金融风控&#xff08;需实时数据处理&#xff09;、医疗诊断&#xff08;敏感病历保护&#xff09;、政…...

【西门子杯工业嵌入式-5-串口实现数据收发】

西门子杯工业嵌入式-5-串口实现数据收发 一、通信基础1.1 什么是通信1.2 嵌入式系统中的通信 二、串行通信原理2.1 串行通信简介2.2 通信参数约定 三、GD32F470 串口资源与性能3.1 串口硬件资源 四、串口通信的实现4.1 串口初始化流程4.2 串口发送函数编写4.3 使用 printf 实现…...

F(x,y)= 0 隐函数 微分法

&#x1f7e6; 一、隐函数微分法简介 ▶ 什么是隐函数&#xff1f; 显函数&#xff1a;形如 y f ( x ) y f(x) yf(x)&#xff0c;变量之间是显式关系。 隐函数&#xff1a;形如 F ( x , y ) 0 F(x, y) 0 F(x,y)0&#xff0c;变量间不是直接表达的&#xff0c;需要通过…...

深度学习登上Nature子刊!特征选择创新思路

2025深度学习发论文&模型涨点之——特征选择 特征选择作为机器学习与数据挖掘领域的核心预处理步骤&#xff0c;其重要性在当今高维数据时代日益凸显。 通过识别最具判别性的特征子集&#xff0c;特征选择算法能够有效缓解"维度灾难"、提升模型泛化能力&#x…...

面壁智能推出 MiniCPM 4.0 端侧大模型,引领端侧智能新变革

在 2025 智源大会期间&#xff0c;面壁智能重磅发布了开源模型 MiniCPM 4.0 的两个新版本&#xff08;0.5B、8B&#xff09;&#xff0c;代号「前进四」。此次发布在人工智能领域引发了广泛关注&#xff0c;标志着端侧大模型技术取得了重大突破。 卓越性能&#xff0c;树立行业…...

NLP学习路线图(三十二): 模型压缩与优化

一、 核心压缩与优化技术详解 1. 知识蒸馏:智慧的传承(Knowledge Distillation, KD) 核心思想:“师授徒业”。训练一个庞大、高性能但笨重的“教师模型”(Teacher Model),让其指导训练一个轻量级的“学生模型”(Student Model)。学生模型学习模仿教师模型的输出行为(…...

javaSE复习(7)

1.KMP算法 使用KMP算法在主串 "abaabaabcabaabc" 中搜索模式串 "abaabc"&#xff0c;到匹配成功时为止&#xff0c;请问在匹配过程中进行的单个字符间的比较次数是&#xff08;&#xff09;。 10次 用于互斥时 初值为1 在一个并发编程环境中&#xff0c…...

算法训练第十一天

150. 逆波兰表达式求值 代码&#xff1a; class Solution(object):def evalRPN(self, tokens):""":type tokens: List[str]:rtype: int"""stack []for i in tokens:if i:b int(stack.pop())a int(stack.pop())stack.append(ab)elif i-:b i…...

【联网玩具】EN 18031欧盟网络安全认证

在当今数字化时代&#xff0c;带联网功能的玩具越来越受到孩子们的喜爱&#xff0c;它们为儿童带来了前所未有的互动体验和学习机会。然而&#xff0c;随着这类玩具的普及&#xff0c;网络安全问题也日益凸显。为了保障儿童使用这类玩具时的安全与隐私&#xff0c;欧盟出台了 E…...

Linux 如何移动目录 (文件夹) (内含 Linux 重命名方法)

1-移动单个文件夹&#xff08;类似于自动剪切和粘贴&#xff09; 看看以下的例子&#xff1a; (base) schen744mgmt-4:~/code/sparseocc/data/nuScenes-Occupancy$ ls nuScenes-Occupancy-v0.1 nuScenes-Occupancy-v0.1.7z (base) schen744mgmt-4:~/code/sparseocc/data/nuS…...

WireShark相关技巧

文章目录 1 Wireshark如何设置解析SIP 1 Wireshark如何设置解析SIP 编辑->首选项->protocols->sip 选中sip 2 点击“编辑”->“首选项”->“protocol”->ESP ,按照如下红框显示&#xff0c;进行勾选&#xff0c;点击应用...

LLMs之Structured Output:vLLM 结构化输出指南—从约束生成到自动解析与高效实现

LLMs之Structured Output&#xff1a;vLLM 结构化输出指南—从约束生成到自动解析与高效实现 导读&#xff1a;随着大语言模型&#xff08;LLM&#xff09;在各类任务中的广泛应用&#xff0c;如何使其输出具备可控性、结构化与可解析性&#xff0c;成为实际部署中的关键问题。…...

DAY 45 Tensorboard使用介绍

知识点回顾&#xff1a; tensorboard的发展历史和原理tensorboard的常见操作tensorboard在cifar上的实战&#xff1a;MLP和CNN模型 作业&#xff1a;对resnet18在cifar10上采用微调策略下&#xff0c;用tensorboard监控训练过程。 PS: tensorboard和torch版本存在一定的不兼容…...

LeetCode刷题 -- 542. 01矩阵 基于 DFS 更新优化的多源最短路径实现

LeetCode刷题 – 542. 01矩阵 基于 DFS 更新优化的多源最短路径实现 题目描述简述 给定一个 m x n 的二进制矩阵 mat&#xff0c;其中&#xff1a; 每个元素为 0 或 1返回一个同样大小的矩阵 ans&#xff0c;其中 ans[i][j] 表示 mat[i][j] 到最近 0 的最短曼哈顿距离 算法思…...

TM中,return new TransactionManagerImpl(raf, fc);为什么返回是new了一个新的实例

这是一个典型的 构造器注入 封装资源的用法 &#x1f9e9; 代码片段 return new TransactionManagerImpl(raf, fc);✅ 简单解释&#xff1a; 这行代码的意思是&#xff1a; 使用已经打开的 RandomAccessFile 和 FileChannel&#xff0c;创建并返回一个新的 TransactionManag…...

将 tensorflow keras 训练数据集转换为 Yolo 训练数据集

以 https://www.kaggle.com/datasets/vipoooool/new-plant-diseases-dataset 为例 1. 图像分类数据集文件结构 (例如用于 yolov11n-cls.pt 训练) import os import csv import random from PIL import Image from sklearn.model_selection import train_test_split import s…...

(新手友好)MySQL学习笔记(6):分组查询,正则表达式

目录 分组查询 创建分组 过滤分组 分组查询练习 正则表达式 匹配单个实例 匹配多个实例 正则表达式练习 练习答案 分组查询练习答案 正则表达式练习答案 分组查询 创建分组 group by 子句&#xff1a;根据一个或多个字段对结果集进行分组&#xff0c;在分组的字段上…...

台式机电脑CPU天梯图2025年6月份更新:CPU选购指南及推荐

组装电脑选硬件的过程中,CPU的选择无疑是最关键的,因为它是最核心的硬件,关乎着一台电脑的性能好坏。对于小白来说,CPU天梯图方便直接判断两款CPU性能高低,准确的说,是多核性能。下面给大家分享一下台式机电脑CPU天梯图2025年6月版,来看看吧。 桌面CPU性能排行榜2025 台…...

【hadoop】Flink安装部署

一、单机模式 步骤&#xff1a; 1、使用XFTP将Flink安装包flink-1.13.5-bin-scala_2.11.tgz发送到master机器的主目录。 2、解压安装包&#xff1a; tar -zxvf ~/flink-1.13.5-bin-scala_2.11.tgz 3、修改文件夹的名字&#xff0c;将其改为flume&#xff0c;或者创建软连接…...

将单体架构项目拆分成微服务时的两种工程结构

一.独立Project 1.示意图 此时我们创建一个文件夹&#xff0c;在这个文件夹中&#xff0c;创建N个Project&#xff0c;每一个Project对应一个微服务&#xff0c;组成我们的最终的项目。 2.特点 适合那种超大型项目&#xff0c;比如淘宝&#xff0c;但管理负担比较重。 二.Mave…...

Unity3D 开发中的创新技术:解锁 3D 开发的新境界

在 3D 开发的广袤天地里&#xff0c;Unity3D 一直是众多开发者的得力伙伴。可如今&#xff0c;普通的开发方式似乎难以满足日益增长的创意与效率需求。你是否好奇&#xff0c;凭什么别家团队能用 Unity3D 打造出令人拍案叫绝的 3D 作品&#xff0c;自己却总感觉差了那么一点火候…...

UOS 20 Pro为国际版WPS设置中文菜单

UOS 20 Pro为国际版WPS设置中文菜单 查看UOS操作系统系统安装国际版wps并汉化方法1:下载zh_CN.tar.gz语言包方法2&#xff1a;手动从国内版wps12的包中提取中文菜单解压国内版wps的包 复制中文语言包到wps国际版目录下安装Windows字体 安装开源office 查看UOS操作系统系统 # 查…...

树莓派系统中设置固定 IP

在基于 Ubuntu 的树莓派系统中&#xff0c;设置固定 IP 地址主要有以下几种方法&#xff1a; 方法一&#xff1a;使用 Netplan 配置&#xff08;Ubuntu 18.04 及以上版本默认使用 Netplan&#xff09; 查看网络接口名称 在终端输入ip link或ip a命令&#xff0c;查看当前所使…...

单例模式与锁(死锁)

目录 线程安全的单例模式 什么是单例模式 单例模式的特点 饿汉实现方式和懒汉实现方式 饿汉⽅式实现单例模式 懒汉⽅式实现单例模式 懒汉⽅式实现单例模式(线程安全版本) 单例式线程池 ThreadPool.hpp threadpool.cc 运行结果 线程安全和重⼊问题 常⻅锁概念 死…...

LLM基础2_语言模型如何文本编码

基于GitHub项目&#xff1a;https://github.com/datawhalechina/llms-from-scratch-cn 字节对编码(BPE) 上一篇博文说到 为什么GPT模型不需要[PAD]和[UNK]&#xff1f; GPT使用更先进的字节对编码(BPE)&#xff0c;总能将词语拆分成已知子词 为什么需要BPE&#xff1f; 简…...

理解世界如淦泽,穿透黑幕需老谋

理解世界如淦泽&#xff0c;穿透黑幕需老谋 卡西莫多 2025年06月07日 安徽 极少主动跟别人提及恩师的名字&#xff0c;生怕自己比孙猴子不成器但又比它更能惹事的德行&#xff0c;使得老师跟着被拖累而脸上无光。不过老师没有象菩提祖师训诫孙猴子那样不能说出师傅的名字&a…...