文章目录
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的