Spring配置加载ContextLoaderListener和DispatcherServlet的区别和关系
2016年11月24日


在Spring应用的web.xml里面可以配置ContextLoaderListener和DispatcherServlet:

<context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>
    classpath:spring-main.xml
  </param-value>
</context-param>

<listener>
  <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<servlet>
  <servlet-name>dispatcher</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  <init-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring-servlet.xml</param-value>
  </init-param>
  <load-on-startup>1</load-on-startup>
</servlet>


这种配置,Spring会加载两个ApplicationContext(应用上下文),启动关键日志如下:

13:44:34.759 [localhost-startStop-1] INFO  org.springframework.web.context.ContextLoader:304 - Root WebApplicationContext: initialization started
......
13:44:35.290 [localhost-startStop-1] INFO  org.springframework.web.context.ContextLoader:344 - Root WebApplicationContext: initialization completed in 531 ms
......
13:44:35.327 [localhost-startStop-1] INFO  o.springframework.web.servlet.DispatcherServlet:489 - FrameworkServlet 'dispatcher': initialization started
......
13:44:35.717 [localhost-startStop-1] INFO  o.springframework.web.servlet.DispatcherServlet:508 - FrameworkServlet 'dispatcher': initialization completed in 390 ms
......

如果把Application打印出来,结果如下:

org.springframework.web.context.support.XmlWebApplicationContext:
Root WebApplicationContext: startup date [Wed Jan 24 13:59:44 CST 2018]; root of context hierarchy
......
org.springframework.web.context.support.XmlWebApplicationContext:
WebApplicationContext for namespace 'dispatcher-servlet': startup date [Wed Jan 24 13:59:45 CST 2018]; parent: Root WebApplicationContext

可以看到,Spring启动时,加载了两次 XmlWebApplicationContext,Context是有继承关系的,其中第二次的Context的parent为第一次的Context,获取Context的父级Context的方法是: applicationContext.getParent();


像上面那样,将xml配置分成两个Context加载,会引起一些意外的问题,比如:


1、在spring-main.xml里面配置了 properties文件,但是在第二个Context(spring-servlet.xml)加载时,是使用不了的(比如@Value("${timeout}"))。


2、在spring-main.xml里面 扫描(component-scan)了Controller类,但是在第二个Context(spring-servlet.xml)加载时,是处理不了的。


原因是,每个 ApplicationContext 处理时,它会new一个新的Envirement和PropertyResolver,所以说它用不了其他ApplicationContext 的Properties。其二,它只会处理 自己生成的那些bean,别的bean,它可以使用,但是不会处理,所以说 如果之前的ApplicationContext 已经处理过Controller类了,那么它就不会再进一步处理Controller类了。所以,所有Controller类必须在spring-servlet.xml里面处理,可以再spring-main.xml里面像下面那样配置:

<context:component-scan base-package="com.zollty">  
    <context:exclude-filter type="annotation"  
        expression="org.springframework.stereotype.Controller" />  
</context:component-scan>


总结:将Spring配置拆分成两份ApplicationContext 配置,会带来一些意想不到的副作用,除非对Spring源码非常熟悉,否则不建议这么配置


最简单的配置方法如下(web.xml):

<context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>
    classpath:spring-main.xml,classpath:spring-servlet.xml
  </param-value>
</context-param>

<listener>
  <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<servlet>
  <servlet-name>dispatcher</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  <load-on-startup>1</load-on-startup>
</servlet>

即,将所有配置集中在一个ApplicationContext内,这样就避免了一些奇怪的问题。


参见另一篇文章:

Spring ApplicationListener ContextRefreshedEvent 多次执行问题及源码分析