SpringBoot CommandLineRunner應(yīng)用啟動(dòng)后執(zhí)行代碼實(shí)例
引言
在企業(yè)級(jí)應(yīng)用開(kāi)發(fā)中,我們經(jīng)常需要在應(yīng)用啟動(dòng)完成后執(zhí)行一些初始化操作,例如預(yù)加載緩存數(shù)據(jù)、創(chuàng)建默認(rèn)管理員賬戶(hù)、數(shù)據(jù)遷移等任務(wù)。
Spring Boot提供了CommandLineRunner接口,使開(kāi)發(fā)者能夠優(yōu)雅地實(shí)現(xiàn)這些需求。
一、CommandLineRunner基礎(chǔ)
CommandLineRunner是Spring Boot提供的一個(gè)接口,用于在Spring應(yīng)用上下文完全初始化后、應(yīng)用正式提供服務(wù)前執(zhí)行特定的代碼邏輯。該接口只包含一個(gè)run方法,方法參數(shù)為應(yīng)用啟動(dòng)時(shí)傳入的命令行參數(shù)。
CommandLineRunner接口定義如下:
@FunctionalInterface
public interface CommandLineRunner {
/**
* 在SpringApplication啟動(dòng)后回調(diào)
* @param args 來(lái)自應(yīng)用程序的命令行參數(shù)
* @throws Exception 如果發(fā)生錯(cuò)誤
*/
void run(String... args) throws Exception;
}當(dāng)一個(gè)Spring Boot應(yīng)用啟動(dòng)時(shí),Spring容器會(huì)在完成所有Bean的初始化后,自動(dòng)檢索并執(zhí)行所有實(shí)現(xiàn)了CommandLineRunner接口的Bean。
這種機(jī)制為應(yīng)用提供了一個(gè)明確的初始化時(shí)機(jī),確保所有依賴(lài)項(xiàng)都已準(zhǔn)備就緒。
二、基本用法
2.1 創(chuàng)建CommandLineRunner實(shí)現(xiàn)
實(shí)現(xiàn)CommandLineRunner接口的最簡(jiǎn)單方式是創(chuàng)建一個(gè)組件類(lèi)并實(shí)現(xiàn)該接口:
package com.example.demo.runner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
/**
* 簡(jiǎn)單的CommandLineRunner實(shí)現(xiàn)
* 用于演示基本用法
*/
@Component
public class SimpleCommandLineRunner implements CommandLineRunner {
private static final Logger logger = LoggerFactory.getLogger(SimpleCommandLineRunner.class);
@Override
public void run(String... args) throws Exception {
logger.info("應(yīng)用啟動(dòng)完成,開(kāi)始執(zhí)行初始化操作...");
// 執(zhí)行初始化邏輯
logger.info("初始化操作完成");
// 如果需要,可以訪問(wèn)命令行參數(shù)
if (args.length > 0) {
logger.info("接收到的命令行參數(shù):");
for (int i = 0; i < args.length; i++) {
logger.info("參數(shù) {}: {}", i, args[i]);
}
}
}
}通過(guò)@Component注解,Spring會(huì)自動(dòng)掃描并注冊(cè)這個(gè)Bean,然后在應(yīng)用啟動(dòng)完成后調(diào)用其run方法。
2.2 使用Lambda表達(dá)式
由于CommandLineRunner是一個(gè)函數(shù)式接口,我們也可以使用Lambda表達(dá)式簡(jiǎn)化代碼。這種方式適合實(shí)現(xiàn)簡(jiǎn)單的初始化邏輯:
package com.example.demo.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 通過(guò)@Bean方法創(chuàng)建CommandLineRunner
*/
@Configuration
public class AppConfig {
private static final Logger logger = LoggerFactory.getLogger(AppConfig.class);
@Bean
public CommandLineRunner initDatabase() {
return args -> {
logger.info("初始化數(shù)據(jù)庫(kù)連接池...");
// 執(zhí)行數(shù)據(jù)庫(kù)初始化邏輯
};
}
@Bean
public CommandLineRunner loadCache() {
return args -> {
logger.info("預(yù)加載緩存數(shù)據(jù)...");
// 執(zhí)行緩存預(yù)熱邏輯
};
}
}在這個(gè)例子中,我們通過(guò)@Bean方法創(chuàng)建了兩個(gè)CommandLineRunner實(shí)例,分別負(fù)責(zé)數(shù)據(jù)庫(kù)初始化和緩存預(yù)熱。
三、進(jìn)階特性
3.1 執(zhí)行順序控制
當(dāng)應(yīng)用中存在多個(gè)CommandLineRunner時(shí),可能需要控制它們的執(zhí)行順序。
Spring提供了@Order注解(或?qū)崿F(xiàn)Ordered接口)來(lái)實(shí)現(xiàn)這一需求:
package com.example.demo.runner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* 具有執(zhí)行順序的CommandLineRunner
* Order值越小,優(yōu)先級(jí)越高,執(zhí)行越早
*/
@Component
@Order(1) // 優(yōu)先級(jí)高,最先執(zhí)行
public class DatabaseInitializer implements CommandLineRunner {
private static final Logger logger = LoggerFactory.getLogger(DatabaseInitializer.class);
@Override
public void run(String... args) throws Exception {
logger.info("第一步:初始化數(shù)據(jù)庫(kù)...");
// 數(shù)據(jù)庫(kù)初始化邏輯
}
}
@Component
@Order(2) // 次優(yōu)先級(jí)
public class CacheWarmer implements CommandLineRunner {
private static final Logger logger = LoggerFactory.getLogger(CacheWarmer.class);
@Override
public void run(String... args) throws Exception {
logger.info("第二步:預(yù)熱緩存...");
// 緩存預(yù)熱邏輯
}
}
@Component
@Order(3) // 最后執(zhí)行
public class NotificationInitializer implements CommandLineRunner {
private static final Logger logger = LoggerFactory.getLogger(NotificationInitializer.class);
@Override
public void run(String... args) throws Exception {
logger.info("第三步:初始化通知服務(wù)...");
// 通知服務(wù)初始化邏輯
}
}通過(guò)@Order注解,我們可以精確控制各個(gè)初始化任務(wù)的執(zhí)行順序,確保依賴(lài)關(guān)系得到滿(mǎn)足。值得注意的是,Order值越小,優(yōu)先級(jí)越高,執(zhí)行越早。
3.2 依賴(lài)注入
CommandLineRunner作為Spring管理的Bean,可以充分利用依賴(lài)注入機(jī)制:
package com.example.demo.runner;
import com.example.demo.service.UserService;
import com.example.demo.service.SystemConfigService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
/**
* 演示在CommandLineRunner中使用依賴(lài)注入
*/
@Component
public class SystemInitializer implements CommandLineRunner {
private static final Logger logger = LoggerFactory.getLogger(SystemInitializer.class);
private final UserService userService;
private final SystemConfigService configService;
@Autowired
public SystemInitializer(UserService userService, SystemConfigService configService) {
this.userService = userService;
this.configService = configService;
}
@Override
public void run(String... args) throws Exception {
logger.info("系統(tǒng)初始化開(kāi)始...");
// 創(chuàng)建默認(rèn)管理員賬戶(hù)
if (!userService.adminExists()) {
logger.info("創(chuàng)建默認(rèn)管理員賬戶(hù)");
userService.createDefaultAdmin();
}
// 加載系統(tǒng)配置
logger.info("加載系統(tǒng)配置到內(nèi)存");
configService.loadAllConfigurations();
logger.info("系統(tǒng)初始化完成");
}
}這個(gè)例子展示了如何在CommandLineRunner中注入并使用服務(wù)組件,實(shí)現(xiàn)更復(fù)雜的初始化邏輯。
3.3 異常處理
CommandLineRunner的run方法允許拋出異常。如果在執(zhí)行過(guò)程中拋出異常,Spring Boot應(yīng)用將無(wú)法正常啟動(dòng)。
這一特性可以用來(lái)確保關(guān)鍵的初始化操作必須成功完成:
package com.example.demo.runner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
/**
* 演示CommandLineRunner中的異常處理
*/
@Component
public class CriticalInitializer implements CommandLineRunner {
private static final Logger logger = LoggerFactory.getLogger(CriticalInitializer.class);
@Override
public void run(String... args) throws Exception {
logger.info("執(zhí)行關(guān)鍵初始化操作...");
try {
// 執(zhí)行可能失敗的操作
boolean success = performCriticalOperation();
if (!success) {
// 如果操作未能成功完成,阻止應(yīng)用啟動(dòng)
throw new RuntimeException("關(guān)鍵初始化操作失敗,應(yīng)用無(wú)法啟動(dòng)");
}
logger.info("關(guān)鍵初始化操作完成");
} catch (Exception e) {
logger.error("初始化過(guò)程中發(fā)生錯(cuò)誤: {}", e.getMessage());
// 重新拋出異常,阻止應(yīng)用啟動(dòng)
throw e;
}
}
private boolean performCriticalOperation() {
// 模擬關(guān)鍵操作的執(zhí)行
return true; // 返回操作結(jié)果
}
}在這個(gè)例子中,如果關(guān)鍵初始化操作失敗,應(yīng)用將無(wú)法啟動(dòng),從而避免系統(tǒng)在不完整狀態(tài)下運(yùn)行。
四、實(shí)際應(yīng)用場(chǎng)景
CommandLineRunner適用于多種實(shí)際場(chǎng)景,下面是一些常見(jiàn)的應(yīng)用:
4.1 數(shù)據(jù)遷移
在系統(tǒng)升級(jí)或數(shù)據(jù)結(jié)構(gòu)變更時(shí),可以使用CommandLineRunner執(zhí)行數(shù)據(jù)遷移操作:
@Component
@Order(1)
public class DataMigrationRunner implements CommandLineRunner {
private final DataMigrationService migrationService;
private static final Logger logger = LoggerFactory.getLogger(DataMigrationRunner.class);
@Autowired
public DataMigrationRunner(DataMigrationService migrationService) {
this.migrationService = migrationService;
}
@Override
public void run(String... args) throws Exception {
logger.info("檢查數(shù)據(jù)庫(kù)版本并執(zhí)行遷移...");
// 獲取當(dāng)前數(shù)據(jù)庫(kù)版本
String currentVersion = migrationService.getCurrentVersion();
logger.info("當(dāng)前數(shù)據(jù)庫(kù)版本: {}", currentVersion);
// 執(zhí)行遷移
boolean migrationNeeded = migrationService.checkMigrationNeeded();
if (migrationNeeded) {
logger.info("需要執(zhí)行數(shù)據(jù)遷移");
migrationService.performMigration();
logger.info("數(shù)據(jù)遷移完成");
} else {
logger.info("無(wú)需數(shù)據(jù)遷移");
}
}
}4.2 任務(wù)調(diào)度初始化
對(duì)于使用動(dòng)態(tài)任務(wù)調(diào)度的應(yīng)用,可以在啟動(dòng)時(shí)從數(shù)據(jù)庫(kù)加載調(diào)度配置:
@Component
public class SchedulerInitializer implements CommandLineRunner {
private final TaskSchedulerService schedulerService;
private final TaskDefinitionRepository taskRepository;
private static final Logger logger = LoggerFactory.getLogger(SchedulerInitializer.class);
@Autowired
public SchedulerInitializer(TaskSchedulerService schedulerService,
TaskDefinitionRepository taskRepository) {
this.schedulerService = schedulerService;
this.taskRepository = taskRepository;
}
@Override
public void run(String... args) throws Exception {
logger.info("初始化任務(wù)調(diào)度器...");
// 從數(shù)據(jù)庫(kù)加載任務(wù)定義
List<TaskDefinition> tasks = taskRepository.findAllActiveTasks();
logger.info("加載了{(lán)}個(gè)調(diào)度任務(wù)", tasks.size());
// 注冊(cè)任務(wù)到調(diào)度器
for (TaskDefinition task : tasks) {
schedulerService.scheduleTask(task);
logger.info("注冊(cè)任務(wù): {}, cron: {}", task.getName(), task.getCronExpression());
}
logger.info("任務(wù)調(diào)度器初始化完成");
}
}4.3 外部系統(tǒng)連接測(cè)試
在應(yīng)用啟動(dòng)時(shí),可以測(cè)試與關(guān)鍵外部系統(tǒng)的連接狀態(tài):
@Component
public class ExternalSystemConnectionTester implements CommandLineRunner {
private final List<ExternalSystemConnector> connectors;
private static final Logger logger = LoggerFactory.getLogger(ExternalSystemConnectionTester.class);
@Autowired
public ExternalSystemConnectionTester(List<ExternalSystemConnector> connectors) {
this.connectors = connectors;
}
@Override
public void run(String... args) throws Exception {
logger.info("測(cè)試外部系統(tǒng)連接...");
for (ExternalSystemConnector connector : connectors) {
String systemName = connector.getSystemName();
logger.info("測(cè)試連接到: {}", systemName);
try {
boolean connected = connector.testConnection();
if (connected) {
logger.info("{} 連接成功", systemName);
} else {
logger.warn("{} 連接失敗,但不阻止應(yīng)用啟動(dòng)", systemName);
}
} catch (Exception e) {
logger.error("{} 連接異常: {}", systemName, e.getMessage());
// 根據(jù)系統(tǒng)重要性決定是否拋出異常阻止應(yīng)用啟動(dòng)
}
}
logger.info("外部系統(tǒng)連接測(cè)試完成");
}
}五、最佳實(shí)踐
在使用CommandLineRunner時(shí),以下最佳實(shí)踐可以幫助開(kāi)發(fā)者更有效地利用這一功能:
- 職責(zé)單一:每個(gè)CommandLineRunner應(yīng)專(zhuān)注于一個(gè)特定的初始化任務(wù),遵循單一職責(zé)原則。
- 合理分組:相關(guān)的初始化任務(wù)可以組織在同一個(gè)CommandLineRunner中,減少代碼碎片化。
- 異步執(zhí)行:對(duì)于耗時(shí)的初始化任務(wù),考慮使用異步執(zhí)行,避免延長(zhǎng)應(yīng)用啟動(dòng)時(shí)間:
@Component
public class AsyncInitializer implements CommandLineRunner {
private final TaskExecutor taskExecutor;
private static final Logger logger = LoggerFactory.getLogger(AsyncInitializer.class);
@Autowired
public AsyncInitializer(TaskExecutor taskExecutor) {
this.taskExecutor = taskExecutor;
}
@Override
public void run(String... args) throws Exception {
logger.info("啟動(dòng)異步初始化任務(wù)...");
taskExecutor.execute(() -> {
try {
logger.info("異步任務(wù)開(kāi)始執(zhí)行");
Thread.sleep(5000); // 模擬耗時(shí)操作
logger.info("異步任務(wù)執(zhí)行完成");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
logger.error("異步任務(wù)被中斷", e);
} catch (Exception e) {
logger.error("異步任務(wù)執(zhí)行失敗", e);
}
});
logger.info("異步初始化任務(wù)已提交,應(yīng)用繼續(xù)啟動(dòng)");
}
}- 優(yōu)雅降級(jí):對(duì)于非關(guān)鍵性初始化任務(wù),實(shí)現(xiàn)優(yōu)雅降級(jí),避免因次要功能故障而阻止整個(gè)應(yīng)用啟動(dòng)。
- 合理日志:使用適當(dāng)?shù)娜罩炯?jí)別記錄初始化過(guò)程,便于問(wèn)題排查和性能分析。
- 條件執(zhí)行:根據(jù)環(huán)境或配置條件決定是否執(zhí)行特定的初始化任務(wù),增強(qiáng)靈活性:
@Component
@ConditionalOnProperty(name = "app.cache.preload", havingValue = "true")
public class ConditionalInitializer implements CommandLineRunner {
private static final Logger logger = LoggerFactory.getLogger(ConditionalInitializer.class);
@Override
public void run(String... args) throws Exception {
logger.info("執(zhí)行條件初始化任務(wù),僅在配置啟用時(shí)執(zhí)行");
// 僅在特定條件下執(zhí)行的初始化邏輯
}
}總結(jié)
Spring Boot的CommandLineRunner接口為應(yīng)用提供了一種優(yōu)雅的機(jī)制,用于在啟動(dòng)完成后執(zhí)行初始化代碼。
通過(guò)實(shí)現(xiàn)這一接口,開(kāi)發(fā)者可以確保在應(yīng)用對(duì)外提供服務(wù)前,必要的準(zhǔn)備工作已經(jīng)完成。
結(jié)合@Order注解可以精確控制多個(gè)初始化任務(wù)的執(zhí)行順序,依賴(lài)注入機(jī)制使得各種服務(wù)組件可以在初始化過(guò)程中方便地使用。
在實(shí)際應(yīng)用中,CommandLineRunner可以用于數(shù)據(jù)遷移、緩存預(yù)熱、連接測(cè)試等多種場(chǎng)景。
通過(guò)遵循單一職責(zé)原則、合理組織代碼、實(shí)現(xiàn)異步執(zhí)行和優(yōu)雅降級(jí)等最佳實(shí)踐,可以構(gòu)建更加健壯和高效的Spring Boot應(yīng)用。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
全排列算法-遞歸與字典序的實(shí)現(xiàn)方法(Java)
下面小編就為大家?guī)?lái)一篇全排列算法-遞歸與字典序的實(shí)現(xiàn)方法(Java) 。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-04-04
線程池調(diào)用kafka發(fā)送消息產(chǎn)生的內(nèi)存泄漏問(wèn)題排查解決
這篇文章主要為大家介紹了線程池調(diào)用kafka發(fā)送消息產(chǎn)生的內(nèi)存泄漏問(wèn)題排查解決,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08
JAVA簡(jiǎn)單鏈接Oracle數(shù)據(jù)庫(kù) 注冊(cè)和登陸功能的實(shí)現(xiàn)代碼
這篇文章主要介紹了JAVA鏈接Oracle并實(shí)現(xiàn)注冊(cè)與登錄功能的代碼實(shí)例,有需要的朋友可以參考一下2014-01-01
Spring @Primary和@Qualifier注解原理解析
這篇文章主要介紹了Spring @Primary和@Qualifier注解原理解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-04-04
java如何拷貝復(fù)制對(duì)象和集合問(wèn)題
這篇文章主要介紹了java如何拷貝復(fù)制對(duì)象和集合問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-09-09
springboot使用jasypt對(duì)配置文件加密加密數(shù)據(jù)庫(kù)連接的操作代碼
這篇文章主要介紹了springboot使用jasypt對(duì)配置文件加密加密數(shù)據(jù)庫(kù)連接的操作代碼,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2024-01-01
老生常談Java網(wǎng)絡(luò)編程TCP通信(必看篇)
下面小編就為大家?guī)?lái)一篇老生常談Java網(wǎng)絡(luò)編程TCP通信(必看篇)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-05-05

