admin管理员组文章数量:1794759
Spring MVC学习(2)—Spring MVC中容器的层次结构以及父子容器的概念
基于最新Spring 5.x,详细介绍了Spring MVC中容器的层次结构以及父子容器的概念。
此前,我们已经学习过了Spring MVC学习(1)—MVC的介绍以及Spring MVC的入门案例,现在我们来学习Spring MVC中容器的层次结构以及父子容器的概念,并且说明一些常见问题。
Spring MVC学习 系列文章Spring MVC学习(1)—MVC的介绍以及Spring MVC的入门案例
Spring MVC学习(2)—Spring MVC中容器的层次结构以及父子容器的概念
Spring MVC学习(3)—Spring MVC中的核心组件以及请求的执行流程
Spring MVC学习(4)—ViewSolvsolver视图解析器的详细介绍与使用案例
Spring MVC学习(5)—基于注解的Controller控制器的配置全解【一万字】
Spring MVC学习(6)—Spring数据类型转换机制全解【一万字】
Spring MVC学习(7)—Validation基于注解的声明式数据校验机制全解【一万字】
Spring MVC学习(8)—HandlerInterceptor处理器拦截器机制全解
Spring MVC学习(9)—项目统一异常处理机制详解与使用案例
Spring MVC学习(10)—文件上传配置、DispatcherServlet的路径配置、请求和响应内容编码
Spring MVC学习(11)—跨域的介绍以及使用CORS解决跨域问题
文章目录- Spring MVC学习 系列文章
- 1 父子容器的介绍
- 2 配置父子容器
- 3 仅配置一个容器
- 4 父子容器的补充
- 5 DispatcherServlet的其他属性
- 6 容器测试
- 6.1 项目搭建
- 6.2 单个容器
- 6.3 父子容器
- 6.4 双重加载
- 6.5 映射失败
我们此前学过并知道普通的Spring应用有自己的ApplicationContext容器,而如果是基于Spring的web应用,那么它的容器有所不同,将会使用WebApplicationContext。
DispatcherServlet 需要WebApplicationContext容器(Web应用程序上下文,扩展了ApplicationContext(普通应用程序上下文))来进行自己的配置,因为WebApplicationContext具有获取ServletContext的getServletContext方法,并且Spring的WebApplicationContext容器同样与web应用的ServletContext相关联,因此我们在web应用程序中可以直接使用RequestContextUtils的静态方法通过当前请求查找WebApplicationContext。
在学习Spring源码的时候,我们知道Spring支持父子容器,但是我们并没有用过,实际上对于许多web应用程序来说,拥有单个WebApplicationContext就足够了,当然也可以有一个有层次的上下文结构,对于比咋的应用程序或许会更好,其中一个Root(根) WebApplicationContext 在多个调度器服务(或其他 Servlet)实例之间共享,每个实例都有其自己的Child(子) WebApplicationContext 配置。
Root WebApplicationContext 通常包含web应用中的基础结构 bean,例如需要跨多个Servlet实例共享的Dao、数据库配置bean、Service等服务bean,也就是三层架构中的业务层和持久层的bean,这些 bean可以在特定Servlet 的子 WebApplicationContext 中重写(即重新声明)。Child WebApplicationContext则用于存放三层架构中的表现层的bean,比如Controller、ViewResolver、HandlerMapping等Spring MVC的组件bean,因此也被称为Servlet WebApplicationContext。
父子容器的关系图如下:
2 配置父子容器父子容器可以配置吗?当然可以!
如果是基于XML的配置,那么Root WebApplicationContext通过ContextLoaderListener去加载名为“contextConfigLocation”的context-param参数来配置。而Servlet WebApplicationContext则是通过spring mvc中提供的DispatchServlet的名为“contextConfigLocation”的init-param参数初始化来加载配置。
如下案例:
<web-app> <display-name>Archetype Created Web Application</display-name> <!--配置contextConfigLocation初始化参数,指定父容器Root WebApplicationContext的配置文件 --> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-config.xml</param-value> </context-param> <!--监听contextConfigLocation参数并初始化父容器--> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!-- 配置spring mvc的前端核心控制器 --> <servlet> <servlet-name>dispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet </servlet-class> <!-- 配置contextConfigLocation初始化参数,指定子容器的配置文件并创建子容器 Servlet WebApplicationContext --> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc-config.xml</param-value> </init-param> <!--配置Servlet的对象的创建时间点:取值如果为非负整数,表示应用加载时创建,值越小,servlet的优先级越高,就越先被加载,如果取值为负数,表示在第一次使用时才加载--> <load-on-startup>1</load-on-startup> </servlet> <!--配置映射路径,/表示将会处理所有的请求--> <servlet-mapping> <servlet-name>dispatcherServlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>在上面的配置中,ContextLoaderListener作为监听器会被优先初始化,随后ServletContext会被初始化并且会将context-param参数设置设置进去,而ContextLoaderListener它实际上是一个ServletContextListener监听器实现,它将会监听ServletContext的创建事件并调用对应的contextInitialized方法,在该方法中将会获取ServletContext配置的contextConfigLocation参数(这里面就有我们配置的配置文件路径,默认路径为/WEB-INF/applicationContext.xml)并且初始化Root WebApplicationContext实例,也就是父容器,实际类型为XmlWebApplicationContext。
随后会将父容器通过setAttribute方法设置到ServletContext中,属性的key为”org.springframework.web.context.WebApplicationContext.ROOT”,最后的”ROOT"字样表明这是一个 Root WebApplicationContext,而WebApplicationContext中也会保留ServletContext的引用,这样WebApplicationContext和ServletContext就关联起来了。
随后DispatcherServlet会被实例化并且设置初始化参数,在创建完毕之后的init()回调方法(该方法在其父类HttpServletBean中)中,将会获取它自己的contextConfigLocation参数,并且根据指定的配置文件在initServletBean()方法中创建Servlet WebApplicationContext,也就是子容器。同时,其会调用ServletContext的getAttribute方法来判断是否存在Root WebApplicationContext。如果存在,则将其设置为自己的parent。这就是父子上下文(父子容器)的概念。
上面的讲解只是大概的过程,我们后面学习源码的时候将会更加的深入了解!
注意:如果配置了ContextLoaderListener,那么一定要配置名为contextConfigLocation的context-param参数,即指定Spring配置文件位置,如果没有配置,那么默认查找的路径为/WEB-INF/applicationContext.xml,找不到对应路径的配置文件就会抛出异常。如果配置了DispatcherServlet,那么一定要配置名为contextConfigLocation的init-param参数,即指定Spring MVC配置文件位置,如果没有配置,那么默认查找的路径为"/WEB-INF/"+容器nameSpace+ ".xml"(默认namespace为servletName+"-servlet"),找不到对应路径的配置文件就会抛出异常。
如果想要通过JavaConfig来配置父子容器,那么需要继承AbstractAnnotationConfigDispatcherServletInitializer来配置,并且配置信需要放在Java配置类中:
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { /** * @return Root WebApplicationContext的配置类 */ @Override protected Class<?>[] getRootConfigClasses() { return new Class<?>[]{RootConfig.class}; } /** * @return Servlet WebApplicationContext的配置类 */ @Override protected Class<?>[] getServletConfigClasses() { return new Class<?>[]{ChildConfig.class}; } /** * @return 映射路径 */ @Override protected String[] getServletMappings() { return new String[]{"/"}; } }如果仍然想要使用XML文件,那么可以继承AbstractAnnotationConfigDispatcherServletInitializer:
public class MyXmlWebAppInitializer extends AbstractDispatcherServletInitializer { @Override protected WebApplicationContext createRootApplicationContext() { XmlWebApplicationContext cxt = new XmlWebApplicationContext(); cxt.setConfigLocation("/WEB-INF/spring-config.xml"); return cxt; } @Override protected WebApplicationContext createServletApplicationContext() { XmlWebApplicationContext cxt = new XmlWebApplicationContext(); cxt.setConfigLocation("/WEB-INF/spring-mvc-config.xml"); return cxt; } @Override protected String[] getServletMappings() { return new String[]{"/"}; } } 3 仅配置一个容器在一个传统的Spring web项目中,通常情况下引入的不同组件都有不同的XML配置文件,这样的好处是可以将这些配置分开,而父子容器的作用大概同样是为了划分框架边界而区分的吧,并且实际上可以配置多个子容器共享一个父容器。
当然,我们可以仅配置一个容器!
注意,如果只配置父容器,那么可能会有很多问题,下面会介绍!
4 父子容器的补充如果一个项目配置了父子容器,那么需要注意以下几点,这些知识点非常重要,可能遇到某些莫名其妙的问题就是因为这些原因:
我们可以通过配置以下属性来定制各个DispatcherServlet实例,虽然可能用不到!这些属性,我们在后面学习Spring MVC源码的时候就能看到解析的过程。
contextClass | 实现了ConfigurableWebApplicationContext接口的容器类全路径名,将会由当前的DispatcherServlet实例化并关联,默认类型为XmlWebApplicationContext。 |
contextConfigLocation | 传递给WebApplicationContext实例(由contextClass指定类型)的配置文件的位置字符串,多个路径使用“,”分隔 |
namespace | WebApplicationContext的名称空间。默认值为 “servletName-servlet”。 |
throwExceptionIfNoHandlerFound | 在某个request找不到对应的Handler处理器时是否抛出NoHandlerFoundException异常,我们可以使用HandlerExceptionResolver来捕获该异常进而统一处理(例如使用@ExceptionHandler方法捕获)。默认值为false,在这种情况下,DispatcherServlet会将响应状态码设置为404(NOT_FOUND),而不会引发该异常。请注意,如果还配置了默认处理Servlet,则始终将未解决的请求转发到默认servlet,并且永远不会引发404错误。 |
contextId | 此Servlet关联的WebApplicationContext的id |
contextAttribute | 已经配置好的此Servlet关联的位于ServletContext中的WebApplicationContext实例的属性名 |
项目结构
com.spring.mvc.service.HelloService和com.spring.mvc.dao.HelloDao模拟业务层和持久层,com.spring.mvc.controller.HelloController模拟表现层:
@Repository public class HelloDao { public HelloDao() { System.out.println("HelloDao create"); } } @Service public class HelloService { public HelloService() { System.out.println("HelloService create"); } @Resource private HelloDao helloDao; } /** * @author lx */ @Controller public class HelloController { public HelloController() { System.out.println("HelloController create"); } @Resource private HelloService helloService; } /** * @author lx */ @Controller public class HelloController { public HelloController() { System.out.println("HelloController create"); } @Resource private HelloService helloService; }spring-config.xml是Spring的配置文件:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="www.springframework/schema/beans" xmlns:xsi="www.w3/2001/XMLSchema-instance" xmlns:context="www.springframework/schema/context" xsi:schemaLocation="www.springframework/schema/beans www.springframework/schema/beans/spring-beans.xsd www.springframework/schema/context www.springframework/schema/context/spring-context.xsd"> <context:component-scan base-package="com.spring.mvc.service,com.spring.mvc.dao"/> </beans>spring-mvc-config.xml是Spring MVC的配置文件:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="www.springframework/schema/beans" xmlns:xsi="www.w3/2001/XMLSchema-instance" xmlns:context="www.springframework/schema/context" xsi:schemaLocation="www.springframework/schema/beans www.springframework/schema/beans/spring-beans.xsd www.springframework/schema/context www.springframework/schema/context/spring-context.xsd"> <context:component-scan base-package="com.spring.mvc.controller"/> </beans> 6.2 单个容器首先我们测试单个子容器,我们的web.xml如下,此时我们需要contextConfigLocation加载两个配置文件:
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "java.sun/dtd/web-app_2_3.dtd" > <web-app> <display-name>Archetype Created Web Application</display-name> <servlet> <servlet-name>dispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet </servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc-config.xml,classpath:spring-config.xml</param-value> </init-param> <load-on-startup>0</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcherServlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>HelloController添加一个测试方法:
/** * 单个容器测试 */ @RequestMapping(path = "/oneLoad") public String oneLoad(HttpServletRequest servletRequest) { //获取当前容器 WebApplicationContext webApplicationContext = RequestContextUtils.findWebApplicationContext(servletRequest); //获取父容器 ApplicationContext parent = webApplicationContext.getParent(); System.out.println("从子容器获取: " + webApplicationContext.getBean(HelloService.class)); if (parent != null) { System.out.println("从父容器获取: " + parent.getBean(HelloService.class)); } else { System.out.println("没有父容器"); } System.out.println("自动注入的bean: " + helloService); return "index.jsp"; }通过tomcat插件启动项目(第一篇文章已经讲过了)!
首先我们将会看到类的加载信:
确实只加载了一次!然后我们尝试调用接口localhost:8081/mvc/oneLoad,输出如下:
说明只有一个容器,并且配置的类都只被加载一次!
6.3 父子容器我们设置父子容器,它们加载不同的配置文件!
<web-app> <display-name>Archetype Created Web Application</display-name> <!--配置contextConfigLocation初始化参数,指定父容器Root WebApplicationContext的配置文件 --> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-config.xml</param-value> </context-param> <!--监听contextConfigLocation参数并初始化父容器--> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <servlet> <servlet-name>dispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet </servlet-class> <!--Servlet WebApplicationContext子容器的配置--> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc-config.xml</param-value> </init-param> <load-on-startup>0</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcherServlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>启动日志如下:
可以看到Root WebApplicationContext initialized以及dispatcherServlet初始化的日志,并且,似乎HelloService和HelloDao在父容器中加载,而HelloController是在dispatcherServlet关联的子容器中加载的!
再次调用oneLoad接口,输出如下,可以看到有两个容器,并且都是同一个对象!
我们debug查看,可以看到实际上HelloController这个类的实例在子容器中,而HelloService和HelloDao的实例则存在父容器中(debug的时候,在applicationContext的beanFactory属性内的singletonObjects集合中可以找到该容器的所有的实例)。
6.4 双重加载我们将两个配置文件的base-package加载路径都改为com.spring.mvc,这样当两个容器创建的时候,就能加载相同的配置了。
改完之后,再次启动项目,这次控制台启动日志如下:
可怕的事情发生了,似乎这些类的构造器被调用了两次,也就是说这些对象都被初始化了两次!
再次调用oneLoad接口,输出如下,可以看到有两个容器,并且父子容器中的HelloService不是同一个对象!
实际上,我们debug就能知道,这两个容器中分别都存放着同类型的不同实例,并且这里并不是很多博客所说的子容器的实例会“覆盖”父容器的实例,仅仅是因为实例的访问机制而已,当我们采用注入或者直接访问所依赖的实例的时候,将会首先在子容器中查找,找不到才回去父容器中查找,因此造成了覆盖的假象。当子容器存在该实例时,父容器的实例默认是无法访问到的(将会自动注入子容器的实例),因此造成了内存的浪费和其他各种问题(比如,如果你的某个配置类有个定时器,那么当这个配置类被加载了两个实例之后,后果可想而知)。
我们一定要避免父子容器加载相同的配置!
6.5 映射失败第一个测试中,我们将所有配置放到子容器中加载,是没有问题的。现在,我们测试尝试将所有的配置放到父容器中加载!
在这个web.xml配置中,我们通过父容器加载所有配置文件,而子容器不加载任何文件。注意DispatcherServlet内部的contextConfigLocation参数一定要有,因为子容器一定存在,即使值为空("")也行,如果没有配置该属性的话将会查找默认配置文件,找不到将会抛出异常(前面有讲)!
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "java.sun/dtd/web-app_2_3.dtd" > <web-app> <display-name>Archetype Created Web Application</display-name> <!--配置contextConfigLocation初始化参数,指定父容器Root WebApplicationContext的配置文件 --> <context-param> <param-name>contextConfigLocation</param-name> <!--加载全部配置文件--> <param-value>classpath:spring-config.xml,classpath:spring-mvc-config.xml</param-value> </context-param> <!--监听contextConfigLocation参数并初始化父容器--> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <servlet> <servlet-name>dispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet </servlet-class> <!--Servlet WebApplicationContext子容器的配置--> <!--param-value为空,不加载任何配置--> <init-param> <param-name>contextConfigLocation</param-name> <param-value/> </init-param> <load-on-startup>0</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcherServlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>此时,我们启动项目,查看控制台日志输出:
嗯,好像没什么问题!然后我们尝试访问接口:localhost:8081/mvc/oneLoad,结果如下:
抛出了404异常,这就是因为父容器中的Controller内部的@RequestMapping没有被解析(实际上是因为这个父容器没有和DispatcherServlet关联,参见入门案例的JavaConfig的关联配置),导致不能创建对应路径的handler,进而对应的资源路径找不到,虽然资源实际上是存在的!
相关文章:
如有需要交流,或者文章有误,请直接留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!
版权声明:本文标题:Spring MVC学习(2)—Spring MVC中容器的层次结构以及父子容器的概念 内容由林淑君副主任自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.xiehuijuan.com/baike/1686616370a86607.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论