⭐⭐⭐⭐⭐⭐
 Github主页👉https://github.com/A-BigTree
 笔记链接👉https://github.com/A-BigTree/Code_Learning
 ⭐⭐⭐⭐⭐⭐
如果可以,麻烦各位看官顺手点个star~😊
如果文章对你有所帮助,可以点赞👍收藏⭐支持一下博主~😆
文章目录
- 9 底层原理
 - 9.1 启动过程
 - 9.1.1 初始化操作调用路线
 - 类和接口之间的关系
 - 调用路线图
 
- 9.1.2 IOC容器创建
 - 9.1.3 IOC容器对象存入应用域
 - 9.1.4 请求映射初始化
 - 9.1.5 总结
 
- 9.2 请求处理过程
 - 9.2.1 总体阶段
 - 流程描述
 - 核心代码
 
- 9.2.2 调用前阶段
 - 建立调用链
 - 调用拦截器`preHandle()`
 - 注入请求参数
 
- 9.3 `ContextLoaderListener`
 - 9.3.1 问题
 - 9.3.2 配置`ContextLoaderListener`
 - 创建spring-persist.xml并配置
 - `ContextLoaderListener`
 - `ContextLoader`
 
- 9.3.3 两个IOC之间的关系
 - 9.3.4 两个容器之间访问关系
 - 9.3.5 重复创建问题
 - 问题
 - 解决方法1
 - 解决方法2
 
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()方法都返回true:applyPreHandle()方法返回true,被取反就不执行if分支,继续执行后续操作,这就是放行; - 任何一个拦截器的 
preHandle()方法返回false:applyPreHandle()方法返回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 类附加声明式事务功能。
结论:基于 s
pring-persist.xml配置文件创建的EmpService有声明式事务的功能影响:由于
DispatcherServlet的 IOC 容器会优先使用自己创建的EmpController,进而装配自己创建的EmpService,所以基于spring-persist.xml配置文件创建的有声明式事务的EmpService用不上。 
 -  
 
解决方法1
让两个配置文件配置自动扫描的包时,各自扫描各自的组件。
- SpringMVC 就扫描 
XxxHandler、XXXController - Spring 扫描 
XxxService和XxxDao 
解决方法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>
                


















