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

大家好,我是躍哥。清明假期已經悄然過去,相比往常就是多了週一這一天的休息,所以大家適應起來應該還蠻快的吧。


爲了讓大家更好的適應工作,今天直接來主題,和大家聊聊 SpringBoot 初始化的那些事,衝鴨。


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

容器刷新完成擴展點

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

基本用法

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

@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++;
    }
}

易錯的點

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

@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) {
        if (event.getApplicationContext().getParent() == null) {
            // root application context 沒有parent
            LOG.info("Increment counter");
            counter++;
        }
    }
}

高階玩法

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

  • 先自定義一個事件:
public class NotifyEvent extends ApplicationEvent {
    private String email;
    private String content;
    public NotifyEvent(Object source) {
        super(source);
    }
    public NotifyEvent(Object source, String email, String content) {
        super(source);
        this.email = email;
        this.content = content;
    }
    // 省略getter/setter方法
}
  • 註冊一個事件監聽器
@Component
public class NotifyListener implements ApplicationListener<NotifyEvent{

    @Override
    public void onApplicationEvent(NotifyEvent event) {
        System.out.println("郵件地址:" + event.getEmail());
        System.out.println("郵件內容:" + event.getContent());
    }
}
  • 發佈事件
@RunWith(SpringRunner.class)
@SpringBootTest
public class ListenerTest 
{
    @Autowired
    private WebApplicationContext webApplicationContext;

    @Test
    public void testListener() {
        NotifyEvent event = new NotifyEvent("object""[email protected]""This is the content");
        webApplicationContext.publishEvent(event);
    }
}
  • 執行單元測試可以看到郵件的地址和內容都被打印出來了

2、SpringBootCommandLineRunner接口

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

@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++;
    }
}

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

  • 多個實現了 CommandLineRunnerBean的執行順序可以根據 Bean上的 @Order註解調整
  • run方法可以接受從控制檯輸入的參數,跟 ApplicationListener<ContextRefreshedEvent>這種擴展相比,更加靈活
// 從控制檯輸入參數示例
java -jar CommandLineAppStartupRunner.jar abc abcd

3、SpringBootApplicationRunner接口

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

@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++;
    }
}

比如:

java -jar CommandLineAppStartupRunner.jar abc abcd --autho=mark verbose

Bean初始化完成擴展點

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

1、@PostConstruct註解

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

@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()));
    }
}

2、 InitializingBean接口

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

@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()));
    }
}

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

通過@Bean注入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(initMethod="init")
public InitMethodExampleBean initMethodExampleBean() {
    return new InitMethodExampleBean();
}

4、通過構造函數注入

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

@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()));
    }
}

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-method






0、重磅!兩萬字長文總結,梳理 Java 入門進階哪些事(推薦收藏)

1、都退稅了嗎?和你聊聊發工資的騷操作。。

2、想蛻變,就必須翻過算法這座山

本文分享自微信公衆號 - 程序員小躍(runningdimple)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章