關於Spring InitialzationBean遇到的坑及分析思考

背景

在項目中,會遇到以下狀況,即須要在 Tomcat 啓動時去執行一些操做,首先咱們想到的是繼承 ServletContextListener,而後在 contextInitialized 加入須要執行的操做,這是一種方法;那麼對於 Spring 項目來講,也能夠繼承 InitialzationBean 來實現,在初始化 bean 和銷燬 bean 的時候執行某個方法,因爲 ServletContextListener 須要在 web.xml 中進行配置,並且可能要注入其餘 bean,因此筆者選擇了繼承 InitialzationBean 來實現。java

遇到的坑

新建一個類,繼承 InitialzationBean,代碼以下:web

import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;

@Component
public class DoOnStart implements InitializingBean {
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("xxxxxxxx");
    }
}
複製代碼

本覺得這樣就 OK 了,啓動 Tomcat 後發現,afterPropertiesSet 方法被執行了兩次,奇怪,難道 Spring 會初始化兩次 Bean?帶着這種猜想,又進行了以下驗證:spring

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class DoOnStart implements InitializingBean, ApplicationContextAware {
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("xxxxxxxx");
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        System.out.println("xxxxxxxx");
    }
}
複製代碼

經過 Debug 發現,setApplicationContext 方法確實執行了兩次,也就是說,有兩個容器被初始化了,經過查看 applicationContext 發現,第一次是 Root WebApplicationContext,第二次是 WebApplicationContext for namespace spring-servlet,看到這裏,茅塞頓開:express

第一次是 Spring 對 Bean 進行了初始化,第二次是 Spring MVC 又對 Bean 進行了初始化mvc

那麼如何解決加載兩次對問題呢?那就是讓 Spring MVC 只掃描 @Controller 註解,配置以下:app

<!-- spring 配置文件-->  
<context:component-scan base-package="com.xxx.xxx">  
     <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller" />  
</context:component-scan>  
  
<!-- spring mvc -->     
<context:component-scan base-package="com.xxx.xxx.web" use-default-filters="false">  
    <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" />  
</context:component-scan>  
複製代碼

爲何要將 Spring 的配置文件和 Spring MVC 的配置文件分開呢?ide

咱們用如下代碼進行測試:源碼分析

@Service  
public class DoOnStart implements InitializingBean {   
    @Autowired  
    private XXXController xxxController;  
  
    @Override  
    public void afterPropertiesSet() throws Exception {  
        System.out.println("xxxxxxxx");  
    }  
}  
複製代碼

有以下狀況:測試

  • Spring 加載所有 bean,MVC 加載 Controller
    • 能夠
  • Spring 加載所有 bean,MVC 容器啥也不加載
    • 能夠
  • Spring 加載全部除了 Controller 的 bean,MVC 只加載 Controller
    • 不能夠,父容器不能訪問子容器的 bean
  • Spring 不加載 bean,MVC 加載全部的 bean
    • 能夠

原來 Spring 是父容器, Spring MVC 是子容器, 子容器能夠訪問父容器的 bean,父容器不能訪問子容器的 beanspa

  • 單例的bean在父子容器中存在一個實例仍是兩個實例?

初始化兩次,Spring 容器先初始化 bean,MVC 容器再初始化 bean,因此應該是兩個 bean

  • 爲啥不把全部 bean 都在子容器中掃描?

缺點是不利於擴展

源碼分析

經過查看 Spring 的加載 bean 的源碼類 AbstractAutowireCapableBeanFactory 可看出其中奧妙,AbstractAutowireCapableBeanFactory 類中的 invokeInitMethods 講解的很是清楚,源碼以下:

protected void invokeInitMethods(String beanName, final Object bean, RootBeanDefinition mbd) throws Throwable {  
  //判斷該bean是否實現了實現了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(new PrivilegedExceptionAction<Object>() {  
                  public Object run() throws Exception {  
                      //直接調用afterPropertiesSet 
                      ((InitializingBean) bean).afterPropertiesSet();  
                      return null;  
                  }  
              },getAccessControlContext());  
          } catch (PrivilegedActionException pae) {  
              throw pae.getException();  
          }  
      }                  
      else {  
          //直接調用afterPropertiesSet 
          ((InitializingBean) bean).afterPropertiesSet();  
      }  
  }  
  if (mbd != null) {  
      String initMethodName = mbd.getInitMethodName();  
      //判斷是否指定了init-method方法,若是指定了init-method方法,則再調用制定的init-method 
      if (initMethodName != null && !(isInitializingBean && "afterPropertiesSet".equals(initMethodName)) &&  
              !mbd.isExternallyManagedInitMethod(initMethodName)) {  
              //進一步查看該方法的源碼,能夠發現init-method方法中指定的方法是經過反射實現 
          invokeCustomInitMethod(beanName, bean, mbd);  
      }  
  }  
複製代碼

總結

  • Spring 爲 bean 提供了兩種初始化 bean 的方式,實現 InitializingBean 接口,實現 afterPropertiesSet 方法,或者在配置文件中經過 init-method 指定,兩種方式能夠同時使用

  • 實現 InitializingBean 接口是直接調用 afterPropertiesSet 方法,比經過反射調用 init-method 指定的方法效率相對來講要高點。可是 init-method 方式消除了對 Spring 的依賴

  • 若是調用 afterPropertiesSet 方法時出錯,則不調用 init-method 指定的方法

  • 要將 Spring 的配置文件和 Spring MVC 的配置文件分開

相關文章
相關標籤/搜索