使用場景:
我們在開發(fā)過程中會有這樣的場景:需要在項目啟動后執(zhí)行一些操作,比如:讀取配置文件信息,數(shù)據(jù)庫連接,刪除臨時文件,清除緩存信息,工廠類初始化等。我們會有多種的實現(xiàn)方式,例如@PostConstruct 、CommandLineRunner、ApplicationRunner、ApplicationListener都可以實現(xiàn)在springboot啟動后執(zhí)行我們特定的邏輯,接下對比下他們的區(qū)別
1 @PostConstruct
該注解被用來修飾一個非靜態(tài)的void方法,被@PostConstruct修飾的方法會在服務(wù)器加載Servlet的時候運(yùn)行,并且只會被服務(wù)器執(zhí)行一次。
觸發(fā)時機(jī):
SpringBoot會把標(biāo)記了Bean相關(guān)注解(例如@Component、@Service、@Repository等)的類或接口自動初始化全局的單一實例,如果標(biāo)記了初始化順序會按照用戶標(biāo)記的順序,否則按照默認(rèn)順序初始化。在初始化的過程中,執(zhí)行完一個Bean的構(gòu)造方法后會執(zhí)行該Bean的@PostConstruct方法(如果有),然后初始化下一個Bean。
spring中bean的創(chuàng)建過程
配置Bean(@Component、@Service、@Controller等注解配置) -----> 解析為Bean的元數(shù)據(jù)(Bean容器中的BeanDefinition對象) --> 根據(jù)Bean的元數(shù)據(jù)生成Bean(創(chuàng)建bean)
創(chuàng)建bean的時候執(zhí)行順序
Constructor(構(gòu)造方法) -> @Autowired(依賴注入) -> @PostConstruct(注釋的方法)
示例:
@PostConstruct
public void dispatcher() throws Exception {
// 邏輯代碼
}
優(yōu)點:
- 使用簡單,在spring容器管理的類中添加此注解即可
缺點:
- .在spring創(chuàng)建bean的時候觸發(fā),此時容器還未完全初始化完畢,如果邏輯中引用了還未完成初始化的bean會導(dǎo)致異常 ,所以需要考慮加載順序。
- 如果@PostConstruct方法內(nèi)的邏輯處理時間較長,就會增加SpringBoot應(yīng)用初始化Bean的時間,進(jìn)而增加應(yīng)用啟動的時間。因為只有在Bean初始化完成后,SpringBoot應(yīng)用才會打開端口提供服務(wù),所以在此之前,應(yīng)用不可訪問。
2 CommandLineRunner、ApplicationRunner
使用起來很簡單,只需要實現(xiàn)CommandLineRunner或者ApplicationRunner接口,重寫run方法就行。
觸發(fā)時機(jī):
通過springboot啟動源碼:
啟動后會執(zhí)行 callRunners方法;
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
//設(shè)置線程啟動計時器
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
//配置系統(tǒng)屬性:默認(rèn)缺失外部顯示屏等允許啟動
configureHeadlessProperty();
//獲取并啟動事件監(jiān)聽器,如果項目中沒有其他監(jiān)聽器,則默認(rèn)只有EventPublishingRunListener
SpringApplicationRunListeners listeners = getRunListeners(args);
//將事件廣播給listeners
listeners.starting();
try {
//對于實現(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...),即控制臺圖樣
Banner printedBanner = printBanner(environment);
//創(chuàng)建上下文對象,并實例化
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(
SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
//配置SPring容器
prepareContext(context, environment, listeners, applicationArguments,
printedBanner);
//刷新Spring上下文,創(chuàng)建bean過程中
refreshContext(context);
//空方法,子類實現(xiàn)
afterRefresh(context, applicationArguments);
//停止計時器:計算線程啟動共用時間
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;
}
callRunners方法:
private void callRunners(ApplicationContext context, ApplicationArguments args) {
//將實現(xiàn)ApplicationRunner和CommandLineRunner接口的類,存儲到集合中
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);
}
}
}
從上面源碼可以看到 ,在springboot完全初始化完畢后,會執(zhí)行CommandLineRunner和ApplicationRunner,兩者唯一的區(qū)別是參數(shù)不同,但是不會影響項目啟動速度,都可以獲取到執(zhí)行參數(shù)。
示例
/**
* @author
* @date 2021-08-23 16:19
*/
@Component
public class ServerDispatcher implements CommandLineRunner {
@Override
public void run(String... args){
// 邏輯代碼
}
}
/**
* @author
* @date 2021-08-23 16:19
*/
@Component
public class ServerDispatcher implements ApplicationRunner {
@Override
public void run(ApplicationArguments args){
// 邏輯代碼
}
}
3 ApplicationListener
通過事件監(jiān)聽我們也可以實現(xiàn)springboot啟動執(zhí)行方法。實現(xiàn)ApplicationListener<ContextRefreshedEvent>,重寫onApplicationEvent方法,便可在所有的bean加載完畢后執(zhí)行。
觸發(fā)時機(jī):
在IOC的容器的啟動過程,當(dāng)所有的bean都已經(jīng)處理完成之后,spring ioc容器會有一個發(fā)布ContextRefreshedEvent事件的動作。
示例
/**
* @author
* @date 2021-08-23 16:19
*/
@Component
public class ServerDispatcher implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
// 邏輯代碼
}
}
注意:
系統(tǒng)會存在兩個容器,一個是root application context ,另一個就是我們自己的 projectName-servlet context(作為root application context的子容器)
這種情況下,就會造成onApplicationEvent方法被執(zhí)行兩次。為了避免上面提到的問題,我們可以只在root application context初始化完成后調(diào)用邏輯代碼,其他的容器的初始化完成,則不做任何處理
//root application context 沒有parent
if (event.getApplicationContext().getParent() == null) {
//邏輯代碼
}
總結(jié)
- 一些比較獨立、內(nèi)容小巧的初始化邏輯,不影響springboot啟動速度的使用@PostConstruct注解;
- 若想通過ApplicationListener事件監(jiān)聽的方式,則需要處理好指定的容器。
- 本人建議使用 CommandLineRunner、ApplicationRunner的方式,不會影響服務(wù)的啟動速度 ,處理起來也比較簡單。