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 { @override public 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 { @override public 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/shanghai username: root password: password driver-class-name: com.mysql.cj.jdbc.driver # jpa配置 jpa: hibernate: ddl-auto: update show-sql: true properties: hibernate: dialect: org.hibernate.dialect.mysql8dialect format_sql: true # redis配置 data: redis: host: localhost port: 6379 database: 0 timeout: 2000ms jedis: pool: max-active: 8 max-wait: -1ms max-idle: 8 min-idle: 0 # 服务器配置 server: port: 8080 servlet: context-path: /api # 日志配置 logging: level: com.example: debug org.springframework: info org.hibernate: info pattern: 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: true database-init: true cache-warmup: true system-check: true initialization: admin-username: admin admin-password: admin123 admin-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/shanghai jpa: hibernate: ddl-auto: create-drop show-sql: true app: runner: database-init: true cache-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/shanghai jpa: hibernate: ddl-auto: validate show-sql: false app: runner: database-init: false # 生产环境通常不自动初始化 cache-warmup: true system-check: true logging: level: com.example: info root: 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 { @override public 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方式 */ @bean public commandlinerunner simplerunner() { return args -> { log.info("=== lambda commandlinerunner 执行 ==="); log.info("这是通过lambda表达式创建的commandlinerunner"); // 执行简单任务 printwelcomemessage(); }; } /** * 带参数处理的lambda方式 */ @bean public commandlinerunner parameterprocessorrunner() { return args -> { log.info("=== 参数处理 commandlinerunner ==="); // 解析和处理命令行参数 processcommandlinearguments(args); }; } /** * 条件执行的runner */ @bean public 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; @override public 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; @override public 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 { @override public 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 { @override public 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 { @override public 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; @override public 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("=== 数据库初始化结束 ==="); } @transactional private 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("角色数据初始化完成"); } @transactional private 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); } } @transactional private 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; @override public 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); @override public 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; @override public 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 setters public 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 { @mock private userservice userservice; private databaseinitializationrunner runner; @beforeeach void setup() { runner = new databaseinitializationrunner(userservice); } @test void testrunwithenabledinitialization() throws exception { // 模拟管理员用户不存在 when(userservice.existsadminuser()).thenreturn(false); // 执行runner runner.run("--spring.profiles.active=test"); // 验证是否创建了管理员用户 verify(userservice, times(1)).createdefaultadminuser(); } @test void testrunwithexistingadmin() throws exception { // 模拟管理员用户已存在 when(userservice.existsadminuser()).thenreturn(true); // 执行runner runner.run(); // 验证没有创建新的管理员用户 verify(userservice, never()).createdefaultadminuser(); } @test void 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 { @test void 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 { @override public 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("异步初始化完成"); }); } @override public 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; @override public void run(string... args) throws exception { // 根据配置执行初始化 } }
7. 常见问题和解决方案
7.1 常见错误
7.1.1 依赖注入问题
// ❌ 问题:依赖项可能未完全初始化 @component public class earlyrunner implements commandlinerunner { @autowired private someservice someservice; // 可能还未准备好 @override public void run(string... args) throws exception { someservice.dosomething(); // 可能失败 } } // ✅ 解决方案:使用构造函数注入和检查 @component @requiredargsconstructor public class saferunner implements commandlinerunner { private final someservice someservice; @override public void run(string... args) throws exception { if (someservice == null) { log.error("someservice未注入"); return; } someservice.dosomething(); } }
7.1.2 执行时间过长
// ✅ 解决方案:添加超时控制和进度监控 @component public class timeoutawarerunner implements commandlinerunner { @override public 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 { @override public 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 最佳实践总结
- 职责分离:每个runner只负责一个特定的初始化任务
- 顺序控制:使用@order注解明确执行顺序
- 异常处理:区分关键和非关键操作的异常处理策略
- 配置驱动:通过配置控制runner的启用和行为
- 日志记录:添加详细的执行日志便于调试
- 性能考虑:对于耗时操作考虑异步执行
- 测试覆盖:编写单元测试和集成测试
- 监控告警:对关键初始化任务添加监控
8.4 技术选型建议
// 新项目推荐使用applicationrunner(参数处理更友好) @component public class modernrunner implements applicationrunner { @override public void run(applicationarguments args) throws exception { // 更方便的参数处理 if (args.containsoption("debug")) { enabledebugmode(); } } } // 但commandlinerunner仍然适用于简单场景 @component public class simplerunner implements commandlinerunner { @override public void run(string... args) throws exception { // 简单直接的初始化逻辑 performbasicinitialization(); } }
8.5 未来发展趋势
随着spring boot的发展,commandlinerunner的使用趋势:
- 云原生支持:更好地支持容器化部署场景
- 健康检查集成:与spring boot actuator更紧密集成
- 监控指标:提供更丰富的执行指标
- 异步优化:更好的异步执行支持
- 配置管理:更灵活的条件执行机制
到此这篇关于commandlinerunner最佳实践小结的文章就介绍到这了,更多相关commandlinerunner用法内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论