【SpringMVC】9—底层原理

这篇具有很好参考价值的文章主要介绍了【SpringMVC】9—底层原理。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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

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

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


9 底层原理

9.1 启动过程

9.1.1 初始化操作调用路线

类和接口之间的关系

【SpringMVC】9—底层原理

调用路线图

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

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

【SpringMVC】9—底层原理

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

【SpringMVC】9—底层原理

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

对应操作:

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

所在方法:getHandlerExecutionChain()

关键操作:

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

【SpringMVC】9—底层原理

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

调用拦截器preHandle()

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

所在方法:doDispatch()

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

【SpringMVC】9—底层原理

从这部分代码我们也能看到,为什么拦截器中的 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()“ 这样的方法,所以没法拿到子容器对象的引用;

【SpringMVC】9—底层原理

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配置文件中的配置方式如下:文章来源地址https://www.toymoban.com/news/detail-409494.html

<!-- 两个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>

到了这里,关于【SpringMVC】9—底层原理的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 手动开发-实现SpringMVC底层机制--小试牛刀

    在这里说的底层机制的实现主要是指:前端控制器、Controller、Service注入容器、对象自动装配、控制器方法获取参数、视图解析、返回json数据。 前端控制器就是核心控制器。在这里我们可以设计一个Servlet来充当核心控制器: LingDispatcherServlet.java .这个控制器的作用主要是接收

    2024年02月08日
    浏览(51)
  • 自己实现 SpringMVC 底层机制 系列之-实现任务阶段 6-完成控制器方法获取参数-@RequestParam

    😀前言 自己实现 SpringMVC 底层机制 系列之-实现任务阶段 6-完成控制器方法获取参数-@RequestParam 🏠个人主页:尘觉主页 🧑个人简介:大家好,我是尘觉,希望我的文章可以帮助到大家,您的满意是我的动力😉😉 在csdn获奖荣誉: 🏆csdn城市之星2名 ⁣⁣⁣⁣ ⁣⁣⁣⁣ ⁣⁣⁣

    2024年02月11日
    浏览(45)
  • JAVASE进阶:强推!源码分析——字符串拼接底层原理、StringBuilder底层原理

    👨‍🎓作者简介:一位大四、研0学生,正在努力准备大四暑假的实习 🌌上期文章:JAVASE进阶:String常量池内存原理分析、字符串输入源码分析 📚订阅专栏:JAVASE进阶 希望文章对你们有所帮助 这是比较重要的内容,学习原理很重要,啃源码也很重要!!! 字符串 常量 的

    2024年02月20日
    浏览(45)
  • Redis进阶底层原理-Cluster集群底层

    Redis底层原理篇

    2024年02月16日
    浏览(38)
  • 【SpringMVC】| SpringMVC执行流程原理 | 常用注解 剥析

    MVC全称Model View Controller,是一种设计创建Web应用程序的模式。这三个单词分别代表Web应用程序的三个部分: Model (模型):指数据模型。用于存储数据以及处理用户请求的业务逻辑。在Web应用中,JavaBean对象,业务模型等都属于Model。 View (视图):用于展示模型中的数据的

    2024年02月06日
    浏览(62)
  • SpringMVC基本原理

    1.Model I开发模式 Model1的开发模式是:JSP+JavaBean的模式,它的核心是Jsp页面,在这个页面中,Jsp页面负责整合页面和JavaBean(业务逻辑),而且渲染页面,它的基本流程如下: 这样的设计模式到这一个jsp页面负责了视图的展示和业务流程的控制高度耦合的结果是Jsp代码十分复杂

    2024年03月15日
    浏览(35)
  • springMVC-原理及入门案例

    (1)springMVC是以spring为基础,因此在使用时,需要先将spring jar引入. (2)SpringMVC是MVC框架,工作在WEB层,替代Strts2.可以超越struts2框架. (3)SpringMVC相对于Struts2来说,更加简洁,是一个轻量级的框架,没有struts2框架重 (4)SpringMVC通过一套注解(对POJO类),可以快速的实现功能让

    2024年02月04日
    浏览(50)
  • SpringMVC实现原理及详解

    在介绍什么是 SpringMVC 之前,我们先看看 Spring 的基本架构。如下图: 我们可以看到,在 Spring 的基本架构中,红色圈起来的 Spring Web MVC ,也就是本系列的主角 SpringMVC,它是属于Spring基本架构里面的一个组成部分,属于SpringFrameWork的后续产品,已经融合在Spring Web Flow里面,所

    2024年01月19日
    浏览(36)
  • SpringMVC组件原理剖析

    主要剖析DispatcherServlet(前端控制器 )初始化的过程,还有DispatcherServlet执行主流程 DispatcherServlet初始化做了两件事情 获得了一个 SpringMVC 的 ApplicationContext容器 注册了 SpringMVC的 九大组件 前端控制器DispatcherServlet是SpringMVC的入口,也是SpringMVC的大脑,主流程的工作都是在此完

    2024年02月08日
    浏览(40)
  • ElasticSearch 底层读写原理

    ​ 写请求是写入 primary shard,然后同步给所有的 replica shard;读请求可以从 primary shard 或 replica shard 读取,采用的是随机轮询算法。 1.选择任意一个DataNode发送请求,例如:node2。此时,node2就成为一个coordinating node(协调节点) 2.计算得到文档要写入的分片 shard = hash(routing)

    2024年04月12日
    浏览(41)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包