【SpringMVC】9—底层原理

⭐⭐⭐⭐⭐⭐
Github主页👉https://github.com/A-BigTree
笔记链接👉https://github.com/A-BigTree/Code_Learning
⭐⭐⭐⭐⭐⭐

如果可以,麻烦各位看官顺手点个star~😊

如果文章对你有所帮助,可以点赞👍收藏⭐支持一下博主~😆


9 底层原理

9.1 启动过程

9.1.1 初始化操作调用路线

类和接口之间的关系

在这里插入图片描述

调用路线图

调用线路图所示是方法调用的顺序,但是实际运行的时候本质上都是调用DispatcherServlet对象的方法。包括这里涉及到的接口的方法,也不是去调用接口中的『抽象方法』。毕竟抽象方法是没法执行的。抽象方法一定是在某个实现类中有具体实现才能被调用。

而对于最终的实现类:DispatcherServlet来说,所有父类的方法最后也都是在DispatcherServlet对象中被调用的。

在这里插入图片描述

9.1.2 IOC容器创建

所在类:org.springframework.web.servlet.FrameworkServlet

protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
  Class<?> contextClass = getContextClass();
  if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
    throw new ApplicationContextException(
        "Fatal initialization error in servlet with name '" + getServletName() +
        "': custom WebApplicationContext class [" + contextClass.getName() +
        "] is not of type ConfigurableWebApplicationContext");
  }
    
    // 通过反射创建 IOC 容器对象
  ConfigurableWebApplicationContext wac =
      (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);

  wac.setEnvironment(getEnvironment());
    
    // 设置父容器
  wac.setParent(parent);
  String configLocation = getContextConfigLocation();
  if (configLocation != null) {
    wac.setConfigLocation(configLocation);
  }
  
  // 配置并且刷新:在这个过程中就会去读XML配置文件并根据配置文件创建bean、加载各种组件
  configureAndRefreshWebApplicationContext(wac);

  return wac;
}

9.1.3 IOC容器对象存入应用域

所在类:org.springframework.web.servlet.FrameworkServlet

protected WebApplicationContext initWebApplicationContext() {
  WebApplicationContext rootContext =
      WebApplicationContextUtils.getWebApplicationContext(getServletContext());
  WebApplicationContext wac = null;

  if (this.webApplicationContext != null) {
    wac = this.webApplicationContext;
    if (wac instanceof ConfigurableWebApplicationContext) {
      ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
      if (!cwac.isActive()) {
        if (cwac.getParent() == null) {
          cwac.setParent(rootContext);
        }
        configureAndRefreshWebApplicationContext(cwac);
      }
    }
  }
  if (wac == null) {
    wac = findWebApplicationContext();
  }
  if (wac == null) {
        // 创建 IOC 容器
    wac = createWebApplicationContext(rootContext);
  }

  if (!this.refreshEventReceived) {
    synchronized (this.onRefreshMonitor) {
      onRefresh(wac);
    }
  }

  if (this.publishContext) {
    // 获取存入应用域时专用的属性名
    String attrName = getServletContextAttributeName();
        
        // 存入
    getServletContext().setAttribute(attrName, wac);
  }

  return wac;
}

看到这一点的意义:SpringMVC 有一个工具方法,可以从应用域获取 IOC 容器对象的引用。

工具类:org.springframework.web.context.support.WebApplicationContextUtils

工具方法:getWebApplicationContext()

@Nullable
public static WebApplicationContext getWebApplicationContext(ServletContext sc) {
  return getWebApplicationContext(sc, WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
}

作用:将来假如我们自己开发时,在IOC容器之外需要从IOC容器中获取bean,那么就可以通过这个工具方法获取IOC容器对象的引用。IOC容器之外的场景会有很多,比如在一个我们自己创建的Filter中。

9.1.4 请求映射初始化

FrameworkServlet.createWebApplicationContext()configureAndRefreshWebApplicationContext()wac.refresh()→触发刷新事件 → org.springframework.web.servlet.DispatcherServlet.initStrategies()org.springframework.web.servlet.DispatcherServlet.initHandlerMappings()

9.1.5 总结

整个启动过程我们关心如下要点:

  • DispatcherServlet 本质上是一个 Servlet,所以天然的遵循 Servlet 的生命周期。所以宏观上是 Servlet 生命周期来进行调度;
  • DispatcherServlet 的父类是 FrameworkServlet
    • FrameworkServlet 负责框架本身相关的创建和初始化;
    • DispatcherServlet 负责请求处理相关的初始化;
  • FrameworkServlet 创建 IOC 容器对象之后会存入应用域;
  • FrameworkServlet 完成初始化会调用 IOC 容器的刷新方法;
  • 刷新方法完成触发刷新事件,在刷新事件的响应函数中,调用 DispatcherServlet 的初始化方法;
  • DispatcherServlet 的初始化方法中初始化了请求映射等;

9.2 请求处理过程

9.2.1 总体阶段

流程描述
  • 目标handler方法执行
    • 建立调用链,确定整个执行流程
    • 拦截器的preHandle()方法
    • 注入请求参数
    • 准备目标handler方法所需所有参数
  • 调用目标handler方法
  • 目标handler方法执行
    • 拦截器的postHandle()方法
    • 渲染视图
    • 拦截器的afterCompletion()方法
核心代码

整个请求处理过程都是doDispatch()方法在宏观上协调和调度,把握了这个方法就理解了SpringMVC总体上是如何处理请求的。

所在类:DispatcherServlet

所在方法:doDispatch()

核心方法中的核心代码:

// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

9.2.2 调用前阶段

建立调用链

相关组件:

全类名:org.springframework.web.servlet.HandlerExecutionChain

在这里插入图片描述

拦截器索引默认是 -1,说明开始的时候,它指向第一个拦截器前面的位置。每执行一个拦截器,就把索引向前移动一个位置。所以这个索引每次都是指向当前拦截器。所以它相当于拦截器的指针。

对应操作:

所在类:org.springframework.web.servlet.handler.AbstractHandlerMapping

所在方法:getHandlerExecutionChain()

关键操作:

  • 把目标handler对象存入
  • 把当前请求要经过的拦截器存入

在这里插入图片描述

结论:调用链是由拦截器和目标handler对象组成的。

调用拦截器preHandle()

所在类:org.springframework.web.servlet.DispatcherServlet

所在方法:doDispatch()

  • 具体调用细节:正序调用;
  • 所在类:org.springframework.web.servlet.HandlerExecutionChain
  • 所在方法:applyPreHandle()

在这里插入图片描述

从这部分代码我们也能看到,为什么拦截器中的 preHandle() 方法通过返回布尔值能够控制是否放行。

  • 每一个拦截器的 preHandle() 方法都返回 trueapplyPreHandle() 方法返回 true,被取反就不执行 if 分支,继续执行后续操作,这就是放行;
  • 任何一个拦截器的 preHandle() 方法返回 falseapplyPreHandle() 方法返回 false,被取反执行 if 分支,return,导致 doDispatch() 方法结束,不执行后续操作,就是不放行。
注入请求参数

接口:org.springframework.web.servlet.HandlerAdapter

作用:字面含义是适配器的意思,具体功能有三个

  • 将请求参数绑定到实体类对象中
  • 给目标 handler 方法准备所需的其他参数,例如:
    • Model、ModelMap、Map……
    • 原生 Servlet API:request、response、session……
    • BindingResult
    • @RequestParam注解标记的零散请求参数
    • @PathVariable注解标记的路径变量
  • 调用目标 handler 方法

所以 HandlerAdapter 这个适配器是将底层的 HTTP 报文、原生的 request 对象进行解析和封装,『适配』到我们定义的 handler 方法上。

通过反射给对应属性注入请求参数应该是下面的过程:

  • 获取请求参数名称;
  • 将请求参数名称首字母设定为大写;
  • 在首字母大写后的名称前附加set,得到目标方法名;
  • 通过反射调用setXxx()方法;

9.3 ContextLoaderListener

9.3.1 问题

目前情况:DispatcherServlet 加载 spring-mvc.xml,此时整个 Web 应用中只创建一个 IOC 容器。将来整合Mybatis、配置声明式事务,全部在 spring-mvc.xml 配置文件中配置也是可以的。可是这样会导致配置文件太长,不容易维护。

所以想到把配置文件分开:

  • 处理浏览器请求相关:spring-mvc.xml 配置文件
  • 声明式事务和整合Mybatis相关:spring-persist.xml 配置文件

配置文件分开之后,可以让 DispatcherServlet 加载多个配置文件。例如:

<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-*.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

如果希望这两个配置文件使用不同的机制来加载:

  • DispatcherServlet 加载 spring-mvc.xml 配置文件:它们和处理浏览器请求相关
  • ContextLoaderListener 加载 spring-persist.xml 配置文件:不需要处理浏览器请求,需要配置持久化层相关功能

此时会带来一个新的问题:在一个 Web 应用中就会出现两个 IOC 容器

  • DispatcherServlet 创建一个 IOC 容器
  • ContextLoaderListener 创建一个 IOC 容器

注意:本节我们探讨的这个技术方案并不是『必须』这样做,而仅仅是『可以』这样做。

9.3.2 配置ContextLoaderListener

创建spring-persist.xml并配置
<!-- 通过全局初始化参数指定 Spring 配置文件的位置 -->
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring-persist.xml</param-value>
</context-param>
 
<listener>
    <!-- 指定全类名,配置监听器 -->
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
ContextLoaderListener
方法名执行时机作用
contextInitialized()Web 应用启动时执行创建并初始化 IOC 容器
contextDestroyed()Web 应用卸载时执行关闭 IOC 容器
ContextLoader

ContextLoader 类是 ContextLoaderListener 类的父类。

9.3.3 两个IOC之间的关系

两个组件分别创建的 IOC 容器是父子关系。

  • 父容器:ContextLoaderListener 创建的 IOC 容器;
  • 子容器:DispatcherServlet 创建的 IOC 容器;

父子关系是如何决定的?

  • Tomcat 在读取 web.xml 之后,加载组件的顺序就是监听器、过滤器、Servlet。
  • ContextLoaderListener初始化时如果检查到有已经存在的根级别 IOC 容器,那么会抛出异常。
  • DispatcherServlet 创建的 IOC 容器会在初始化时先检查当前环境下是否存在已经创建好的 IOC 容器。
    • 如果有:则将已存在的这个 IOC 容器设置为自己的父容器
    • 如果没有:则将自己设置为 root 级别的 IOC 容器

9.3.4 两个容器之间访问关系

子容器中的 EmpController 装配父容器中的 EmpService 能够正常工作。说明子容器可以访问父容器中的bean。

分析:“子可用父,父不能用子”的根本原因是子容器中有一个属性 getParent() 可以获取到父容器这个对象的引用。

源码依据:

  • AbstractApplicationContext 类中,有 parent 属性;
  • AbstractApplicationContext 类中,有获取 parent 属性的 getParent() 方法;
  • 子容器可以通过 getParent() 方法获取到父容器对象的引用;
  • 进而调用父容器中类似 “getBean()” 这样的方法获取到需要的 bean 完成装配;
  • 而父容器中并没有类似 “getChildren()“ 这样的方法,所以没法拿到子容器对象的引用;

在这里插入图片描述

9.3.5 重复创建问题

问题
  • 浪费内存空间

  • 两个 IOC 容器能力是不同的

    • spring-mvc.xml:仅配置和处理请求相关的功能。所以不能给 service 类附加声明式事务功能。

      结论:基于 spring-mvc.xml 配置文件创建的 EmpService 的 bean 不带有声明式事务的功能

      影响:DispatcherServlet 处理浏览器请求时会调用自己创建的 EmpController,然后再调用自己创建的EmpService,而这个 EmpService 是没有事务的,所以处理请求时没有事务功能的支持。

    • spring-persist.xml:配置声明式事务。所以可以给 service 类附加声明式事务功能。

      结论:基于 spring-persist.xml 配置文件创建的 EmpService 有声明式事务的功能

      影响:由于 DispatcherServlet 的 IOC 容器会优先使用自己创建的 EmpController,进而装配自己创建的EmpService,所以基于 spring-persist.xml 配置文件创建的有声明式事务的 EmpService 用不上。

解决方法1

让两个配置文件配置自动扫描的包时,各自扫描各自的组件。

  • SpringMVC 就扫描 XxxHandlerXXXController
  • Spring 扫描 XxxServiceXxxDao
解决方法2

如果由于某种原因,必须扫描同一个包,确实存在重复创建对象的问题,可以采取下面的办法处理。

  • spring-mvc.xml 配置文件在整体扫描的基础上进一步配置:仅包含被 @Controller 注解标记的类。
  • spring-persist.xml 配置在整体扫描的基础上进一步配置:排除被 @Controller 注解标记的类。

具体spring-mvc.xml配置文件中的配置方式如下:

<!-- 两个Spring的配置文件扫描相同的包 -->
<!-- 为了解决重复创建对象的问题,需要进一步制定扫描组件时的规则 -->
<!-- 目标:『仅』包含@Controller注解标记的类 -->
<!-- use-default-filters="false"表示关闭默认规则,表示什么都不扫描,此时不会把任何组件加入IOC容器;
        再配合context:include-filter实现“『仅』包含”效果 -->
<context:component-scan base-package="com.atguigu.spring.component" use-default-filters="false">

    <!-- context:include-filter标签配置一个“扫描组件时要包含的类”的规则,追加到默认规则中 -->
    <!-- type属性:指定规则的类型,根据什么找到要包含的类,现在使用annotation表示基于注解来查找 -->
    <!-- expression属性:规则的表达式。如果type属性选择了annotation,那么expression属性配置注解的全类名 -->
    <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

具体spring-persist.xml配置文件中的配置方式如下:

<!-- 两个Spring的配置文件扫描相同的包 -->
<!-- 在默认规则的基础上排除标记了@Controller注解的类 -->
<context:component-scan base-package="com.atguigu.spring.component">

    <!-- 配置具体排除规则:把标记了@Controller注解的类排除在扫描范围之外 -->
    <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一棵___大树

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值