关于Spring InitialzationBean遇到的坑及分析思考

4,346 阅读3分钟

背景

在项目中,会遇到如下情况,即需要在 Tomcat 启动时去执行一些操作,首先我们想到的是继承 ServletContextListener,然后在 contextInitialized 加入需要执行的操作,这是一种方法;那么对于 Spring 项目来说,也可以继承 InitialzationBean 来实现,在初始化 bean 和销毁 bean 的时候执行某个方法,由于 ServletContextListener 需要在 web.xml 中进行配置,而且可能要注入其他 bean,所以笔者选择了继承 InitialzationBean 来实现。

遇到的坑

新建一个类,继承 InitialzationBean,代码如下:

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?带着这种猜测,又进行了如下验证:

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,看到这里,茅塞顿开:

第一次是 Spring 对 Bean 进行了初始化,第二次是 Spring MVC 又对 Bean 进行了初始化

那么如何解决加载两次对问题呢?那就是让 Spring MVC 只扫描 @Controller 注解,配置如下:

<!-- 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 的配置文件分开呢?

我们用以下代码进行测试:

@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,父容器不能访问子容器的 bean

  • 单例的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 的配置文件分开