七種方式,教你在 SpringBoot 初始化時搞點事情!

我們經常需要在容器啓動的時候做一些鉤子動作,比如註冊消息消費者,監聽配置等,今天就總結下SpringBoot留給開發者的 7 個啓動擴展點。

容器刷新完成擴展點

1、監聽容器刷新完成擴展點ApplicationListener<ContextRefreshedEvent>

基本用法

熟悉Spring的同學一定知道,容器刷新成功意味着所有的Bean初始化已經完成,當容器刷新之後Spring將會調用容器內所有實現了ApplicationListener<ContextRefreshedEvent>BeanonApplicationEvent方法,應用程序可以以此達到監聽容器初始化完成事件的目的。

 1@Component
 2public class StartupApplicationListenerExample implements 
 3  ApplicationListener<ContextRefreshedEvent> {
 4
 5    private static final Logger LOG 
 6      = Logger.getLogger(StartupApplicationListenerExample.class);
 7
 8    public static int counter;
 9
10    @Override public void onApplicationEvent(ContextRefreshedEvent event) {
11        LOG.info("Increment counter");
12        counter++;
13    }
14}
15
16

易錯的點

這個擴展點用在web容器中的時候需要額外注意,在 web 項目中(例如spring mvc),系統會存在兩個容器,一個是root application context, 另一個就是我們自己的context(作爲root application context的子容器)。如果按照上面這種寫法,就會造成onApplicationEvent方法被執行兩次。解決此問題的方法如下:

 1@Component
 2public class StartupApplicationListenerExample implements 
 3  ApplicationListener<ContextRefreshedEvent> {
 4
 5    private static final Logger LOG 
 6      = Logger.getLogger(StartupApplicationListenerExample.class);
 7
 8    public static int counter;
 9
10    @Override public void onApplicationEvent(ContextRefreshedEvent event) {
11        if (event.getApplicationContext().getParent() == null) {
12            // root application context 沒有parent
13            LOG.info("Increment counter");
14            counter++;
15        }
16    }
17}
18
19

高階玩法

當然這個擴展還可以有更高階的玩法:自定義事件,可以藉助Spring以最小成本實現一個觀察者模式:

 1public class NotifyEvent extends ApplicationEvent {
 2    private String email;
 3    private String content;
 4    public NotifyEvent(Object source) {
 5        super(source);
 6    }
 7    public NotifyEvent(Object source, String email, String content) {
 8        super(source);
 9        this.email = email;
10        this.content = content;
11    }
12    // 省略getter/setter方法
13}
14
15
 1@Component
 2public class NotifyListener implements ApplicationListener<NotifyEvent> {
 3
 4    @Override
 5    public void onApplicationEvent(NotifyEvent event) {
 6        System.out.println("郵件地址:" + event.getEmail());
 7        System.out.println("郵件內容:" + event.getContent());
 8    }
 9}
10
11
 1@RunWith(SpringRunner.class)
 2@SpringBootTest
 3public class ListenerTest {
 4    @Autowired
 5    private WebApplicationContext webApplicationContext;
 6
 7    @Test
 8    public void testListener() {
 9        NotifyEvent event = new NotifyEvent("object", "abc@qq.com", "This is the content");
10        webApplicationContext.publishEvent(event);
11    }
12}
13
14

2、SpringBootCommandLineRunner接口

當容器上下文初始化完成之後,SpringBoot也會調用所有實現了CommandLineRunner接口的run方法,下面這段代碼可起到和上文同樣的作用:

 1@Component
 2public class CommandLineAppStartupRunner implements CommandLineRunner {
 3    private static final Logger LOG =
 4      LoggerFactory.getLogger(CommandLineAppStartupRunner.class);
 5
 6    public static int counter;
 7
 8    @Override
 9    public void run(String...args) throws Exception {
10        LOG.info("Increment counter");
11        counter++;
12    }
13}
14
15

對於這個擴展點的使用有額外兩點需要注意:

1// 從控制檯輸入參數示例
2java -jar CommandLineAppStartupRunner.jar abc abcd
3
4

3、SpringBootApplicationRunner接口

這個擴展和SpringBootCommandLineRunner接口的擴展類似,只不過接受的參數是一個ApplicationArguments類,對控制檯輸入的參數提供了更好的封裝,以--開頭的被視爲帶選項的參數,否則是普通的參數

 1@Component
 2public class AppStartupRunner implements ApplicationRunner {
 3    private static final Logger LOG =
 4      LoggerFactory.getLogger(AppStartupRunner.class);
 5
 6    public static int counter;
 7
 8    @Override
 9    public void run(ApplicationArguments args) throws Exception {
10        LOG.info("Application started with option names : {}", 
11          args.getOptionNames());
12        LOG.info("Increment counter");
13        counter++;
14    }
15}
16
17

比如:

1java -jar CommandLineAppStartupRunner.jar abc abcd --autho=mark verbose
2
3

Bean初始化完成擴展點

前面的內容總結了針對容器初始化的擴展點,在有些場景,比如監聽消息的時候,我們希望Bean初始化完成之後立刻註冊監聽器,而不是等到整個容器刷新完成,Spring針對這種場景同樣留足了擴展點:

1、@PostConstruct註解

@PostConstruct註解一般放在Bean的方法上,被@PostConstruct修飾的方法會在Bean初始化後馬上調用:

 1@Component
 2public class PostConstructExampleBean {
 3
 4    private static final Logger LOG 
 5      = Logger.getLogger(PostConstructExampleBean.class);
 6
 7    @Autowired
 8    private Environment environment;
 9
10    @PostConstruct
11    public void init() {
12        LOG.info(Arrays.asList(environment.getDefaultProfiles()));
13    }
14}
15
16

2、 InitializingBean接口

InitializingBean的用法基本上與@PostConstruct一致,只不過相應的Bean需要實現afterPropertiesSet方法

 1@Component
 2public class InitializingBeanExampleBean implements InitializingBean {
 3
 4    private static final Logger LOG 
 5      = Logger.getLogger(InitializingBeanExampleBean.class);
 6
 7    @Autowired
 8    private Environment environment;
 9
10    @Override
11    public void afterPropertiesSet() throws Exception {
12        LOG.info(Arrays.asList(environment.getDefaultProfiles()));
13    }
14}
15
16

3、@Bean註解的初始化方法

通過@Bean注入Bean的時候可以指定初始化方法:

Bean的定義

 1public class InitMethodExampleBean {
 2
 3    private static final Logger LOG = Logger.getLogger(InitMethodExampleBean.class);
 4
 5    @Autowired
 6    private Environment environment;
 7
 8    public void init() {
 9        LOG.info(Arrays.asList(environment.getDefaultProfiles()));
10    }
11}
12
13
14

Bean注入

1@Bean(initMethod="init")
2public InitMethodExampleBean initMethodExampleBean() {
3    return new InitMethodExampleBean();
4}
5
6

4、通過構造函數注入

Spring也支持通過構造函數注入,我們可以把搞事情的代碼寫在構造函數中,同樣能達到目的

 1@Component 
 2public class LogicInConstructorExampleBean {
 3
 4    private static final Logger LOG 
 5      = Logger.getLogger(LogicInConstructorExampleBean.class);
 6
 7    private final Environment environment;
 8
 9    @Autowired
10    public LogicInConstructorExampleBean(Environment environment) {
11        this.environment = environment;
12        LOG.info(Arrays.asList(environment.getDefaultProfiles()));
13    }
14}
15
16

Bean初始化完成擴展點執行順序?

可以用一個簡單的測試:

 1@Component
 2@Scope(value = "prototype")
 3public class AllStrategiesExampleBean implements InitializingBean {
 4
 5    private static final Logger LOG 
 6      = Logger.getLogger(AllStrategiesExampleBean.class);
 7
 8    public AllStrategiesExampleBean() {
 9        LOG.info("Constructor");
10    }
11
12    @Override
13    public void afterPropertiesSet() throws Exception {
14        LOG.info("InitializingBean");
15    }
16
17    @PostConstruct
18    public void postConstruct() {
19        LOG.info("PostConstruct");
20    }
21
22    public void init() {
23        LOG.info("init-method");
24    }
25}
26
27

實例化這個Bean後輸出:

1[main] INFO o.b.startup.AllStrategiesExampleBean - Constructor
2[main] INFO o.b.startup.AllStrategiesExampleBean - PostConstruct
3[main] INFO o.b.startup.AllStrategiesExampleBean - InitializingBean
4[main] INFO o.b.startup.AllStrategiesExampleBean - init-method
5
6
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/68Wq3-7xJrB99yIAzhI6WA