SpringBoot總結(jié)之CommandLineRunner

筆者基于目前業(yè)務(wù)需求需要提前將部分?jǐn)?shù)據(jù)加載到Spring容器中。大家可以想一下解決方案,下面評(píng)論去留言。筆者能夠想到的解決方案:
1、 定義靜態(tài)常量,隨著類的生命周期加載而提前加載(這種方式可能對(duì)于工作經(jīng)驗(yàn)較少的伙伴,選擇是最多的);
2、 實(shí)現(xiàn)CommandLineRunner接口;容器啟動(dòng)之后,加載實(shí)現(xiàn)類的邏輯資源,已達(dá)到完成資源初始化的任務(wù);
3、 @PostConstruct;在具體Bean的實(shí)例化過程中執(zhí)行,@PostConstruct注解的方法,會(huì)在構(gòu)造方法之后執(zhí)行;
加載順序?yàn)椋篊onstructor > @Autowired > @PostConstruct > 靜態(tài)方法;
特點(diǎn):
只有一個(gè)非靜態(tài)方法能使用此注解 被注解的方法不得有任何參數(shù) 被注解的方法返回值必須為void 被注解方法不得拋出已檢查異常 此方法只會(huì)被執(zhí)行一次
4、 實(shí)現(xiàn)InitializingBean接口;重寫afterPropertiesSet()方法;
以上方案供大家參考,提供一種解決思路。但是日常開發(fā)中有可能需要實(shí)現(xiàn)在項(xiàng)目啟動(dòng)后執(zhí)行的功能,因此誕生了此篇文章。
思路:SpringBoot提供的一種簡單的實(shí)現(xiàn)方案,實(shí)現(xiàn)CommandLineRunner接口,實(shí)現(xiàn)功能的代碼放在實(shí)現(xiàn)的run方法中加載,并且如果多個(gè)類需要夾加載順序,則實(shí)現(xiàn)類上使用@Order注解,且value值越小則優(yōu)先級(jí)越高。
實(shí)踐
上面筆者做了簡單的介紹,下面我們進(jìn)入實(shí)戰(zhàn)part。
基于CommandLineRunner接口建立兩個(gè)實(shí)現(xiàn)類為RunnerLoadOne 、RunnerLoadTwo ;并設(shè)置加載順序;
筆者這里使用了ClassDo對(duì)象,主要是能夠體現(xiàn)@Order注解的加載順序,實(shí)際應(yīng)用開發(fā)中,大家根據(jù)業(yè)務(wù)需求場景適當(dāng)調(diào)整(學(xué)以致用吧)。
@Component
@Order(1)
public class RunnerLoadOne implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
ClassDo classDo = SpringContextUtil.getBean(ClassDo.class);
classDo.setClassName("Java");
System.out.println("------------容器初始化bean之后,加載資源結(jié)束-----------");
}
}
@Component
@Order(2)
public class RunnerLoadTwo implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
ClassDo bean = SpringContextUtil.getBean(ClassDo.class);
System.out.println("依賴預(yù)先加載的資源數(shù)據(jù):" + bean.getClassName());
}
}
啟動(dòng)主實(shí)現(xiàn)類,看到console打印的結(jié)果如下:
...
2020-08-06 21:20:14.582 INFO 6612 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Bean with name 'dataSource' has been autodetected for JMX exposure
2020-08-06 21:20:14.592 INFO 6612 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Located MBean 'dataSource': registering with JMX server as MBean [com.zaxxer.hikari:name=dataSource,type=HikariDataSource]
2020-08-06 21:20:14.686 INFO 6612 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8666 (http) with context path ''
2020-08-06 21:20:14.693 INFO 6612 --- [ main] com.qxy.InformalEssayApplication : Started InformalEssayApplication in 121.651 seconds (JVM running for 173.476)
------------容器初始化bean之后,加載資源結(jié)束-----------
依賴預(yù)先加載的資源數(shù)據(jù):Java
源碼跟蹤
通過上面的實(shí)踐操作,大家應(yīng)該理解如何使用的,下面帶著大家理解一下底層如何實(shí)現(xiàn)的;
常規(guī)操作,主啟動(dòng)類debugger走起來~
run()方法
跟進(jìn)run方法后,一路F6直達(dá)以下方法
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
//設(shè)置線程啟動(dòng)計(jì)時(shí)器
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
//配置系統(tǒng)屬性:默認(rèn)缺失外部顯示屏等允許啟動(dòng)
configureHeadlessProperty();
//獲取并啟動(dòng)事件監(jiān)聽器,如果項(xiàng)目中沒有其他監(jiān)聽器,則默認(rèn)只有EventPublishingRunListener
SpringApplicationRunListeners listeners = getRunListeners(args);
//將事件廣播給listeners
listeners.starting();
try {
//對(duì)于實(shí)現(xiàn)ApplicationRunner接口,用戶設(shè)置ApplicationArguments參數(shù)進(jìn)行封裝
ApplicationArguments applicationArguments = new DefaultApplicationArguments(
args);
//配置運(yùn)行環(huán)境:例如激活應(yīng)用***.yml配置文件
ConfigurableEnvironment environment = prepareEnvironment(listeners,
applicationArguments);
configureIgnoreBeanInfo(environment);
//加載配置的banner(gif,txt...),即控制臺(tái)圖樣
Banner printedBanner = printBanner(environment);
//創(chuàng)建上下文對(duì)象,并實(shí)例化
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(
SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
//配置SPring容器
prepareContext(context, environment, listeners, applicationArguments,
printedBanner);
//刷新Spring上下文,創(chuàng)建bean過程中
refreshContext(context);
//空方法,子類實(shí)現(xiàn)
afterRefresh(context, applicationArguments);
//停止計(jì)時(shí)器:計(jì)算線程啟動(dòng)共用時(shí)間
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass)
.logStarted(getApplicationLog(), stopWatch);
}
//停止事件監(jiān)聽器
listeners.started(context);
//開始加載資源
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, listeners, exceptionReporters, ex);
throw new IllegalStateException(ex);
}
listeners.running(context);
return context;
}
本篇文章主要是熟悉SpringBoot的CommandLineRunner接口實(shí)現(xiàn)原理。因此上面SpringBoot啟動(dòng)過程方法不做過多介紹。我們直接進(jìn)入正題CallRunners()方法內(nèi)部。
callRunners方法
private void callRunners(ApplicationContext context, ApplicationArguments args) {
//將實(shí)現(xiàn)ApplicationRunner和CommandLineRunner接口的類,存儲(chǔ)到集合中
List<Object> runners = new ArrayList<>();
runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
//按照加載先后順序排序
AnnotationAwareOrderComparator.sort(runners);
for (Object runner : new LinkedHashSet<>(runners)) {
if (runner instanceof ApplicationRunner) {
callRunner((ApplicationRunner) runner, args);
}
if (runner instanceof CommandLineRunner) {
callRunner((CommandLineRunner) runner, args);
}
}
}
上面部分代碼非常簡單,對(duì)于Spring源碼見到如此簡單邏輯代碼,內(nèi)心是否有一絲絲的激動(dòng)~
private void callRunner(CommandLineRunner runner, ApplicationArguments args) {
try {
//調(diào)用各個(gè)實(shí)現(xiàn)類中的邏輯實(shí)現(xiàn)
(runner).run(args.getSourceArgs());
}
catch (Exception ex) {
throw new IllegalStateException("Failed to execute CommandLineRunner", ex);
}
}
到此結(jié)束,再跟進(jìn)run()方法,就可以看到我們實(shí)現(xiàn)的資源加載邏輯啦~
總結(jié)
新手跟進(jìn)源代碼理解總結(jié),若存在不當(dāng)之處,希望大佬及時(shí)指正。相信經(jīng)過上面簡答的介紹,大家應(yīng)該能夠清晰的理解CommandLineRunner接口的實(shí)際應(yīng)用場景了。
