1. 概述
在本教程中,我們將重點介紹如何在 Spring 應用程序的啓動時執行邏輯。
2. 在啓動時執行邏輯
在 Spring 應用程序啓動期間或之後執行邏輯是一種常見的場景。但它也常常導致多種問題。
為了從反控制原則中獲益,我們需要放棄對應用程序流程的某些控制權,特別是啓動時實例化、設置邏輯等,需要特別關注。
我們不能簡單地將我們的邏輯包含在 Bean 的構造函數中,也不能在實例化任何對象之後調用方法,因為在這些過程中我們無法控制流程。
下面來看一個實際的例子:
@Component
public class InvalidInitExampleBean {
@Autowired
private Environment env;
public InvalidInitExampleBean() {
env.getActiveProfiles();
}
}在這裏,我們試圖在構造函數中訪問一個 自動注入 字段。當構造函數被調用時,Spring Bean 尚未完全初始化。這會導致問題,因為 訪問尚未初始化字段會導致 NullPointerExceptions.
下面我們將探討 Spring 提供的幾種處理這種情況的方法。
2.1. @PostConstruct 註解
我們可以使用 Javax 的 @PostConstruct 註解來標註一個在 Bean 初始化後立即執行的方法。請注意,Spring 即使沒有需要注入的依賴,也會運行標註的方法。
以下是 @PostConstruct 註解的實際應用:
@Component
public class PostConstructExampleBean {
private static final Logger LOG
= Logger.getLogger(PostConstructExampleBean.class);
@Autowired
private Environment environment;
@PostConstruct
public void init() {
LOG.info(Arrays.asList(environment.getDefaultProfiles()));
}
}我們能看到,Environment 實例安全地注入,然後在帶有 @PostConstruct 註解的方法中被調用,而沒有拋出 NullPointerException。
2.2. <em>InitializingBean</em> 接口
採用 <em>InitializingBean</em> 方法的工作方式與之前類似。 區別在於,我們無需註解方法,而是需要實現 <em>InitializingBean</em> 接口以及 <em>afterPropertiesSet()</em> 方法。
以下是如何使用 <em>InitializingBean</em> 接口實現之前的示例:
@Component
public class InitializingBeanExampleBean implements InitializingBean {
private static final Logger LOG
= Logger.getLogger(InitializingBeanExampleBean.class);
@Autowired
private Environment environment;
@Override
public void afterPropertiesSet() throws Exception {
LOG.info(Arrays.asList(environment.getDefaultProfiles()));
}
}2.3. ApplicationListener
我們可以使用這種方法在 Spring 容器初始化完成後
為了實現這一點,我們需要創建一個實現
@Component
public class StartupApplicationListenerExample implements
ApplicationListener<ContextRefreshedEvent> {
private static final Logger LOG
= Logger.getLogger(StartupApplicationListenerExample.class);
public static int counter;
@Override public void onApplicationEvent(ContextRefreshedEvent event) {
LOG.info("Increment counter");
counter++;
}
}
我們可以通過使用新引入的 @EventListener 註解來獲得相同的結果:
@Component
public class EventListenerExampleBean {
private static final Logger LOG
= Logger.getLogger(EventListenerExampleBean.class);
public static int counter;
@EventListener
public void onApplicationEvent(ContextRefreshedEvent event) {
LOG.info("Increment counter");
counter++;
}
}我們希望選擇一個適合我們需求的事件。在本例中,我們選擇了 ContextRefreshedEvent。
2.4. @Bean initMethod 屬性
我們可以使用 initMethod 屬性在 Bean 初始化之後運行一個方法。
以下是一個 Bean 的示例:
public class InitMethodExampleBean {
private static final Logger LOG = Logger.getLogger(InitMethodExampleBean.class);
@Autowired
private Environment environment;
public void init() {
LOG.info(Arrays.asList(environment.getDefaultProfiles()));
}
}請注意,我們尚未實現任何特殊接口,也沒有使用任何特殊註解。
然後,我們可以使用 @Bean 註解來定義 Bean。
@Bean(initMethod="init")
public InitMethodExampleBean initMethodExampleBean() {
return new InitMethodExampleBean();
}以下是 Bean 定義在 XML 配置中的外觀:
<bean id="initMethodExampleBean"
class="com.baeldung.startup.InitMethodExampleBean"
init-method="init">
</bean>2.5. 構造函數注入
如果使用構造函數注入來注入字段,我們可以簡單地將邏輯包含在構造函數中:
@Component
public class LogicInConstructorExampleBean {
private static final Logger LOG
= Logger.getLogger(LogicInConstructorExampleBean.class);
private final Environment environment;
@Autowired
public LogicInConstructorExampleBean(Environment environment) {
this.environment = environment;
LOG.info(Arrays.asList(environment.getDefaultProfiles()));
}
}2.6. Spring Boot <em>CommandLineRunner</em>
Spring Boot 提供了一個 <em>CommandLineRunner</em> 接口,該接口包含一個回調 <em>run()</em> 方法。該方法可以在 Spring 應用上下文實例化後,在應用程序啓動時被調用。
讓我們來看一個例子:
@Component
public class CommandLineAppStartupRunner implements CommandLineRunner {
private static final Logger LOG =
LoggerFactory.getLogger(CommandLineAppStartupRunner.class);
public static int counter;
@Override
public void run(String...args) throws Exception {
LOG.info("Increment counter");
counter++;
}
}注意:如文檔 所述,在同一應用程序上下文中可以定義多個 CommandLineRunner Bean,並且可以使用 @Ordered 接口或 @Order 註解對其進行排序。
2.7. Spring Boot <em ApplicationRunner</em>
類似於 <em CommandLineRunner</em>>, Spring Boot 還提供了一個 <em ApplicationRunner</em> 接口,該接口包含一個 <em run()</em> 方法,在應用程序啓動時被調用。但是,與將原始 <em String</em> 類型的參數傳遞到回調方法不同,我們有 `<em ApplicationArguments 類的實例。
<em ApplicationArguments</em> 接口提供了獲取選項值和普通參數值的各種方法。以 -- 開頭的參數是一個選項參數。
以下是一個示例:
@Component
public class AppStartupRunner implements ApplicationRunner {
private static final Logger LOG =
LoggerFactory.getLogger(AppStartupRunner.class);
public static int counter;
@Override
public void run(ApplicationArguments args) throws Exception {
LOG.info("Application started with option names : {}",
args.getOptionNames());
LOG.info("Increment counter");
counter++;
}
}3. 組合機制
為了對我們的 Bean 擁有完全的控制權,我們可以將上述機制組合在一起。
這是執行順序:
- 構造函數
- 帶有 @PostConstruct 註解的方法
- InitializingBean 的 afterPropertiesSet() 方法
- 在 XML 中指定的 init-method 方法
讓我們創建一個組合所有機制的 Spring Bean:
@Component
@Scope(value = "prototype")
public class AllStrategiesExampleBean implements InitializingBean {
private static final Logger LOG
= Logger.getLogger(AllStrategiesExampleBean.class);
public AllStrategiesExampleBean() {
LOG.info("Constructor");
}
@Override
public void afterPropertiesSet() throws Exception {
LOG.info("InitializingBean");
}
@PostConstruct
public void postConstruct() {
LOG.info("PostConstruct");
}
public void init() {
LOG.info("init-method");
}
}如果嘗試實例化這個 Bean,我們可以看到按照上述指定順序記錄的日誌:
[main] INFO o.b.startup.AllStrategiesExampleBean - Constructor
[main] INFO o.b.startup.AllStrategiesExampleBean - PostConstruct
[main] INFO o.b.startup.AllStrategiesExampleBean - InitializingBean
[main] INFO o.b.startup.AllStrategiesExampleBean - init-method4. 結論
在本文中,我們展示了多種在 Spring 應用啓動時運行邏輯的方法。