Spring MVC学习(2)—Spring MVC中容器的层次结构以及父子容器的概念

「这是我参与2022首次更文挑战的第14天,活动详情查看:2022首次更文挑战」。

基于最新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解决跨域问题

1 父子容器的介绍

我们此前学过并知道普通的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配置文件,这样的好处是可以将这些配置分开,而父子容器的作用大概同样是为了划分框架边界而区分的吧,并且实际上可以配置多个子容器共享一个父容器。

当然,我们可以仅配置一个容器!

  1. 对于XML的配置来说,如果不需要配置父容器,那么我们可以直接不配置context-param和listener,将所有的业务层和持久层的配置都写在spring MVC的配置文件中即可,如果不需要配置子容器,那么我们在DispatcherServlet的contextConfigLocation参数的param-value中不填写任何值就行了,也就是让它是空着(这表示没有配置文件,如果不添加该属性,将会使用默认配置文件)。
  2. 对于JavaConfig的配置来说,如果不需要配置父容器,那么我们可以直接让getRootConfigClasses方法返回null,如果不需要配置子容器,那么我们可以直接让getServletConfigClasses方法返回null。

注意,如果只配置父容器,那么可能会有很多问题,下面会介绍!

4 父子容器的补充

如果一个项目配置了父子容器,那么需要注意以下几点,这些知识点非常重要,可能遇到某些莫名其妙的问题就是因为这些原因:

  1. 在父子容器的配置中,子容器可以引用父容器中的bean,但是父容器不能够引用子容器中的bean的,并且各个子容器中定义的bean是互不可见的。在查找bean时,如果子容器存在该bean,那么将直接从子容器获取,否则才会尝试从父容器中获取!
  2. 如果父子容器的配置中有相同的bean,比如两个配置中扫描包的路径有重叠,那么这个bean将会被初始化两次,注意,这不是覆盖,而是在父子的容器各自初始化两一次并且保存在各自的容器中,这将带来很多问题!
    1. 这样导致在两个父子IOC容器中生成大量的相同bean,这就会造成内存资源的浪费。
    2. 对于某些配置bean,比如Mq的消费者,如果加载了两次,那么带来很大的问题。
    3. 可能导致某些隐性的问题,比如如果子容器和父容器都加载了Service的bean,但是AOP的配置只是在父容器中被应用,那么这样回到这Service的AOP配置失效,因为,子容器将直接使用自己内部的没有经过AOP增强的Service对象。
      1. 一个避免方法就是使用注解配置,比如@Transactional,但是最好的办法就是:要么所有配置都在子容器中加载,要么父子容器加载的类通过不同的包路径彻底分开!
  3. 如果基于XML配置:
    1. Servlet WebApplicationContext子容器是一定会被初始化的,因为它是随着DispatcherServlet的创建而默认创建,而DispatcherServlet一般都需要配置。只不过你可以选择不在子容器中初始化你的配置,也就是将param-value参数空着。
    2. Root WebApplicationContext父容器不一定会被初始化,因为它被ContextLoaderListener创建,而ContextLoaderListener可以不配置,也就是说父容器可以真正的不存在。
    3. 如果同时配置了父子容器,那么请求将会先到达子容器,而在@ReqestMapping的解析过程中,只是对Servlet WebApplicationContext容器中的bean进行处理的,并没有去查找父容器的bean(实际上是只查找当前DispatcherServlet关联的容器)。因此如果Controller被配置到父容器中,那么因为不会对父容器中含有@RequestMapping注解的函数进行处理,更不会生成相应的handler。那么对应的请求过来的时候由于找不到对应的hander,将无法访问该Controller内部的@ReqestMapping对应的资源。
  4. 如果基于JavaConfig配置:
    1. 如是通过实现WebApplicationInitializer接口来进行配置的项目(比如入门案例中的JavaConfig配置),此时将只有一个容器,虽然它的displayName为Root WebApplicationContext,但是由于它和DispatcherServlet是发生了关联,因此实际上可以看作Servlet WebApplicationContext容器,因此可以解析该容器的Controller实例内部的@ReqestMapping注解,也就是说可以正常使用。
    2. 如是通过继承AbstractAnnotationConfigDispatcherServletInitializer抽象类来进行配置的项目:
      1. Servlet WebApplicationContext子容器一定会被初始化,即使你的getServletConfigClasses方法返回null,那只是表示子容器将不会加载你的任何其他配置而已。
      2. 如果getRootConfigClasses方法返回null,那么父容器将不会被初始化,也就是说此时项目只有一个Servlet WebApplicationContext。
    3. 如果同时配置了父子容器,那么请求将会先到达子容器。而在@ReqestMapping的解析过程中,只是对Servlet WebApplicationContext容器中的bean进行处理的,并没有去查找父容器的bean。因此如果Controller被配置到父容器中,那么因为不会对父容器中含有@RequestMapping注解的函数进行处理,更不会生成相应的handler。那么对应的请求过来的时候由于找不到对应的hander,将无法访问该Controller内部的@ReqestMapping对应的资源。
  5. 另外需要注意的是,Spring boot采用了不同的初始化顺序和策略,使用Spring配置来引导自身和嵌入式Servlet容器(tomcat)。Filter和 Servlet 在 Spring 配置中声明,并在嵌入式的Servlet 容器中注册。我们使用Spring boot之后,根本不需要关心上面的这些问题,后面我们会在学习Spring boot的时候一一介绍。
  6. 如果只配置“一个”容器,那么建议只配置Servlet WebApplicationContext,也就是和DispatcherServlet关联的容器。

5 DispatcherServlet的其他属性

我们可以通过配置以下属性来定制各个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实例的属性名

6 容器测试

6.1 项目搭建

项目结构

在这里插入图片描述

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="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/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="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/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"
        "http://java.sun.com/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插件启动项目(第一篇文章已经讲过了)!

首先我们将会看到类的加载信息:

在这里插入图片描述

确实只加载了一次!然后我们尝试调用接口http://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"
        "http://java.sun.com/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>
复制代码

此时,我们启动项目,查看控制台日志输出:

在这里插入图片描述

嗯,好像没什么问题!然后我们尝试访问接口:http://localhost:8081/mvc/oneLoad,结果如下:

在这里插入图片描述

抛出了404异常,这就是因为父容器中的Controller内部的@RequestMapping没有被解析(实际上是因为这个父容器没有和DispatcherServlet关联,参见入门案例的JavaConfig的关联配置),导致不能创建对应路径的handler,进而对应的资源路径找不到,虽然资源实际上是存在的!

相关文章:

  1. spring.io/
  2. Spring Framework 5.x 学习
  3. Spring Framework 5.x 源码

如有需要交流,或者文章有误,请直接留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

Guess you like

Origin juejin.im/post/7062168234715250719