前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从ContextLoaderListener谈Spring父子容器

从ContextLoaderListener谈Spring父子容器

作者头像
zhangheng
发布2020-04-29 14:22:21
9160
发布2020-04-29 14:22:21
举报

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

现象

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

代码语言:javascript
复制
<!-- 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的过程

演示

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

接口

代码语言:javascript
复制
public interface TestInterface {
}

上下文1

代码语言:javascript
复制
//ApplicationOne.java
public class ApplicationOne {
    public ApplicationOne() {
        System.out.println("ApplicationOne.init");
    }

    public void test(){
        System.out.println("ApplicationOne.test");
    }
}
代码语言:javascript
复制
//ApplicationAopTimeHandler.java
public class ApplicationAopTimeHandler {
    public void printTimeBefore() {
        System.out.println("\nCurrentTime = " + System.currentTimeMillis());
    }

    public void printTimeAfter() {
        System.out.println("CurrentTime = " + System.currentTimeMillis() + "\n");
    }
}
代码语言:javascript
复制
//TestInterfaceOne.java
public class TestInterfaceOne implements TestInterface {
}
代码语言:javascript
复制
//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

代码语言:javascript
复制
//ApplicationTwo.java
public class ApplicationTwo {
    public ApplicationTwo() {
        System.out.println("ApplicationTwo.init");
    }

    public void test(){
        System.out.println("ApplicationTwo.test");
    }
}
代码语言:javascript
复制
//TestInterfaceTwo.java
public class TestInterfaceTwo implements TestInterface {
}
代码语言:javascript
复制
<?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>

测试主类

代码语言:javascript
复制
//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可不可以跨容器传递,接口可不可以跨容器传递。

结果如下:

代码语言:javascript
复制
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?

](https://stackoverflow.com/questions/9016122/contextloaderlistener-or-not)

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

代码语言:javascript
复制
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时运行。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018-04-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 现象
  • 演示
  • 结论
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档