SpringMVC知识回顾及查漏补缺

SpringMVC简介

什么是MVC

MVC是一种软件架构的思想,将软件按照模型、识图、控制器来划分

M为Model,模型层,指工程中的Javabean,作用是处理数据

Javabean分为两类:

  • 实体类bean:专门存储业务数据的
  • 业务处理bean:指service或dao对象,专门用于处理业务逻辑和数据访问

V为View,视图层,指工程中的html或jsp等页面,作用是与用户进行交互,展示数据

C为Controller,控制层,指工程中的servlet,作用是接收请求和响应浏览器

MVC的工作流程

用户通过视图层发送请求到服务器,在服务器中请求被控制层接收,控制层调用响应的模型层处理请求,处理完毕将结果返回到控制层,控制层再根据请求处理的结果找到相应的视图,渲染数据后最终响应给浏览器

注:三层架构分为表述层(表示层)、业务逻辑层、数据访问层,表述层表示前端页面和后端servlet

什么是SpringMVC

SpringMVC是Spring的一个后续产品,是它的一个子项目

SpringMVC是Spring为表述层开发提供一套完备的解决方案

特点

  • Spring家族原生产品,与IOC容器等基础设施无缝对接
  • 基于原生的servlet,通过功能强大的DispatcherServlet,对请求和响应进行统一处理
  • 表述层各细分领域需要解决的问题全方位覆盖,提供全面解决方案
  • 代码清晰简洁,大幅度提高开发效率
  • 内部组件化程度高,可插拔式组件即插即用,想要什么功能配置相应组件即可
  • 性能卓著,尤其适合现代大型、超大型互联网项目要求

环境配置

web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 配置SpringMVC前端控制器,对浏览器发送的请求统一进行处理 -->
<servlet>
<servlet-name>SpringMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- 通过初始化参数指定SpringMVC配置文件位置和名称 -->
<init-param>
<param-name>contextConfigLocation</param-name>
<!-- classpath:表示从类路径下查找配置文件 -->
<param-value>classpath*:spring/*.xml</param-value>
</init-param>
<!-- 将DispatcherServlet的启动时间提前到工程启动时 -->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>

SpringMVC配置文件

1
2
3
4
5
6
7
<!-- 组件扫描 -->
<context:component-scan base-package="com.zhuweitung.springmvc"/>
<!-- 配置JSP视图解析器 -->
<bean id="internalResourceViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/"/>
<property name="suffix" value=".jsp"/>
</bean>

基础功能

@RequestMapping注解

功能

将请求和处理请求的控制器方法关联起来,建立映射关系

位置
  • 标识一个类:设置映射请求的请求路径的初始信息

  • 标识一个方法:设置映射请求的请求路径的具体信息

属性
  • name:映射名称标识
  • value/path:请求路径,用数组可以表示多个请求路径,请求路径不匹配时不会报404错误
  • method:请求方式,GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE,请求方式不匹配时会报405错误
    • @GetMapping:get请求方式的RequestMapping的派生注解
    • @PostMapping:post请求方式的RequestMapping的派生注解
    • @PutMapping:put请求方式的RequestMapping的派生注解
    • @DeleteMapping:delete请求方式的RequestMapping的派生注解
  • params:通过请求的请求参数来匹配请求映射,可以通过四种表达式设置请求参数和请求映射的匹配关系,请求参数不匹配时会报400错误
    • “param”:要求请求映射所匹配的请求必须携带param请求参数
    • “!param”:要求请求映射所匹配的请求必须不能携带param请求参数
    • “param=value”:要求请求映射所匹配的请求必须携带param请求参数,且参数值为value
    • “param!=value”:要求请求映射所匹配的请求必须携带param请求参数,且参数值不为value
  • headers:通过请求的请求头信息匹配请求映射,可以通过四种表达式设置请求头信息和请求映射的匹配关系,请求头信息不匹配时不会报404错误
    • “header”:要求请求映射所匹配的请求必须携带header请求头信息
    • “!header”:要求请求映射所匹配的请求必须不能携带header请求头信息
    • “header=value”:要求请求映射所匹配的请求必须携带header请求头信息,且信息为value
    • “header!=value”:要求请求映射所匹配的请求必须携带header请求头信息,且信息不为value
Ant风格路径
  • ?:表示任意单个字符(除?和/),如@GetMapping(“he?lo”)可以匹配 /hello、/healo等路径
  • *:表示0个若多个字符
  • **:表示任意的一层或多层目录,只能使用/**/xxx的方式
路径占位符

原始方式:/deleteUser?id=1

rest方式:/deleteUser/1

@RequestMapping注解的value属性中通过占位符{xxx}表示传输的数据,再通过@PathVariable注解,将占位符所表示的数据复制给控制其方法的形参,当请求携带占位符数量不一致时会报404错误

1
2
3
4
@GetMapping("demo/{id}/{name}")
public String demo(HttpServletRequest request, @PathVariable String id, @PathVariable String name) {
return "demo";
}

SpringMVC获取请求参数

通过ServletAPI获取
1
2
3
4
5
6
7
@GetMapping("getParam")
@ResponseBody
public String getParam(HttpServletRequest request) {
System.out.println(request.getParameter("name"));
System.out.println(Arrays.toString(request.getParameterValues("hobby")));
return "success";
}
通过控制器形参获取

请求参数名必须和形参名称保持一致

1
2
3
4
5
6
7
8
@GetMapping("getParam2")
@ResponseBody
public String getParam2(String id, String name, String[] hobby) {
System.out.println(id);
System.out.println(name);
System.out.println(Arrays.toString(hobby));
return "success";
}
@RequestParam注解

@RequestParam是将请求参数和控制器方法的形参创建映射关系

@RequestParam注解一共有三个属性:

  • value:指定为形参赋值的请求参数的参数名
  • required:设置是否必须传输此请求参数,默认为true,默认情况下当value所指定的请求参数没有传输且未设置默认值时会报400错误
  • defaultValue:不管required属性值为true或false,当value所指定的请求参数没有传输时,则使用默认值为形参赋值
@RequestHeader注解

@RequestHeader是将请求头信息和控制器方法的形参创建映射关系

@RequestHeader注解的属性和用法与@RequestParam注解一致

@CookieValue注解

@CookieValue是将cookie数据和控制器方法的形参创建映射关系

@CookieValue注解的属性和用法与@RequestParam注解一致

通过POJO获取请求参数

可以在控制器方法的形参位置设置一个实体类型的形参,若浏览器传输的请求参数和实体类中的属性名一致,那么请求参数就会为此属性赋值

解决获取请求参数的乱码问题

get请求乱码需要修改tomcat的配置,conf/server.xml中给对应的Connector标签增加URIEncoding="UTF-8"属性;

post请求乱码需要在在DispatcherServlet调度请求之前处理请求的编码问题,所以需要在Filter中做处理

在web.xml中配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 配置字符集过滤器 -->
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/</url-pattern>
</filter-mapping>

域对象共享数据

使用ServletAPI向request域对象共享数据
1
2
3
4
5
6
@GetMapping("demo/{id}/{name}")
public String demo(HttpServletRequest request, @PathVariable String id, @PathVariable String name) {
request.setAttribute("id", id);
request.setAttribute("name", name);
return "demo";
}
使用ModelAndView向request域对象共享数据
1
2
3
4
5
6
7
8
@GetMapping("modelAndView")
public ModelAndView modelAndView() {
ModelAndView view = new ModelAndView();
view.addObject("id", "model");
view.addObject("name", "view");
view.setViewName("demo");
return view;
}
使用Model向request域对象共享数据
1
2
3
4
5
6
@GetMapping("model")
public String model(Model model) {
model.addAttribute("id", "2");
model.addAttribute("name", "model");
return "demo";
}
使用Map向request域对象共享数据
1
2
3
4
5
6
@GetMapping("map")
public String map(Map<String, Object> map) {
map.put("id", "3");
map.put("name", "map");
return "demo";
}
使用ModelMap向request域对象共享数据
1
2
3
4
5
6
@GetMapping("modelMap")
public String modelMap(ModelMap modelMap) {
modelMap.put("id", "4");
modelMap.put("name", "modelMap");
return "demo";
}
Model、ModelMap、Map的关系

运行类型都是BindingAwareModelMap

以上几种向request域共享对象的方法最终都会在DispatcherServlet的doDispatch方法中被处理为ModelAndView

向session域共享数据
1
2
3
4
5
6
@GetMapping("session")
public String session(HttpSession session) {
session.setAttribute("id", "5");
session.setAttribute("name", "session");
return "demo";
}
向application域共享数据
1
2
3
4
5
6
7
@GetMapping("application")
public String application(HttpServletRequest request) {
ServletContext application = request.getServletContext();
application.setAttribute("id", "6");
application.setAttribute("name", "application");
return "demo";
}

SpringMVC的视图

SpingMVC中的视图是View接口,视图的作用是渲染数据,将模型Model中的数据展示给用户

SpringMVC视图的种类很多,默认有转发视图和重定向视图

当工程引入jstl的依赖,转发视图会自动转换为JstlView

若使用的视图技术为Thymeleaf,在SpringMVC的配置文件中配置了Thymeleaf的视图解析器,由此视图解析器解析之后所得到的是ThymeleafView

转发视图

SpringMVC中默认的转发视图是InternalResourceView(当配置的视图解析器不为InternalResourceViewResolver时可以debug看到差别,不然都是InternalResourceView)

SpringMVC中创建转发视图的情况:

当控制器方法中所设置的视图名称以"forward:"为前缀时,创建InternalResourceView视图,此时的视图名称不会被SpringMVC配置文件中所配置的视图解析器解析,而是会将前缀"forward:"去掉,剩余部分作为最终路径通过转发的方式实现跳转

1
2
3
4
@GetMapping("forward")
public String forward() {
return "forward:/view/default";
}
重定向视图

SpringMVC中默认的重定向视图是RedirectView

当控制器方法中所设置的视图名称以"redirect:"为前缀时,创建RedirectView视图,此时的视图名称不会被SpringMVC配置文件中所配置的视图解析器解析,而是会将前缀"redirect:"去掉,剩余部分作为最终路径通过重定向的方式实现跳转

1
2
3
4
@GetMapping("redirect")
public String redirect() {
return "redirect:/view/default";
}
视图控制器

当控制器方法中,仅仅用来实现页面跳转,即只需要设置视图名称时,可以将处理器方法使用view-controller标签进行表示

1
2
3
4
<!-- 访问testView的请求跳转到success页面 -->
<mvc:view-controller path="/testView" view-name="success"></mvc:view-controller>
<!-- 当SpringMVC中设置任何一个view-controller时,其他控制器中的请求映射将全部失效,此时需要在SpringMVC的核心配置文件中设置开启mvc注解驱动 -->
<mvc:annotation-driven />

RESTful

简介

REST:Representational State Transfer,表现层资源状态转移

RESTful的实现

HTTP 协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE

它们分别对应四种基本操作:GET 用来获取资源,POST 用来新建资源,PUT 用来更新资源,DELETE 用来删除资源

REST 风格提倡 URL 地址使用统一的风格设计,从前到后各个单词使用斜杠分开,不使用问号键值对方式携带请求参数,而是将要发送给服务器的数据作为 URL 地址的一部分,以保证整体风格的一致性

操作 传统方式 REST风格
查询操作 getUserById?id=1 user/1–>get请求方式
保存操作 saveUser user–>post请求方式
删除操作 deleteUser?id=1 user/1–>delete请求方式
更新操作 updateUser user–>put请求方式

通过过滤器处理delete和put请求

由于浏览器只支持发送get和post方式的请求,发送put和delete请求需要使用SpringMVC 提供的HiddenHttpMethodFilter将 POST 请求转换为 DELETE 或 PUT 请求

web.xml中配置过滤器(需要配置在字符集过滤器后面,否则字符集过滤器就会失效)

1
2
3
4
5
6
7
8
9
<!-- 配置HiddenHttpMethodFilter -->
<filter>
<filter-name>HiddenHttpMethodFilter</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>HiddenHttpMethodFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

静态资源访问

在spring配置文件中配置:

1
2
3
4
<!-- 开放对静态资源的访问 -->
<mvc:default-servlet-handler/>
<!-- 开启mvc注解驱动 -->
<mvc:annotation-driven/>

第一个配置表示开启默认的Servlet请求处理器,而对静态资源的访问就是通过容器默认servlet处理(DefaultServletHttpRequestHandler)的,资源的请求过程为:先到DispatcherServlet处理,发现无法处理,就到默认Servlet处理

当开启第一个配置之后@RequestMapping注解方式配置的处理器就会失效,这时就需要加上第二个配置,让MVC注解生效

扩展功能

HttpMessageConverter

HttpMessageConverter,报文信息转换器,将请求报文转换为Java对象,或将Java对象转换为响应报文

HttpMessageConverter提供了两个注解和两个类型:@RequestBody,@ResponseBody,RequestEntity,ResponseEntity

@RequestBody

@RequestBody可以获取请求体,需要在控制器方法设置一个形参,使用@RequestBody进行标识,当前请求的请求体就会为当前注解所标识的形参赋值

@ResponseBody

@ResponseBody用于标识一个控制器方法,可以将该方法的返回值直接作为响应报文的响应体响应到浏览器

处理Json的步骤

  • 引入json依赖,如jackson、fastjson
  • 在spring配置文件中开启mvc注解驱动(<mvc:annotation-driven/>)
  • HandlerAdaptor中会自动装配一个消息转换器,如引入的是jackson,则为MappingJackson2HttpMessageConverter,可以将响应到浏览器的Java对象转换为json格式的字符串
1
2
3
4
5
@GetMapping("user/{name}")
@ResponseBody
public List<User> updateName(@PathVariable String name) {
return userService.findAllByName(name);
}
RequestEntity

RequestEntity封装请求报文的一种类型,需要在控制器方法的形参中设置该类型的形参,当前请求的请求报文就会赋值给该形参,可以通过getHeaders()获取请求头信息,通过getBody()获取请求体信息

ResponseEntity

ResponseEntity用于控制器方法的返回值类型,该控制器方法的返回值就是响应到浏览器的响应报文

@RestController

@RestController注解是springMVC提供的一个复合注解,标识在控制器的类上,就相当于为类添加了@Controller注解,并且为其中的每个方法添加了@ResponseBody注解

文件上传和下载

文件下载
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@GetMapping("download")
public void download(HttpServletRequest request, HttpServletResponse response) throws IOException {
ServletContext ctx = request.getServletContext();
String filename = "logo.png";
String filePath = "/static/img/" + filename;
InputStream is = ctx.getResourceAsStream(filePath);
// 通过响应头设置返回数据类型
response.setContentType(ctx.getMimeType(filePath));
// 通过响应头设置数据用于下载
String headerFilename = filename;
String ua = request.getHeader("User-Agent");
if (ua.toLowerCase().contains("firefox")) {
// 火狐浏览器,文件名base64编码防止乱码
headerFilename = "=?utf-8?B?" + new BASE64Encoder().encode(filename.getBytes(StandardCharsets.UTF_8)) + "?=";
} else {
// IE、谷歌浏览器,文件名url编码防止乱码
headerFilename = URLEncoder.encode(filename, "utf-8");
}
response.setHeader("Content-Disposition", "attachment;filename=" + headerFilename);
IOUtils.copy(is, response.getOutputStream());
}
文件上传

在spring配置文件中配置文件上传解析器:

1
2
<!-- 配置文件上传解析器 id必须为multipartResolver -->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"/>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@PostMapping("upload")
@ResponseBody
public String upload(HttpServletRequest request, MultipartFile file) throws IOException {
String originalFilename = file.getOriginalFilename();
String extName = FileUtil.extName(originalFilename);
if (StrUtil.isNotBlank(extName)) {
extName = "." + extName;
}
String fileName = UUID.fastUUID().toString(true) + extName;
ServletContext ctx = request.getServletContext();
String uploadFilePath = ctx.getRealPath("/upload") + File.separator + DateUtil.today() + File.separator + fileName;
FileUtil.mkParentDirs(uploadFilePath);
file.transferTo(new File(uploadFilePath));
return "success";
}

拦截器

过滤器作用于浏览器请求到DispatcherServlet之间

SpringMVC中的拦截器用于拦截控制器方法的执行

SpringMVC中的拦截器需要实现HandlerInterceptor

SpringMVC的拦截器必须在Spring的配置文件中进行配置

拦截器的配置
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 配置拦截器 -->
<mvc:interceptors>
<!-- 对DispatcherServlet所处理的所有请求进行拦截 -->
<!-- <bean class="com.zhuweitung.springmvc.interceptor.GlobalHandlerInterceptor"/>-->
<!-- 注册为组件后可以使用此方式,效果与上面的方式一致 -->
<ref bean="globalHandlerInterceptor"/>
<!-- 对指定请求路径进行拦截配置 -->
<mvc:interceptor>
<mvc:mapping path="/admin/**"/>
<mvc:exclude-mapping path="/admin/hello"/>
<ref bean="adminHandlerInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
抽象方法

拦截器有三个抽象方法:

  • preHandle:控制器方法执行之前执行preHandle(),其boolean类型的返回值表示是否拦截或放行,返回true为放行,即调用控制器方法;返回false表示拦截,即不调用控制器方法

  • postHandle:控制器方法执行之后执行postHandle()

  • afterComplation:处理完视图和模型数据,渲染视图完毕之后执行afterComplation()

执行顺序

若每个拦截器执行preHandle都放行:

多个拦截器的执行顺序和拦截器在SpringMVC的配置文件的配置顺序有关

preHandle()会按照配置的顺序执行,而postHandle()和afterComplation()会按照配置的反序执行

1
2
3
4
5
6
com.zhuweitung.springmvc.interceptor.GlobalHandlerInterceptor preHandle
com.zhuweitung.springmvc.interceptor.AdminHandlerInterceptor preHandle
com.zhuweitung.springmvc.interceptor.AdminHandlerInterceptor postHandle
com.zhuweitung.springmvc.interceptor.GlobalHandlerInterceptor postHandle
com.zhuweitung.springmvc.interceptor.AdminHandlerInterceptor afterCompletion
com.zhuweitung.springmvc.interceptor.GlobalHandlerInterceptor afterCompletion

若某个拦截器执行preHandle没有放行:

preHandle()返回false和它之前的拦截器的preHandle()都会执行,postHandle()都不执行,返回false的拦截器之前的拦截器的afterComplation()会执行

1
2
3
com.zhuweitung.springmvc.interceptor.GlobalHandlerInterceptor preHandle
com.zhuweitung.springmvc.interceptor.AdminHandlerInterceptor preHandle
com.zhuweitung.springmvc.interceptor.GlobalHandlerInterceptor afterCompletion

HandlerExecutionChain相关源码:

1
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
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
HandlerInterceptor[] interceptors = getInterceptors();
if (!ObjectUtils.isEmpty(interceptors)) {
// 拦截器正序执行
for (int i = 0; i < interceptors.length; i++) {
HandlerInterceptor interceptor = interceptors[i];
if (!interceptor.preHandle(request, response, this.handler)) {
// 拦截器执行preHandle返回false后直接执行afterCompletion
triggerAfterCompletion(request, response, null);
return false;
}
// 记录当前拦截器索引
this.interceptorIndex = i;
}
}
return true;
}

void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv)
throws Exception {
// 所有拦截器preHandle都返回true才会到这里
HandlerInterceptor[] interceptors = getInterceptors();
if (!ObjectUtils.isEmpty(interceptors)) {
// 拦截器倒序遍历
for (int i = interceptors.length - 1; i >= 0; i--) {
HandlerInterceptor interceptor = interceptors[i];
interceptor.postHandle(request, response, this.handler, mv);
}
}
}

void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex)
throws Exception {
HandlerInterceptor[] interceptors = getInterceptors();
if (!ObjectUtils.isEmpty(interceptors)) {
// 从记录的拦截器索引位置开始倒序遍历
for (int i = this.interceptorIndex; i >= 0; i--) {
HandlerInterceptor interceptor = interceptors[i];
try {
interceptor.afterCompletion(request, response, this.handler, ex);
}
catch (Throwable ex2) {
logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
}
}
}
}

异常处理

SpringMVC提供了一个处理控制器方法执行过程中所出现的异常的接口:HandlerExceptionResolver

HandlerExceptionResolver接口的实现类有:DefaultHandlerExceptionResolver和SimpleMappingExceptionResolver

SpringMVC提供了自定义的异常处理器SimpleMappingExceptionResolver

基于配置的异常处理
1
2
3
4
5
6
7
8
<!-- 自定义异常处理 -->
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<props>
<prop key="com.zhuweitung.springmvc.exception.SimpleException">error</prop>
</props>
</property>
</bean>
基于注解的异常处理

@ControllerAdvice:将当前类标识为异常处理的组件

@ExceptionHandler:用于设置所标识方法处理的异常

1
2
3
4
5
6
7
8
9
10
@ControllerAdvice
public class SimpleExceptionAdvice {

@ExceptionHandler(value = {SimpleException.class})
public String handleSimpleException(Exception exception, Model model) {
model.addAttribute("exception", exception);
return "error";
}
}

注解配置SpringMVC

使用配置类和注解代替web.xml和SpringMVC配置文件的功能

初始化类代替web.xml

在Servlet3.0环境中,容器会在类路径中查找实现javax.servlet.ServletContainerInitializer接口的类,如果找到的话就用它来配置Servlet容器。 Spring提供了这个接口的实现,名为SpringServletContainerInitializer,这个类反过来又会查找实现WebApplicationInitializer的类并将配置的任务交给它们来完成。Spring3.2引入了一个便利的WebApplicationInitializer基础实现,名为AbstractAnnotationConfigDispatcherServletInitializer,当我们的类扩展了AbstractAnnotationConfigDispatcherServletInitializer并将其部署到Servlet3.0容器的时候,容器会自动发现它,并用它来配置Servlet上下文。

1
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
public class WebInit extends AbstractAnnotationConfigDispatcherServletInitializer {
/**
* 指定spring配置类
* 指定
*/
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{SpringConfig.class};
}
/**
* 指定SpringMVC配置类
*/
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{WebConfig.class};
}
/**
* 指定DispatcherServlet的映射路径
*/
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
/**
* 设置过滤器
*/
@Override
protected Filter[] getServletFilters() {
// 配置字符集过滤器
CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter("UTF-8");
// 配置HiddenHttpMethodFilter
HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
return new Filter[]{characterEncodingFilter, hiddenHttpMethodFilter};
}
}

SpringConfig配置类代替spring配置文件

1
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
@Configuration
@EnableTransactionManagement // 开启事务
@PropertySource("classpath:hikari.properties")
public class SpringConfig {
@Value("${dataSourceClassName}")
private String dataSourceClassName;
@Value("${dataSource.driverClassName}")
private String driverClassName;
@Value("${dataSource.jdbcUrl}")
private String jdbcUrl;
@Value("${dataSource.username}")
private String username;
@Value("${dataSource.password}")
private String password;
@Value("${minimumIdle}")
private Integer minimumIdle;
@Value("${maximumPoolSize}")
private Integer maximumPoolSize;
@Bean
public DataSource hikariDataSource() {
Properties properties = new Properties();
properties.put("dataSourceClassName", dataSourceClassName);
properties.put("dataSource.driverClassName", driverClassName);
properties.put("dataSource.jdbcUrl", jdbcUrl);
properties.put("dataSource.username", username);
properties.put("dataSource.password", password);
properties.put("minimumIdle", minimumIdle);
properties.put("maximumPoolSize", maximumPoolSize);
return new HikariDataSource(new HikariConfig(properties));
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource hikariDataSource) {
return new JdbcTemplate(hikariDataSource);
}
@Bean
public DataSourceTransactionManager dataSourceTransactionManager(DataSource hikariDataSource) {
return new DataSourceTransactionManager(hikariDataSource);
}
}

WebConfig配置类代替SpringMVC配置文件

1
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
@Configuration
@EnableWebMvc // 开启mvc注解驱动
@ComponentScan(basePackages = "com.zhuweitung.springmvc") // 组件扫描
public class WebConfig implements WebMvcConfigurer {
/**
* 配置JSP视图解析器
*/
@Bean
public InternalResourceViewResolver internalResourceViewResolver() {
return new InternalResourceViewResolver("/WEB-INF/", ".jsp");
}
/**
* 配置文件上传解析器
*/
@Bean
public MultipartResolver multipartResolver() {
return new CommonsMultipartResolver();
}
/**
* 开放对静态资源的访问
*/
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
/**
* 配置拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new GlobalHandlerInterceptor());
registry.addInterceptor(new AdminHandlerInterceptor()).addPathPatterns("/admin/**").exclude
}
}

SpringMVC执行流程

SpringMVC常用组件

  • DispatcherServlet:前端控制器,不需要工程师开发,由框架提供

    • 作用:统一处理请求和响应,整个流程控制的中心,由它调用其它组件处理用户的请求
  • HandlerMapping:处理器映射器,不需要工程师开发,由框架提供

    • 作用:根据请求的url、method等信息查找Handler,即控制器方法
  • Handler:处理器(Controller),需要工程师开发

    • 作用:在DispatcherServlet的控制下Handler对具体的用户请求进行处理
  • HandlerAdapter:处理器适配器,不需要工程师开发,由框架提供

    • 作用:通过HandlerAdapter对处理器(控制器方法)进行执行
  • ViewResolver:视图解析器,不需要工程师开发,由框架提供

    • 作用:进行视图解析,得到相应的视图,例如:ThymeleafView、InternalResourceView、RedirectView
  • View:视图

    • 作用:将模型数据通过页面展示给用户

DispatcherServlet初始化过程

DispatcherServlet 本质上是一个 Servlet,所以天然的遵循 Servlet 的生命周期,DispatcherServlet的初始化入口也是init(ServletConfig config)方法

首先DispatcherServlet中没有重写init(ServletConfig config)方法,向上找它的父类,发现由GenericServlet实现了,里面调用了init()方法

1
2
3
4
5
6
7
public void init(ServletConfig config) throws ServletException {
this.config = config;
// 该处理为空实现,留给子类
this.init();
}
public void init() throws ServletException {
}

HttpServletBean重写了init()方法,然后调用了initServletBean()方法,进行封装初始化参数

1
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
@Override
public final void init() throws ServletException {
// Set bean properties from init parameters.
// 解析web.xml中init-param 并封装至pvs中
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
if (!pvs.isEmpty()) {
try {
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
// 该处理为空实现,留给子类,使用模板模式
initBeanWrapper(bw);
// 属性的注入
bw.setPropertyValues(pvs, true);
}
catch (BeansException ex) {
if (logger.isErrorEnabled()) {
logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
}
throw ex;
}
}
// Let subclasses do whatever initialization they like.
// 初始化Servlet对象,模板模式留给子类扩展
initServletBean();
}
protected void initServletBean() throws ServletException {
}

FrameworkServlet重写了initServletBean()方法,里面调用了initWebApplicationContext()方法,进行对WebApplicationContext的初始化

1
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
protected WebApplicationContext initWebApplicationContext() {
// Srping的IOC容器
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
// SpringMVC的IOC容器
WebApplicationContext wac = null;
// ...
if (wac == null) {
// 根据contextAttribute属性加载
wac = findWebApplicationContext();
}
if (wac == null) {
// 创建IOC容器
wac = createWebApplicationContext(rootContext);
}
if (!this.refreshEventReceived) {
synchronized (this.onRefreshMonitor) {
// 刷新容器,该处理为空实现,留给子类
onRefresh(wac);
}
}
if (this.publishContext) {
// 将IOC容器在应用域共享
String attrName = getServletContextAttributeName();
getServletContext().setAttribute(attrName, wac);
}
return wac;
}

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());
// 将Spring的IOC容器设置为父容器
wac.setParent(parent);
String configLocation = getContextConfigLocation();
if (configLocation != null) {
wac.setConfigLocation(configLocation);
}
// 初始化Spring环境加载配置文件
configureAndRefreshWebApplicationContext(wac);
return wac;
}

DispatcherServlet重写了onRefresh方法,方法里面会初始化策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
protected void onRefresh(ApplicationContext context) {
initStrategies(context);
}

protected void initStrategies(ApplicationContext context) {
// 初始化文件上传解析器
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
// 初始化处理器映射器
initHandlerMappings(context);
// 初始化处理器适配器
initHandlerAdapters(context);
// 初始化异常处理器
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
// 初始化视图解析器
initViewResolvers(context);
initFlashMapManager(context);
}

DispatcherServlet调用组件处理请求

首先DispatcherServlet中没有实现service(ServletRequest req, ServletResponse res)方法,从它的父类找,发现HttpServlet实现了,里面又调用了service(HttpServletRequest req, HttpServletResponse resp)

这个方法被FrameworkServlet重写,里面又调用processRequest方法,processRequest方法中调用了doService方法,该方法为空方法,被DispatcherServlet实现

1
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
@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
logRequest(request);
// Keep a snapshot of the request attributes in case of an include,
// to be able to restore the original attributes after the include.
Map<String, Object> attributesSnapshot = null;
if (WebUtils.isIncludeRequest(request)) {
attributesSnapshot = new HashMap<>();
Enumeration<?> attrNames = request.getAttributeNames();
while (attrNames.hasMoreElements()) {
String attrName = (String) attrNames.nextElement();
if (this.cleanupAfterInclude || attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) {
attributesSnapshot.put(attrName, request.getAttribute(attrName));
}
}
}
// Make framework objects available to handlers and view objects.
request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());
if (this.flashMapManager != null) {
FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
if (inputFlashMap != null) {
request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
}
request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
}
try {
// 处理请求和响应
doDispatch(request, response);
}
finally {
if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Restore the original attribute snapshot, in case of an include.
if (attributesSnapshot != null) {
restoreAttributesAfterInclude(request, attributesSnapshot);
}
}
}
}

doService方法中调用了doDispatch方法

1
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
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
// 拦截器调用链
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
// 通过控制器方法创建相应的处理器适配器,调用所对应的控制器方法
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
// 调用拦截器的preHandle()
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
// 由处理器适配器调用具体的控制器方法,最终获得ModelAndView对象
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
// 调用拦截器的postHandle()
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
// 后续处理:处理模型数据和渲染视图
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
// ...
}

doDispatch方法中调用了processDispatchResult方法

1
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
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {
boolean errorView = false;
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
}
else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}
// Did the handler return a view to render?
if (mv != null && !mv.wasCleared()) {
// 处理模型数据和渲染视图
render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
else {
if (logger.isTraceEnabled()) {
logger.trace("No view rendering, null ModelAndView returned.");
}
}
if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Concurrent handling started during a forward
return;
}
if (mappedHandler != null) {
// Exception (if any) is already handled..
// 调用拦截器的afterCompletion()
mappedHandler.triggerAfterCompletion(request, response, null);
}
}

SpringMVC执行流程

  • 用户向服务器发送请求,请求被前端控制器DispatcherServlet捕获
  • DispatcherServlet对请求URL进行解析,得到请求资源标识符(URI),判断请求URI对应的映射
    • 若映射不存在
      • 判断是否配置了mvc:default-servlet-handler
      • 若没配置,客户端显示404错误
      • 若有配置,则访问目标资源(一般为静态资源),找不目标资源时客户端显示404错误
    • 若映射存在
      • 根据该URI,调用HandlerMapping获取该Handler配置的所有相关对象(Handler对象以及对于的拦截器),最后以HandlerExecutionChain执行链对象的形式返回
      • DispatcherServlet得到Handler,获取HandlerAdapter
      • 获得HandlerAdapter后,执行拦截器的preHandler方法
      • 提取请求中的模型数据,填充到Handler入参,执行Handler(Controller)方法,处理请求
        • 填充数据模型到入参时,spring还做了下面工作
        • HttpMessageConveter:将请求消息转换为一个对象
        • 数据类型转换:如将String转换为Integer
        • 数据格式化:如将字符串转换成格式化数字或格式化日期等
        • 数据验证:验证数据的有效性(长度、格式等),验证结果存储到BindingResult或Error中
      • Handler执行完成后,向DispatcherServlet 返回一个ModelAndView对象
      • 开始执行拦截器的postHandle方法
      • 根据返回的ModelAndView选择一个适合的ViewResolver进行视图解析,根据Model和View,来渲染视图
        • 此时会判断是否存在异常,如果存在异常,则执行HandlerExceptionResolver进行异常处理
      • 渲染视图完毕执行拦截器的afterCompletion方法
      • 将渲染结果返回给客户端

SpringMVC知识回顾及查漏补缺
https://blog.kedr.cc/posts/3435866348/
作者
zhuweitung
发布于
2021年1月21日
许可协议