从ContextLoaderListener谈Spring父子容器

SSM开发网上一找一大堆,可是有时候深入使用一下问题还不少,今天就来看看ContextLoaderListener配置带来的问题。

现象

在使用springmvc开发时,我们经常在web.xml中的配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<!-- Spring -->
<!-- 配置Spring配置文件路径 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath*:spring/spring-config-*.xml
classpath:other.xml
</param-value>
</context-param>
<!-- 配置Spring上下文监听器 -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<!-- Spring MVC 核心控制器 DispatcherServlet 配置 -->
<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/spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<!--不拦截jsp-->
<url-pattern>/*</url-pattern>
<!-- 拦截所有/* 的请求,交给DispatcherServlet处理,性能最好 -->
<!-- <url-pattern>/*</url-pattern> -->
</servlet-mapping>

我们会使用ContextLoaderListener加载一部分service/dao/component之类的bean,然后再在servlet中加载controller。

以上配置会有什么问题呢?

首先我们得知道,tomcat启动后首先会实例化初始化ContextLoaderListener,创建出一个上下文容器。随后实例化初始化servlet,调用init方法,在springmvc的DispatcherServelt父类中,会将ContextLoaderListener中上下文作为servlet中上下文的父容器。如果读者不了解bean的生成过程,建议先阅读 Spring生成bean的过程

演示

为了更加简明的展示出父子容器带来的问题。这里我将创建几个简单的类来模拟上述行为。

接口

1
2
public interface TestInterface {
}

上下文1

1
2
3
4
5
6
7
8
9
10
//ApplicationOne.java
public class ApplicationOne {
public ApplicationOne() {
System.out.println("ApplicationOne.init");
}

public void test(){
System.out.println("ApplicationOne.test");
}
}
1
2
3
4
5
6
7
8
9
10
//ApplicationAopTimeHandler.java
public class ApplicationAopTimeHandler {
public void printTimeBefore() {
System.out.println("\nCurrentTime = " + System.currentTimeMillis());
}

public void printTimeAfter() {
System.out.println("CurrentTime = " + System.currentTimeMillis() + "\n");
}
}
1
2
3
//TestInterfaceOne.java
public class TestInterfaceOne implements TestInterface {
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//applicationContextOne.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">

<bean id="app1" class="ApplicationOne" />
<bean id="timeHandler" class="ApplicationAopTimeHandler" />

<bean id="interface1" class="TestInterfaceOne" />

<aop:config>
<aop:aspect id="time" ref="timeHandler">
<aop:pointcut id="addAllMethod" expression="execution(* *.*(..))" />
<aop:before method="printTimeBefore" pointcut-ref="addAllMethod" />
<aop:after method="printTimeAfter" pointcut-ref="addAllMethod" />
</aop:aspect>
</aop:config>
</beans>

上下文2

1
2
3
4
5
6
7
8
9
10
//ApplicationTwo.java
public class ApplicationTwo {
public ApplicationTwo() {
System.out.println("ApplicationTwo.init");
}

public void test(){
System.out.println("ApplicationTwo.test");
}
}

1
2
3
//TestInterfaceTwo.java
public class TestInterfaceTwo implements TestInterface {
}
1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd">

<bean id="app2" class="ApplicationTwo" />

<bean id="interface2" class="TestInterfaceTwo" />
</beans>

测试主类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//ApplicationContextTest.java
public class ApplicationContextTest {
public static void main(String[] args) {
ApplicationContext ctx1 =
new ClassPathXmlApplicationContext("applicationContextOne.xml");

System.out.println("创建上下文1完成\n");

String[] names = ctx1.getBeanNamesForType(TestInterface.class);
System.out.println("类型实例数"+names.length);
System.out.println("名称"+names[0]+"\n");


ApplicationContext ctx2 = new ClassPathXmlApplicationContext(new String[]{"applicationContextTwo.xml"}, ctx1);

System.out.println("创建上下文2完成\n");

System.out.println("测试拦截器效果\n");
ApplicationOne app1 = (ApplicationOne) ctx1.getBean("app1");
app1.test();

ApplicationTwo app2 = (ApplicationTwo) ctx2.getBean("app2");
app2.test();



names = ctx2.getBeanNamesForType(TestInterface.class);
System.out.println("类型实例数"+names.length);
System.out.println("名称"+names[0]);

}
}

在上边的代码中,我们要模拟什么呢?

上下文1 拥有接口的实现类,拥有一个打印类,还有一个用给这个类做aop的类。

上下文2 拥有接口的另一个实现类,拥有另一个打印类。

上下文1作为上下文2的父容器,看看两个容器之间有没有隔阂,aop可不可以跨容器传递,接口可不可以跨容器传递。

结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ApplicationOne.init
创建上下文1完成

类型实例数1
名称interface1

ApplicationTwo.init
创建上下文2完成

测试拦截器效果


CurrentTime = 1522921095986
ApplicationOne.test
CurrentTime = 1522921096200

ApplicationTwo.test
类型实例数1
名称interface2

结论

以上结果说明了什么?

  1. 如果在父类配置aop,不会对子容器起效果。(经测试,在子容器配置aop,不会对父容器起效果)
  2. 如果在两个容器中都有接口的实现类,在一个上下文中根据接口获取所有实现类bean,则只能获取这个上下文的实现类bean,不能获取所有的bean。
  3. 经测试,如果在子容器中配置了父容器的配置文件,则会重新构建父容器的beanDefinition,重新初始化父容器的bean。

笔者曾经也有这样的疑问,之前在ContextLoaderListener下的配置文件中设置了aop,但是发现对controller不起效果。google一下,查到了这篇问答ContextLoaderListener or not?

其中有有一个回答比较全,贴在这里供大家参考。

1
2
3
4
5
6
7
8
9
10
11
12
13
In your case, no, there's no reason to keep the ContextLoaderListener and applicationContext.xml. If your app works fine with just the servlet's context, that stick with that, it's simpler.

Yes, the generally-encouraged pattern is to keep non-web stuff in the webapp-level context, but it's nothing more than a weak convention.

The only compelling reasons to use the webapp-level context are:

If you have multiple DispatcherServlet that need to share services
If you have legacy/non-Spring servlets that need access to Spring-wired services
If you have servlet filters that hook into the webapp-level context (e.g. Spring Security's DelegatingFilterProxy, OpenEntityManagerInViewFilter, etc)

None of these apply to you, so the extra complexity is unwarranted.

Just be careful when adding background tasks to the servlet's context, like scheduled tasks, JMS connections, etc. If you forget to add <load-on-startup> to your web.xml, then these tasks won't be started until the first access of the servlet.

翻译如下:

如果只使用servlet的上下文就可以正常工作,没有理由保留ContextLoaderListener。

在web开发中,普遍鼓励在ContextLoaderListener的上下文中保留非web内容,但它只不过是一个小小的建议。

使用webapp级别上下文的唯一令人信服的理由是:

  1. 如果有多个需要共享服务的DispatcherServlet。
  2. 如果有非Spring Servlet访问spring服务时。
  3. 如果有servlet过滤器挂钩到webapp级别的上下文中(例如Spring Security的DelegatingFilterProxy,OpenEntityManagerInViewFilter等)

如果你没有以上这三种需求,那就没有必要把问题搞复杂。

如果将后台任务添加到servlet的上下文中时,请注意,比如计划任务,JMS连接等。如果忘记将<load-on-startup>添加到web.xml中,那么这些任务将在第一次初始化该servlet时运行。