Spring MVC 之基本工作原理
# 搭建
- 配置 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- spring 包都有 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.10</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.fengqianrun</groupId>
<artifactId>study-springMVC</artifactId>
<version>0.0.1-SNAPSHOT</version>
<!-- tomcat 认war包 -->
<packaging>war</packaging>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- 只使用mvc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
</dependencies>
</project>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
- 创建 webapp/WEB-INF 目录,并创建 web.xml 文件
<?xml version="1.0" encoding="UTF-8"?>
<!-- 这个文件是tomcat要去读取的文件,文件路径必须在 webapp/WEB-INF 下,webapp和 java是同级目录 -->
<web-app>
<servlet>
<servlet-name>app</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<!-- 指定spring.xml地址 -->
<param-value>/WEB-INF/spring.xml</param-value>
</init-param>
<!--数字只是决定初始化顺序
默认负数:客户端第一次访问才初始化
大于零:的数表示服务器启动时,初始化
数字越小越先初始化
-->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>app</servlet-name>
<!-- 访问路径前缀 -->
<url-pattern>/app/*</url-pattern>
</servlet-mapping>
</web-app>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- 创建 /WEB-INF/spring.xml 文件
<?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.fengqianrun.mvc" />
</beans>
2
3
4
5
6
7
8
9
- 配置 tomcat,Deployment 中配置 war,并修改 Application context
- 访问 url
访问地址为 http://ip:port/tomcat配置的Application context/web.xml里的servlet-name/controller/method
# DispatcherServlet 初始化讲解
DispatcherServlet 继承自 HttpServletBean,其最终父类还是 Servlet,只是实现了规范,重写了一些方法。 比如 HttpServletBean 该类重写了 init () 方法,在启动指定 DispatcherServlet 的时候,会调用被重写的 init 方法。整体 init 流程如下:
整个 DispatcherServlet 加载流程:
1. tomcat会调用servlet的init()方法
2. HttpServletBean重写了init()方法
2.1 读取web.xml里面的内容并封装
2.2 执行核心 initServletBean() 方法
2.3 initServletBean() 方法调用 initWebApplicationContext()
3. initWebApplicationContext 方法,用于创建 context 上下文
3.1 调用findWebApplicationContext()查询 web.xml 是否自定义了 contextAttribute 这个属性
3.2 没用自定义则 createWebApplicationContext(rootContext) 创建 org.springframework.web.context.support.XmlWebApplicationContext 实例,并跟父容器绑定。
3.3 给 context 设置环境信息 wac.setEnvironment(getEnvironment());
3.4 设置定义 contextConfigLocation(/WEB-INF/spring.xml) 的路径
3.5 调用 configureAndRefreshWebApplicationContext() 方法
4. configureAndRefreshWebApplicationContext 方法,配置context上下文,并初始化bean
4.1 设置上下文 ID,为`应用名称`+`servlet-name`
4.2 把已有的上下文(cotent)以及配置(config)设置到新的context中
4.3 给新的context添加一个ApplicationListener,主要为 ContextRefreshListener,当上下文刷新完毕后通知,该类被通知会调用 **FrameworkServlet.this.onApplicationEvent(event);** 方法,这个方法很重要
4.4 调用 refresh() 方法,执行 bean 的初始化操作(spring那一套),执行完调用 this.finishRefresh(); 也就通知到 4.3 中的 ContextRefreshListener对象并调用 FrameworkServlet.this.onApplicationEvent(event);
5. FrameworkServlet.this.onApplicationEvent(event);调用到DispatcherServlet.initStrategies()方法并会执行以下各种方法:
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
6. initHandlerMappings,用于把已经加载到spring容器的对象进行挑拣,把实现了 @RequestMapping | @Controller 的Bean摘出来并得到所有实现了@RequestMapping注解的方法 注册到 mappingRegister 容器中
6.1 从容器中读取到实现了HandlerMapping.class 的Bean,这里就是找我们自定义实现了HandlerMapping.class的Bean
6.2 如果没有自定义的 HandlerMapping,会加载默认的 HandlerMapping,默认有 BeanNameUrlHandlerMapping,RequestMappingHandlerMapping,RouterFunctionMapping,把找到的注册到 Bean容器中
6.2.1 RequestMappingHandlerMapping 在注册Bean的时候会执行 afterPropertiesSet()方法,该方法里面会得到所有Bean,并判断类型是否是有 Controller.class 或 RequestMapping.class 注解,如果符合条件代表你是一个 ControllerHandler
6.2.2 如果是一个 ControllerHandler,则获取该类中的方法并得到有只含有RequestMapping.class注解的方法,并且解析注解上的参数,把方法注册到一个 mappingRegistry 里
6.3 BeanNameUrlHandlerMapping 是由 ApplicationContextAware 感知调用初始化方法的
6.3.1 BeanNameUrlHandlerMapping 和 RequestMappingHandlerMapping 解析的方式不一样,BeanNameUrlHandlerMapping得到容器中所有Bean,会判断BeanName的前缀以 '/'开头并收集,并且注册到 handlerMap中
6.3.2 BeanNameUrlHandlerMapping 和其他controller写法不一样,具体要给类加 @Component("/test") 并且还有实现 implements Controller
6.3.3 找到匹配条件的方法把他维护到自己的 handlerMap 中
6.4 注意 BeanNameUrlHandlerMapping 和 RequestMappingHandlerMapping维护了不同的 handler 容器,所以相同的请求路径不会报错,如果相同,执行BeanNameUrlHandlerMapping的方法,因为优先级比RequestMappingHandlerMapping靠前
7. initHandlerAdapters,初始化方法会先把所需要的准备好加载进去
7.1 initHandlerAdapters 从spring容器中找加了 @ControllerAdvice 的Bean
7.2 得到Bean后判断加了 @ModelAttribute 的注解但不包含有 @RequestMapping注解的方法 存到 modelAttributeAdviceCache中
7.3 得到Bean后判断加了 @InitBinder 的方法 存到 initBinderAdviceCache 中
7.4 从容其中得到所有实现 RequestBodyAdvice 或 ResponseBodyAdvice 接口,记录下来
7.5 初始化 HttpRequestHandlerAdapter、SimpleControllerHandlerAdapter、RequestMappingHandlerAdapter、HandlerFunctionAdapter
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 父子容器
父子容器,就是在一个 web.xml 里面指定两个 servlet,加载不同的 spring.xml,共享一个 listener 父容器的对象。注意:父容器是会在 servlet 节点之前解析的。父子容器具体实现如下:
<?xml version="1.0" encoding="UTF-8"?>
<!-- 这个文件是tomcat要去读取的文件,文件路径必须在 webapp/WEB-INF 下,webapp和 java是同级目录 -->
<web-app>
<!-- 父容器 -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<!-- 描述bean的文件 -->
<param-value>/WEB-INF/spring2.xml</param-value>
</context-param>
<!-- 子1 -->
<servlet>
<servlet-name>app</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<!-- 指定spring.xml地址 -->
<param-value>/WEB-INF/spring.xml</param-value>
</init-param>
<!--数字只是决定初始化顺序
默认负数:客户端第一次访问才初始化
大于零:的数表示服务器启动时,初始化
数字越小越先初始化
-->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>app</servlet-name>
<!-- 访问路径前缀 -->
<url-pattern>/app/*</url-pattern>
</servlet-mapping>
<!-- 子2 -->
<servlet>
<servlet-name>app1</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<!-- 指定spring.xml地址 -->
<param-value>/WEB-INF/spring1.xml</param-value>
</init-param>
<!--数字只是决定初始化顺序
默认负数:客户端第一次访问才初始化
大于零:的数表示服务器启动时,初始化
数字越小越先初始化
-->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>app1</servlet-name>
<!-- 访问路径前缀 -->
<url-pattern>/app1/*</url-pattern>
</servlet-mapping>
</web-app>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
ContextLoaderListener 会创建一个容器 ApplicationContext,解析配置的 xml 文件,走 spring 常规的 Bean 加载流程。 这个 ApplicationContext 会被做为 servlet 的父容器被加载到 servletContext(map)中,当 servlet 被加载的时候会和父容器进行绑定,详见 DispatcherServlet 初始化讲解 3.2
# 代码取代 xml 配置
整体和 xml 是差不多的
public class MyWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
// Load Spring web application configuration
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(AppConfig.class);
// Create and register the DispatcherServlet
DispatcherServlet servlet = new DispatcherServlet(context);
ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet);
registration.setLoadOnStartup(1);
registration.addMapping("/app/*");
}
@ComponentScan("com.fengqianrun.mvc")
public class AppConfig{
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
MyWebApplicationInitializer 被加载是通过 SpringServletContainerInitializer 实现了 ServletContainerInitializer 的规范,在 SpringServletContainerInitializer 中有一个 @HandlesTypes,里面就定义了 WebApplicationInitializer,会把该类加载传入 SpringServletContainerInitializer 的 onStartup 方法,该方法里面继续调用实现了 WebApplicationInitializer 的 onStartup 方法
# 请求实现
我们都知道早出写一个 HttpServlet 并实现 service () 转发具体 doGet doPost 并实现业务逻辑。DispatcherServlet 一样实现了 HttpServlet 并重写了 service、doGet、doPost 等方法,实现具体流程是:
1. 由DispatcherServlet的父类FrameworkServlet具体实现了service、doGet、doPost方法
2. FrameworkServlet会先被调用service()方法,解析方法的请求方式是 get 还是 post,后调用HttpServlet的service()方法进一步判断是调用 doGet 还是 doPost方法
3. HttpServlet在调用FrameworkServlet具体实现了doGet或doPost方法
3.1
4. doPost实现会调用FrameworkServlet的子类DispatcherServlet的doService方法
4.1 doService 首先会打印请求的信息
4.2 把context添加到 request 的请求中
4.3 flashMapManager
4.4 调用 doDispatch 方法
5. doDispatch
5.1 得到默认的三个 mapping( BeanNameUrlHandlerMapping,RequestMappingHandlerMapping,RouterFunctionMapping,详细在DispatcherServlet 初始化讲解6.2中)
5.2 便利每个 mapping,并得到请求的路径,根据路径去找 handler(这个handler就是DispatcherServlet 初始化讲解6.2.2的方法),如果是BeanNameUrlHandlerMapping找到的是Bean,RequestMappingHandlerMapping找到的是 HanlerMethod 统一为 handler
5.3 找到handler后会封装为一个 handler执行链,这个执行链包含了拦截器,所以称为链
5.4 由于获取的 handler 是一个object,无法确定是BeanNameUrlHandlerMapping的Bean还是RequestMappingHandlerMapping的HandlerMethod,所以执行 getHandlerAdapter 进行适配
5.4.1 把已经加载的 handlerAdapters 进行便利(DispatcherServlet 初始化讲解 7.5)
5.4.2 每个 handlerAdapter 实现方式不一样,会有个统一的 supports 方法来判断是否实现了不同 mapping 的要求,并找到合适的 adapter
5.4.2.1 如 RequestMappingHandlerMapping 对应的是 RequestMappingHandlerAdapter,adapter 的 supports会判断是否是 HandlerMethod 的实例,是则得到这个 adapter
5.4.3 handler执行链开始执行拦截器,会遍历所有的拦截器执行执行前置方法,如果拦截器前置方法返回false则后面不在执行
5.4.4 拦截器执行完毕后就可以执行得到的 HandlerAdapter 的 handler 方法,去真正执行 controller 的方法,返回值会封装成 ModelAndView
5.4.4.1 检查是否限制了请求方式,比如只支持 POST 请求
5.4.4.2 判断是否开启session锁,用于对持有相同session的请求进行并发限制
5.4.4.3 执行invokeHandlerMethod方法
5.4.4.3.1 找出@InitBinder创建一个 binderFactory 工厂,该工厂是对Method请求参数做类型转换,找的是当前Method或全局的@InitBinder的转换器
5.4.4.3.2 生成ModelFactory,把@ModelAttribute的key和value,以及@SessionAttributes的key和value添加到 Model中,可以保障在controller的Model中得到数据
5.4.4.3.3 创建一个处理方法的对象,设置方法解析器(解析@RequestParam等注解或对象),返回值解析器(比如加了@ResponseBody要解析成JSON),设置binderFactory
5.4.4.3.4 创建 ModelAndViewContainer,给ModelAndViewContainer 添加解析的内容(初始化),然后具体去执行 invokAndHandler(req,ModelAndViewContainer),得到更多的信息给 ModelAndViewContainer,比如返回结果
5.4.4.3.5 最后对 ModelAndViewContainer 进行处理,判断当前请求是否进行了重定向
5.4.5 通过返回的 ModelAndView 查找并设置视图
5.4.6 执行拦截器后置方法
5.4.7 视图渲染
5.4.8 再调用拦截的执行完成方法
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31