⭐⭐⭐⭐⭐⭐
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>