在项目中,会遇到如下情况,即需要在 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 是父容器, Spring MVC 是子容器, 子容器可以访问父容器的 bean,父容器不能访问子容器的 bean
初始化两次,Spring 容器先初始化 bean,MVC 容器再初始化 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 的配置文件分开