SpringBoot 源碼解析 (三)—– Spring Boot 精髓:啟動時初始化數據

在我們用 springboot 搭建項目的時候,有時候會碰到在項目啟動時初始化一些操作的需求 ,針對這種需求 spring boot為我們提供了以下幾種方案供我們選擇:

  • ApplicationRunner 與 CommandLineRunner 接口

  • Spring容器初始化時InitializingBean接口和@PostConstruct

  • Spring的事件機制

ApplicationRunner與CommandLineRunner

我們可以實現 ApplicationRunner 或 CommandLineRunner 接口, 這兩個接口工作方式相同,都只提供單一的run方法,該方法在SpringApplication.run(…)完成之前調用,不知道大家還對我上一篇文章結尾有沒有印象,我們先來看看這兩個接口

public interface ApplicationRunner {
    void run(ApplicationArguments var1) throws Exception;
}

public interface CommandLineRunner {
    void run(String... var1) throws Exception;
}

都只提供單一的run方法,接下來我們來看看具體的使用

ApplicationRunner

構造一個類實現ApplicationRunner接口

//需要加入到Spring容器中
@Component public class ApplicationRunnerTest implements ApplicationRunner {

    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println("ApplicationRunner");
    }
}

很簡單,首先要使用@Component將實現類加入到Spring容器中,為什麼要這樣做我們待會再看,然後實現其run方法實現自己的初始化數據邏輯就可以了

CommandLineRunner

對於這兩個接口而言,我們可以通過Order註解或者使用Ordered接口來指定調用順序, @Order() 中的值越小,優先級越高

//需要加入到Spring容器中
@Component
@Order(1)
public class CommandLineRunnerTest implements CommandLineRunner {

    @Override
    public void run(String... args) throws Exception {
        System.out.println("CommandLineRunner...");
    }
}

同樣需要加入到Spring容器中,CommandLineRunner的參數是最原始的參數,沒有進行任何處理,ApplicationRunner的參數是ApplicationArguments,是對原始參數的進一步封裝

源碼分析

大家回顧一下我上一篇文章,也就是的最後一步,這裏我直接把代碼複製過來

private void callRunners(ApplicationContext context, ApplicationArguments args) {
    List<Object> runners = new ArrayList<Object>();
    //獲取容器中所有的ApplicationRunner的Bean實例
    runners.addAll(context.getBeansOfType(ApplicationRunner.class).values()); //獲取容器中所有的CommandLineRunner的Bean實例
    runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
    AnnotationAwareOrderComparator.sort(runners);
    for (Object runner : new LinkedHashSet<Object>(runners)) {
        if (runner instanceof ApplicationRunner) {
            //執行ApplicationRunner的run方法
 callRunner((ApplicationRunner) runner, args);
        }
        if (runner instanceof CommandLineRunner) {
            //執行CommandLineRunner的run方法
 callRunner((CommandLineRunner) runner, args);
        }
    }
}

很明顯,是直接從Spring容器中獲取ApplicationRunner和CommandLineRunner的實例,並調用其run方法,這也就是為什麼我要使用@Component將ApplicationRunner和CommandLineRunner接口的實現類加入到Spring容器中了。

InitializingBean

在spring初始化bean的時候,如果bean實現了 InitializingBean 接口,在對象的所有屬性被初始化后之後才會調用afterPropertiesSet()方法

@Component public class InitialingzingBeanTest implements InitializingBean {

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("InitializingBean..");
    }
}

我們可以看出spring初始化bean肯定會在 ApplicationRunner和CommandLineRunner接口調用之前。

@PostConstruct

@Component public class PostConstructTest {

    @PostConstruct public void postConstruct() {
        System.out.println("init...");
    }
}

我們可以看到,只用在方法上添加@PostConstruct註解,並將類注入到Spring容器中就可以了。我們來看看@PostConstruct註解的方法是何時執行的

在Spring初始化bean時,對bean的實例賦值時,populateBean方法下面有一個initializeBean(beanName, exposedObject, mbd)方法,這個就是用來執行用戶設定的初始化操作。我們看下方法體:

protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {
    if (System.getSecurityManager() != null) {
        AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
            // 激活 Aware 方法
            invokeAwareMethods(beanName, bean);
            return null;
        }, getAccessControlContext());
    }
    else {
        // 對特殊的 bean 處理:Aware、BeanClassLoaderAware、BeanFactoryAware
        invokeAwareMethods(beanName, bean);
    }

    Object wrappedBean = bean;
    if (mbd == null || !mbd.isSynthetic()) {
        // 后處理器
        wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
    }

    try {
        // 激活用戶自定義的 init 方法
 invokeInitMethods(beanName, wrappedBean, mbd);
    }
    catch (Throwable ex) {
        throw new BeanCreationException(
                (mbd != null ? mbd.getResourceDescription() : null),
                beanName, "Invocation of init method failed", ex);
    }
    if (mbd == null || !mbd.isSynthetic()) {
        // 后處理器
        wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
    }
    return wrappedBean;
}

我們看到會先執行后處理器然後執行invokeInitMethods方法,我們來看下applyBeanPostProcessorsBeforeInitialization

public Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName)  
        throws BeansException {  

    Object result = existingBean;  
    for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) { result = beanProcessor.postProcessBeforeInitialization(result, beanName);  
        if (result == null) {  
            return result;  
        }  
    }  
    return result;  
}

public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)  
        throws BeansException {  

    Object result = existingBean;  
    for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) {  
        result = beanProcessor.postProcessAfterInitialization(result, beanName);  
        if (result == null) {  
            return result;  
        }  
    }  
    return result;  
}

獲取容器中所有的後置處理器,循環調用後置處理器的postProcessBeforeInitialization方法,這裏我們來看一個BeanPostProcessor

public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBeanPostProcessor implements InstantiationAwareBeanPostProcessor, BeanFactoryAware, Serializable {
    public CommonAnnotationBeanPostProcessor() {
        this.setOrder(2147483644);
        //設置初始化參數為PostConstruct.class
        this.setInitAnnotationType(PostConstruct.class); this.setDestroyAnnotationType(PreDestroy.class);
        this.ignoreResourceType("javax.xml.ws.WebServiceContext");
    }
    //略...
}

在構造器中設置了一個屬性為PostConstruct.class,再次觀察CommonAnnotationBeanPostProcessor這個類,它繼承自InitDestroyAnnotationBeanPostProcessor。InitDestroyAnnotationBeanPostProcessor顧名思義,就是在Bean初始化和銷毀的時候所作的一個前置/後置處理器。查看InitDestroyAnnotationBeanPostProcessor類下的postProcessBeforeInitialization方法:

public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {  
  LifecycleMetadata metadata = findLifecycleMetadata(bean.getClass());  
   try {  
      metadata.invokeInitMethods(bean, beanName);  
   }  
   catch (InvocationTargetException ex) {  
       throw new BeanCreationException(beanName, "Invocation of init method failed", ex.getTargetException());  
   }  
   catch (Throwable ex) {  
       throw new BeanCreationException(beanName, "Couldn't invoke init method", ex);  
   }  
    return bean;  
}  

private LifecycleMetadata buildLifecycleMetadata(final Class clazz) {  
       final LifecycleMetadata newMetadata = new LifecycleMetadata();  
       final boolean debug = logger.isDebugEnabled();  
       ReflectionUtils.doWithMethods(clazz, new ReflectionUtils.MethodCallback() {  
           public void doWith(Method method) {  
              if (initAnnotationType != null) {  
                   //判斷clazz中的methon是否有initAnnotationType註解,也就是PostConstruct.class註解
                  if (method.getAnnotation(initAnnotationType) != null) { //如果有就將方法添加進LifecycleMetadata中
 newMetadata.addInitMethod(method);  
                     if (debug) {  
                         logger.debug("Found init method on class [" + clazz.getName() + "]: " + method);  
                     }  
                  }  
              }  
              if (destroyAnnotationType != null) {  
                    //判斷clazz中的methon是否有destroyAnnotationType註解
                  if (method.getAnnotation(destroyAnnotationType) != null) {  
                     newMetadata.addDestroyMethod(method);  
                     if (debug) {  
                         logger.debug("Found destroy method on class [" + clazz.getName() + "]: " + method);  
                     }  
                  }  
              }  
           }  
       });  
       return newMetadata;  
} 

在這裡會去判斷某方法是否有PostConstruct.class註解,如果有,則添加到init/destroy隊列中,後續一一執行。@PostConstruct註解的方法會在此時執行,我們接着來看invokeInitMethods

protected void invokeInitMethods(String beanName, final Object bean, @Nullable RootBeanDefinition mbd)
        throws Throwable {

    // 是否實現 InitializingBean // 如果實現了 InitializingBean 接口,則只掉調用bean的 afterPropertiesSet()
    boolean isInitializingBean = (bean instanceof InitializingBean); if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) {
        if (logger.isDebugEnabled()) {
            logger.debug("Invoking afterPropertiesSet() on bean with name '" + beanName + "'");
        }
        if (System.getSecurityManager() != null) {
            try {
                AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () -> {
                    ((InitializingBean) bean).afterPropertiesSet();
                    return null;
                }, getAccessControlContext());
            }
            catch (PrivilegedActionException pae) {
                throw pae.getException();
            }
        }
        else {
            // 直接調用 afterPropertiesSet()
 ((InitializingBean) bean).afterPropertiesSet();
        }
    }

    if (mbd != null && bean.getClass() != NullBean.class) {
        // 判斷是否指定了 init-method(),
        // 如果指定了 init-method(),則再調用制定的init-method
        String initMethodName = mbd.getInitMethodName();
        if (StringUtils.hasLength(initMethodName) &&
                !(isInitializingBean && "afterPropertiesSet".equals(initMethodName)) &&
                !mbd.isExternallyManagedInitMethod(initMethodName)) {
            // 利用反射機制執行
            invokeCustomInitMethod(beanName, bean, mbd);
        }
    }
}

首先檢測當前 bean 是否實現了 InitializingBean 接口,如果實現了則調用其 afterPropertiesSet(),然後再檢查是否也指定了 init-method(),如果指定了則通過反射機制調用指定的 init-method()

我們也可以發現@PostConstruct會在實現 InitializingBean 接口的afterPropertiesSet()方法之前執行

Spring的事件機制

基礎概念

Spring的事件驅動模型由三部分組成

  • 事件: ApplicationEvent ,繼承自JDK的 EventObject ,所有事件都要繼承它,也就是被觀察者
  • 事件發布者: ApplicationEventPublisher 及 ApplicationEventMulticaster 接口,使用這個接口,就可以發布事件了
  • 事件監聽者: ApplicationListener ,繼承JDK的 EventListener ,所有監聽者都繼承它,也就是我們所說的觀察者,當然我們也可以使用註解 @EventListener ,效果是一樣的

事件

在Spring框架中,默認對ApplicationEvent事件提供了如下支持:

  • ContextStartedEvent:ApplicationContext啟動后觸發的事件
  • ContextStoppedEvent:ApplicationContext停止后觸發的事件
  • ContextRefreshedEvent: ApplicationContext初始化或刷新完成后觸發的事件 ;(容器初始化完成后調用,所以我們可以利用這個事件做一些初始化操作)
  • ContextClosedEvent:ApplicationContext關閉后觸發的事件;(如 web 容器關閉時自動會觸發spring容器的關閉,如果是普通 java 應用,需要調用ctx.registerShutdownHook();註冊虛擬機關閉時的鈎子才行) 

構造一個類繼承ApplicationEvent

public class TestEvent extends ApplicationEvent {

    private String message;
    
    public TestEvent(Object source) {
        super(source);
    }

    public void getMessage() {
        System.out.println(message);
    }

    public void setMessage(String message) {
        this.message = message;
    }

}

創建事件監聽者

有兩種方法可以創建監聽者,一種是直接實現ApplicationListener的接口,一種是使用註解 @EventListener , 註解是添加在監聽方法上的 ,下面的例子是直接實現的接口

@Component
public class ApplicationListenerTest implements ApplicationListener<TestEvent> {
    @Override
    public void onApplicationEvent(TestEvent testEvent) {
        testEvent.getMessage();
    }
}

事件發布

對於事件發布,代表者是 ApplicationEventPublisher 和 ApplicationEventMulticaster ,ApplicationContext接口繼承了ApplicationEventPublisher,並在AbstractApplicationContext實現了具體代碼,實際執行是委託給ApplicationEventMulticaster(可以認為是多播)

下面是一個事件發布者的測試實例:

@RunWith(SpringRunner.class)
@SpringBootTest
public class EventTest {
    @Autowired
    private ApplicationContext applicationContext;

    @Test
    public void publishTest() {
        TestEvent testEvent = new TestEvent("");
        testEvent.setMessage("hello world");
       applicationContext.publishEvent(testEvent);
    }
}

利用ContextRefreshedEvent事件進行初始化操作

利用 ContextRefreshedEvent 事件進行初始化,該事件是 ApplicationContext 初始化完成后調用的事件,所以我們可以利用這個事件,對應實現一個 監聽器 ,在其 onApplicationEvent() 方法里初始化操作

@Component public class ApplicationListenerTest implements ApplicationListener<ContextRefreshedEvent> {

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        System.out.println("容器刷新完成后,我被調用了..");
    }
}

 

 

 

 

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

台灣海運大陸貨務運送流程

兩岸物流進出口一站式服務

您可能也會喜歡…