【spring】ContextLoaderListener和DispatcherServlet区别(contextConfigLocation、contextClass参数)&父子容器 web.xml


Spring MVC的web.xml配置详解 完整详细的配置
ContextLoaderListener和DispatcherServlet区别(contextConfigLocation、contextClass参数)&父子容器 web.xml 解释ContextLoaderListener和DispatcherServlet区别
DispatcherServlet详解DispatcherServlet是请求入口,覆盖了springmvc的整个流程
spring容器和springmvc容器,以及web容器的关系

前言

在spring和springmvc进行整合的时候有2个容器,为了区分,称之为父子容器。

  • DispatcherServlet对应子容器,又称springmvc容器。负责解析视图、HandlerMapping、HandlerAdapter、和@Controller标签,即spring mvc独有的功能。
  • ContextLoaderListener对应父容器,又称spring容器。是IOC的核心功能,提供解析IOC标签功能,包括@Component、@Service、@Repository等

2个容器的区别和联系 参见 spring容器和springmvc容器,以及web容器的关系

这里有个常见注意事项,即@Controller标识的类既会被父容器解析,也会被子容器解析,如果在构造函数加打印日志的话,你会发现打印2遍。但是父容器虽然解析了,但是不会建立与RequestMapping的映射关系,只有子容器能完成该功能,因此,当你配置出错,仅在父容器配置扫描路径的话,会导致url访问出现404。

1. 父子容器概念

在spring和springmvc进行整合的时候,一般情况下我们会使用不同的配置文件来配置spring和springmvc,因此我们的应用中会存在至少2个ApplicationContext实例,由于是在web应用中,因此最终实例化的是ApplicationContext的子接口WebApplicationContext。如下图所示:
在这里插入图片描述
上图中显示了2个WebApplicationContext实例,为了进行区分,分别称之为:Servlet WebApplicationContext、Root WebApplicationContext。 其中:

  • Servlet WebApplicationContext:这是对J2EE三层架构中的web层进行配置,如控制器(controller)、视图解析器(view resolvers)等相关的bean。通过spring mvc中提供的DispatcherServlet来加载配置,通常情况下,配置文件的名称为spring-servlet.xml。
  • Root WebApplicationContext:这是对J2EE三层架构中的service层、dao层进行配置,如业务bean,数据源(DataSource)等。通常情况下,配置文件的名称为applicationContext.xml。在web应用中,其一般通过ContextLoaderListener来加载

以下是一个web.xml配置案例:

<?xml version="1.0" encoding="UTF-8"?>  
  
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"  
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaeehttp://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">  
    
    <!—创建Root WebApplicationContext-->
    <context-param>  
        <param-name>contextConfigLocation</param-name>  
        <param-value>/WEB-INF/spring/applicationContext.xml</param-value>  
    </context-param>  
  
    <listener>  
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>  
    </listener>  
    
    <!—创建Servlet WebApplicationContext-->
    <servlet>  
        <servlet-name>dispatcher</servlet-name>  
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>  
        <init-param>  
            <param-name>contextConfigLocation</param-name>  
            <param-value>/WEB-INF/spring/spring-servlet.xml</param-value>  
        </init-param>  
        <load-on-startup>1</load-on-startup>  
    </servlet>  
    <servlet-mapping>  
        <servlet-name>dispatcher</servlet-name>  
        <url-pattern>/*</url-pattern>  
    </servlet-mapping>  
  
</web-app>

在上面的配置中:

  • ContextLoaderListener会被优先初始化时,其会根据<context-param>元素中contextConfigLocation参数指定的配置文件路径,在这里就是"/WEB-INF/spring/applicationContext.xml”,来创建WebApplicationContext实例。

    并调用ServletContext的setAttribute方法,将其设置到ServletContext中,属性的key为”org.springframework.web.context.WebApplicationContext.ROOT”,最后的”ROOT"字样表明这是一个 Root WebApplicationContext。

  • DispatcherServlet在初始化时,会根据<init-param>元素中contextConfigLocation参数指定的配置文件路径,即"/WEB-INF/spring/spring-servlet.xml”,来创建Servlet WebApplicationContext。

    同时,其会调用ServletContext的getAttribute方法来判断是否存在Root WebApplicationContext。如果存在,则将其设置为自己的parent。这就是父子上下文(父子容器)的概念。

父子容器的作用在于,当我们尝试从child context(即:Servlet WebApplicationContext)中获取一个bean时,如果找不到,则会委派给parent context (即Root WebApplicationContext)来查找。

如果我们没有通过ContextLoaderListener来创建Root WebApplicationContext,那么Servlet WebApplicationContext的parent就是null,也就是没有parent context。

1.1 为什么要有父子容器

笔者理解,父子容器的作用主要是划分框架边界。

在J2EE三层架构中,在service层我们一般使用spring框架, 而在web层则有多种选择,如spring mvc、struts等。因此,通常对于web层我们会使用单独的配置文件。例如在上面的案例中,一开始我们使用spring-servlet.xml来配置web层,使用applicationContext.xml来配置service、dao层。如果现在我们想把web层从spring mvc替换成struts,那么只需要将spring-servlet.xml替换成Struts的配置文件struts.xml即可,而applicationContext.xml不需要改变。

事实上,如果你的项目确定了只使用spring和spring mvc的话,你甚至可以将service 、dao、web层的bean都放到spring-servlet.xml中进行配置,并不是一定要将service、dao层的配置单独放到applicationContext.xml中,然后使用ContextLoaderListener来加载。在这种情况下,就没有了Root WebApplicationContext,只有Servlet WebApplicationContext。

当然要注意前提,下文会详细介绍功能的划分

1.2 是否可以把所有类都通过Spring父容器来管理?

即,是否把所有类都放到Spring的applicationContext.xml配置文件中,而spring-servlet.xml里面不配置?

即applicationContext.xml:

<!--覆盖全部的classpath-->
 <context:component-scan base-package="com.test"></context:component-scan>

spring-servlet.xml:

<context:component-scan

答案是否定的:这样会导致我们请求接口的时候产生404。因为在解析@ReqestMapping注解的过程中initHandlerMethods()函数只是对Spring MVC 容器(即子容器)中的bean进行处理的,并没有去查找父容器的bean, 因此不会对父容器中含有@RequestMapping注解的函数进行处理,更不会生成相应的handler。所以当请求过来时找不到处理的handler,导致404。

1.3 是否可以把所有类都通过Spring mvc子容器来管理?

答:原则上我们是可以把service、dao 和controller都交给springMVC去管理,直接在SpringMVC配置文件中让它扫所有包就可以,但是出于未来扩展的考虑,spring和springMVC分开配置,由 spring 去管理service,有利于以后扩展,即便以后加多个struct2也不用影响原有配置,可以全配置到spring-servlet.xml中。

为什么可行?因为无非就是把所有的东西全部交给子容器来管理了,子容器执行了refresh方法,把在它的配置文件里面的东西全部加载管理起来来了。虽然可以这么做不过一般应该是不推荐这么去做的,一般人也不会这么干的。

1.4 可以父子容器同时扫描

一般好像也没啥问题,也有未知问题的可能性,但是浪费内存是肯定的。

1.5 标准配置

1.5.1 通过exclude-filter和include-filter进行隔离

通过exclude-filter和include-filter ,让2个容器各自加载相关的注解标签类。

applicationContext.xml,加载全部的标签,但是排除掉Controller、ControllerAdvice声明:

	<context:component-scan base-package="com.springmvc">
	  <!---exclude-filter是排除掉的意思-->
		<context:exclude-filter type="annotation"
			expression="org.springframework.stereotype.Controller"/>
		<context:exclude-filter type="annotation"
			expression="org.springframework.web.bind.annotation.ControllerAdvice"/>
	</context:component-scan>

spring-servlet.xml,扫描路径,仅包含Controller、ControllerAdvice声明:

	<mvc:default-servlet-handler/>
	<mvc:annotation-driven></mvc:annotation-driven>

  <!--扫描路径,仅包含Controller、ControllerAdvice声明-->
	<context:component-scan base-package="com.springmvc" use-default-filters="false">
	    <!--include-filter是指包含的意思,type是声明式,即指带@Controller的标签的类-->
		<context:include-filter type="annotation"
			expression="org.springframework.stereotype.Controller"/>
		<context:include-filter type="annotation"
			expression="org.springframework.web.bind.annotation.ControllerAdvice"/>
	</context:component-scan>

ControllerAdvice和Controller类似,也是spring mvc相关的。

理论上除了@Controller必须在子容器外,其余的@标签可以在父容器,也可以在子容器,但是 如果你的项目里有用到事物、或者aop,一定要记得 要么都放到子容器的配置文件中,要么都放到父容器中,不然一部分内容在子容器和一部分内容在父容器,可能就会导致你的事物或者AOP不生效。

因此,严格按照标准配置,这样可以避免任何问题!

1.5.2 可以通过packge隔离不同功能的类

applicationContext.xml:

	<context:component-scan base-package="com.a">
	 
	</context:component-scan>

spring-servlet.xml:

	<context:component-scan base-package="com.b">
	 
	</context:component-scan>

一个加载a包(不包含任何spring mvc功能),一个加载b包(仅包含spring mvc功能),这样,就不会产生干扰了!

1.6 总结

Spring MVC WEB 层容器可作为 “业务层” Spring 容器的子容器:即 WEB 层容器可以引用业务层容器的 Bean,而业务层容器却访问不到 WEB 层容器的 Bean。

在这里插入图片描述

如果不注意父子容器,可能会产生不少的问题:
如果开发者不知道Spring mvc里分有两个WebApplicationContext,会导致各种重复构造bean、各种bean无法注入的问题。

有一些bean,比如全局的aop处理的类,如果先在父WebApplicationContext里初始化了,那么子WebApplicationContext里的初始化的bean就没有处理到。如果在子WebApplicationContext里初始化,在父WebApplicationContext里的类就没有办法注入了。

结论:为了避免出现上述问题,尽量把不同的配置归类到不同的配置文件下,然后,让ContextLoaderListener和DispatcherServlet分别加载不同的配置文件!

可以参考下面的这个例子:该用户由于让ContextLoaderListener和DispatcherServlet加载了相同的配置文件,导致同一个bean被调用了2次构造函数。
《Spring Bean重复执行两次(实例被构造两次)问题分析》

1.7 springboot中还有父子容器吗

2. ContextLoaderListener

Listener是一种监听事件的机制,等价于你订阅了某个事件,当你启动一个spring工程或一个jar包,在整个服务启动的周期中,会发布不同的事件,而Listener可以响应这些事件,进而做一些事情,可以先看下 Spring中ApplicationListener的使用中的例子,当你自定义一个Listener时,在相应的时机会被触发,而ContextLoaderListener就是在某个时机负责创建容器的

一般用法,web.xml配置:

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

对于一个web容器,web容器提供了一个全局的上下文环境,这个上下文就是ServletContext,其为后面Spring IOC容器提供宿主环境,而ContextLoaderListener作用就是初始化ServletContext。

因此,在指定的或默认的配置文件中,需要加上component-scan标签,这样就会扫描指定路径下的bean,并加载到容器中,这就是IOC功能:

<context:component-scan base-package="com.test.springmvc">	</context:component-scan>

在web容器启动时会触发容器初始化事件,contextLoaderListener监听到这个事件后其contextInitialized方法就会被调用,在这个方法中,spring会初始化一个启动上下文,这个上下文就是根上下文,也就是WebApplicationContext,实际实现类默认是XmlWebApplicationContext,这个其实就是spring的IoC容器,这个IoC容器初始化完后,Spring会将它存储到ServletContext,可供后面获取到该IOC容器中的bean。

XmlWebApplicationContext此时就是父容器。

2.1 为什么定义在web.xml中

如前文所述,tomcat等web服务器中,需要提供创建spring容器的接口,ContextLoaderListener正是这个接口的具体实现。

我们看下一个普通的spring是如何启动的,参见 【spring】JavaConfig、@Configuration、@ComponentScan入门例子

在这里插入图片描述
如上图所示,我们需要定义一个main方法,并且指定一个spring容器AnnotationConfigApplicationContext,对应JavaConfig配置形式,(你也可以使用其他的,例如XmlWebApplicationContext,对应xml形式的配置),那么在web项目下,我们没有main方法,因此,需要一个入口,来new 一个容器。因此,这就是在web.xml定义ContextLoaderListener的根本原因

2.2 配置参数

ContextLoaderListener接收2个参数,可以通过 <context-param>来指定:

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

如果不设置,他们都有默认值:

  • contextClass 实现WebApplicationContext接口的类,当前的servlet用它来创建上下文。如果这个参数没有指定, 默认使用XmlWebApplicationContext。

  • contextConfigLocation 传给上下文实例(由contextClass指定)的字符串,用来指定上下文的位置。
    默认会以 /WEB-INF/applicationContext.xml作为配置文件

    这个2个参数,对于DispatcherServlet也同样有效,ContextLoaderListener不设置命令空间,DispatcherServlet可以设置命令空间,如何不设置,用servlet-name作默认的命名空间

    public class XmlWebApplicationContext extends AbstractRefreshableWebApplicationContext {
    	protected String[] getDefaultConfigLocations() {
    		if (getNamespace() != null) {
    			return new String[] {DEFAULT_CONFIG_LOCATION_PREFIX + getNamespace() + DEFAULT_CONFIG_LOCATION_SUFFIX};
    		}
    		else {
    			return new String[] {DEFAULT_CONFIG_LOCATION};
    		}
    	}
    

    在这里插入图片描述

2.3 初始化过程

public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
    //监听到初始化容器事件时,进行初始化WebApplicationContext
    @Override
    public void contextInitialized(ServletContextEvent event) {
        initWebApplicationContext(event.getServletContext());
    }

ContextLoaderListener负责监听初始化容器事件,监听到初始化容器事件时,进行初始化WebApplicationContext操作,contextInitialized()是响应监听事件的回调函数,具体的初始化逻辑定义在父类ContextLoader 中:

public class ContextLoader {
/**

 * @return the new WebApplicationContext 返回类型
 * @see #ContextLoader(WebApplicationContext) 
 * @see #CONTEXT_CLASS_PARAM 解析类
 * @see #CONFIG_LOCATION_PARAM  配置文件路径
 */
  public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
    // 先判断ServletContext中是否已存在上下文,有的话说明已加载或配置信息有误(看下面抛出的异常信息)
    if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
        throw new IllegalStateException(
                "Cannot initialize context because there is already a root application context present - " +
                "check whether you have multiple ContextLoader* definitions in your web.xml!");
    }
      

    try {
        // Store context in local instance variable, to guarantee that
        // it is available on ServletContext shutdown.
        if (this.context == null) {
            // 创建WebApplicationContext上下文
            this.context = createWebApplicationContext(servletContext);
        }
       .......
        servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
    
        /* 省略部分代码 */
}

上面initWebApplicationContext()方法中,通过createWebApplicationContext(servletContext)创建root上下文(即IOC容器),之后Spring会以WebApplicationContext.ROOTWEBAPPLICATIONCONTEXTATTRIBUTE属性为Key,将该root上下文存储到ServletContext中。

这个属性很重要,后面的DispatcherServlet会依此,取出root上下文作为自己的父上下文

下面看看createWebApplicationContext(servletContext)源码:

protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
    // 确定载入的上下文的类型,参数是在web.xml中配置的contextClass(没有则使用默认的)
    Class<?> contextClass = determineContextClass(sc);
    .....
    // 初始化WebApplicationContext并强转为ConfigurableWebApplicationContext类型
    return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
}

上面createWebApplicationContext(servletContext)方法里的determineContextClass方法用于查找root上下文的Class类型,看源码:

protected Class<?> determineContextClass(ServletContext servletContext) {
    //CONTEXT_CLASS_PARAM参数是在web.xml中配置的contextClass(没有则使用默认的)
    String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
    if (contextClassName != null) {
      
            return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
       
    }
    else {
       //如何没有指定,则读取默认设置的类即XmlWebApplicationContext
        contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
        try {
            return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
        }
        catch (ClassNotFoundException ex) {
            throw new ApplicationContextException(
                    "Failed to load default context class [" + contextClassName + "]", ex);
        }
    }
}

从以上可以看到如果web.xml中配置了实现ConfigurableWebApplicationContext的contextClass类型就用那个参数,否则使用默认的XmlWebApplicationContext

3. DispatcherServlet

DispatcherServlet位于springmvc包下,提供读取spring mvc相关的配置能力。

因此,在指定的或默认的配置文件中,需要加上component-scan标签,这样就会扫描指定路径下的bean,并加载到容器中,@Controller标签的类才会被容器加载:

<context:component-scan base-package="com.test.springmvc">	</context:component-scan>

在contextLoaderListener监听器初始化完毕后,开始初始化web.xml中配置的Servlet,这个servlet可以配置多个,以DispatcherServlet为例,这个servlet实际上是一个标准的前端控制器,用以转发、处理每个servlet请求。

DispatcherServlet上下文在初始化的时候会建立自己的IoC上下文,用以持有spring mvc相关的bean。在建立DispatcherServlet自己的IoC上下文时,会利用WebApplicationContext.ROOTWEBAPPLICATIONCONTEXTATTRIBUTE先从ServletContext中获取之前的根上下文(即WebApplicationContext)作为自己上下文的parent上下文。有了这个parent上下文之后,再初始化自己持有的上下文。这个DispatcherServlet初始化自己上下文的工作在其initStrategies方法中实现的,基本工作就是初始化处理器映射、视图解析等。

存在2个上下文,一个是父的,根上下文,一个是spring mvc自己的

这个servlet自己持有的上下文默认实现类也是XmlWebApplicationContext。初始化完毕后,spring以与servlet的名字相关的属性为Key,也将其存到ServletContext中。这样每个servlet就持有自己的上下文,即拥有自己独立的bean空间,同时各个servlet共享相同的bean,即根上下文(WebApplicationContext)。

3.1 配置

DispatcherServlet默认使用XmlWebApplicationContext作为上下文,上下文就是配置相关的bean归属的容器,因此也需要参数来设置容器,参数和ContextLoaderListener一样,如果没有配置,则使用默认配置,只不过默认值不同:

  • contextClass 实现WebApplicationContext接口的类,当前的servlet用它来创建上下文。如果这个参数没有指定, 默认使用XmlWebApplicationContext

  • contextConfigLocation 传给上下文实例(由contextClass指定)的字符串,用来指定上下文的位置。

    这个字符串可以被分成多个字符串(使用逗号作为分隔符) 来支持多个上下文(在多上下文的情况下,如果同一个bean被定义两次,后面一个优先)。默认为/WEB-INF/[server-name]-servlet.xml

  • namespace WebApplicationContext命名空间。默认值是[server-name]-servlet。

    设置源码:

    FrameworkServlet.java
    //当没有命名空间,则用servlet-name作为命令空间
    public String getNamespace() {
    		return (this.namespace != null ? this.namespace : getServletName() + DEFAULT_NAMESPACE_SUFFIX);
    	}
    

我们以contextConfigLocation 为例,展示定制参数的用法:

<servlet>
        <servlet-name>spring</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>
 </servlet>

通过contextConfigLocation指定了spring mvc的配置文件为“classpath:spring-servlet.xml”


参考:
《浅谈ContextLoaderListener及其上下文与DispatcherServlet的区别》 参考设置父子容器的源码
《spring和springmvc父子容器概念介绍》 参考父子容器的种种区别
《Spring和SpringMVC父子容器关系初窥》 父子容器,有个漏配component-scan导致404的

  • 14
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值