springboot啟動時執(zhí)行:@PostConstruct 、CommandLineRunner、ApplicationRunner、ApplicationListener對比

使用場景:

我們在開發(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é)

  1. 一些比較獨立、內(nèi)容小巧的初始化邏輯,不影響springboot啟動速度的使用@PostConstruct注解;
  2. 若想通過ApplicationListener事件監(jiān)聽的方式,則需要處理好指定的容器。
  3. 本人建議使用 CommandLineRunner、ApplicationRunner的方式,不會影響服務(wù)的啟動速度 ,處理起來也比較簡單。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容