Java - Spring MVC
Spring MVC 正式名称叫 Spring Web MVC,它是 Spring Framework 中的一部分。Spring MVC 实现了 MVC 架构
- M(Module),处理业务,最终返回数据,属于 Service 和 DAO
- V(View),展示数据,模板引擎 Velocity/FreeMarker/Thymeleaf
- C(Controller),处理用户的请求和响应,作为 M 和 V 的调度。
MVC(Model-View-Controller)与三层模型(Three-Tier Architecture)都是分层它两有啥区别?三层中视图层包含了 Controller 和 View,而业务层和持久层用 Model。
目录
- 目录
- 1 请求处理
- 1.1 @RequestMapping 请求映射
- 1.1.1 路径映射元素 value 和 path
- 1.1.2 method 元素
- 1.1.3 params 元素
- 1.1.4 headers 元素
- 1.1.5 consumes
- 1.1.6 produces
- 1.2 请求方法快速映射
- 1.3 请求获取
- 1.3.1 HttpServletRequest 获取请求
- 1.3.2 @RequestParam 获取请求参数
- 1.3.3 获取请求头
- 1.3.4 @RequestBody 获取请求体
- 1.3.5 RequestEntity 请求详情
- 1.3.6 处理 multipart/form-data 请求
- 1.3.7 JSR-303 数据校验🔨
- 1.4 三个域对象
- 2 响应处理
- 3 异常处理器
- 4 拦截器
- 5 Spring MVC 注解方式开发
- 6 部署应用
1 请求处理
1.配置环境依赖
使用 Spring MVC 要配置 Maven 依赖,下载前记得先改项目 Maven 配置文件和仓库位置。
<!--设置打包方式-->
<packaging>war</packaging>
<dependencies>
<!-- Spring MVC 依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>6.1.10</version>
</dependency>
<!-- Spring Web MVC 默认支持的日志框架 Logback 依赖 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.7</version>
</dependency>
<!-- Servlet 依赖 -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
<scope>provided</scope>
</dependency>
<!-- Spring6 和 Thymeleaf 整合依赖 -->
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring6</artifactId>
<version>3.1.2.RELEASE</version>
</dependency>
</dependencies>
设置为打包方式为 war 包,使用 IDEA 的 Tomcat 部署才不会出错。
spring-webmvc,会自动引入 Spring 6 中的各种依赖,比如 AOP 和 IoC。
日志使用 Logback 是因为 Spring MVC 默认就支持 Logback 我们只需要引入依赖就能使用它,无需主动配置就能得到很好的日志支持。
引入 Servlet,Spring MVC 是 底层使用的是 Servlet 处理请求和响应,选用 6.0.0 版本是因为 Tomcat 10.1.x 就实现了这个版本规范,我们通过设置 provided 就不会打包到最终 war 包里,节省空间,直接去使用 Tomcat 提供的依赖就好。
thymeleaf-spring6 是 Thymeleaf 模板引擎在 Spring6 中的支持,它会自动引入 Thymeleaf 依赖。
2.建立 Web 目录
001-RequestHandler
├─ 001-RequestHandler.iml
├─ pom.xml
├─ src
│ ├─ main
│ │ ├─ java
│ │ ├─ resources
│ │ └─ web
│ │ └─ WEB-INF
│ │ └─ web.xml
│ └─ test
│ └─ java
└─ target
└─ classes
3.配置 DispatcherServlet
DispatcherServlet 叫前端控制器,它是所有请求的入口,由它分配。
既然需要转发到这个 Servlet 上,那必须启用这个类的路径映射
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
version="6.0">
<!-- 配置前端控制器 -->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>0</load-on-startup>
<!-- 自定义 Web 配置文件名称和存放路径 -->
<!--<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/application.xml</param-value>
</init-param>-->
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
这里设置的是 /,意思是在访问 / 或者其子路径资源时都交给 DispatcherServlet 处理,但是 .jsp 的除外,它本身发就是 Servlet 可以自己处理请求。
DispatcherServlet 这个 Servlet 我们还配置 <load-on-startup>
让其在容器启动时自动实例化,这是因为第一次访问实例化需要些时间,避免用户感知到系统初始化卡顿,所以提前初始化。
4.创建 Controller
这里创建了 com.raingray.controller 包,在里面放了个 Index.Controller.java 控制器。
package com.raingray.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class IndexContrller {
@RequestMapping("/")
public String index() {
return "index";
}
}
这里在 IndexController 控制器类上使用 @Controller 是后面需要放入 IoC 容器管理。
里面的 index 方法上面也有个注解 @RequestMapping 是用来定义请求路径相关的信息,这里给 value 赋值成 /,代表访问 / 时 DispatcherServlet 会通过 @RequestMapping 找到这个方法,最后 return 的这个 index 字符串是视图文件名,有了名称才能结合 thymeleaf 配置的路径找到具体物理视图渲染数据。
这里还没创建视图,直接在 WEB-INF 下面建立个 templates 目录,里面创一个 index 文件名的模板,后缀随便取,但是一般大家都用 html,所以这里我就创建成 index.html。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>this is title</title>
</head>
<body>
<h1>test index template</h1>
</body>
</html>
如果你想用 thymeleaf 模板的标签,一定要在 html 标签添加属性 xmlns:th="http://www.thymeleaf.org"
,没加就是个普通的 html 文件,加上了才是模板文件。
5.编写 Spring MVC 配置文件
前面只是创建了控制器和模板,但是得让他们生效啊,所以通过编写 Spring MVC 配置文件配置它们,Spring MVC 配置文件实际上就是个 Spring 配置文件。
编写这个配置文件,对文件名和存放路径有个默认约定,文件名是前端控制器 Servlet 名字加上 -servlet.xml
,路径存放在 WEB-INF 目录下。这里 Servlet 名字是 springmvc,那么完整的配置文件名是 springmvc-servlet.xml。
如果不想要默认配置名称,在定义前端控制器这个 Servlet 初始化参数 contextConfigLocation。
<!-- 自定义 Spring MVC 配置文件名称和存放路径 -->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/application.xml</param-value>
</init-param>
这里我就用默认名称来配置,在 WEB-INF 下创建 springmvc-servlet.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.raingray.controller"/>
<!--配置视图解析器-->
<bean id="thymeleafViewResolver" class="org.thymeleaf.spring6.view.ThymeleafViewResolver">
<!--作用于视图渲染的过程中,可以设置视图渲染后输出时采用的编码字符集。响应给客户端的字符集。-->
<property name="characterEncoding" value="UTF-8"/>
<!--如果配置多个视图解析器,它来决定优先使用哪个视图解析器,它的值越小优先级越高-->
<property name="order" value="1"/>
<!--当 ThymeleafViewResolver 渲染模板时,会使用该模板引擎来解析、编译和渲染模板-->
<property name="templateEngine">
<bean class="org.thymeleaf.spring6.SpringTemplateEngine">
<!--用于指定 Thymeleaf 模板引擎使用的模板解析器。模板解析器负责根据模板位置、模板资源名称、文件编码等信息,加载模板并对其进行解析-->
<property name="templateResolver">
<bean class="org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver">
<!--设置模板文件的位置(前缀)-->
<property name="prefix" value="/WEB-INF/templates/"/>
<!--设置模板文件后缀(后缀),Thymeleaf文件扩展名不一定是html,也可以是其他,例如txt,大部分都是html-->
<property name="suffix" value=".html"/>
<!--设置模板类型,例如:HTML,TEXT,JAVASCRIPT,CSS等-->
<property name="templateMode" value="HTML"/>
<!--用于模板文件在读取和解析过程中采用的编码字符集。模板文件中字符的字符集-->
<property name="characterEncoding" value="UTF-8"/>
</bean>
</property>
</bean>
</property>
</bean>
</beans>
这里主要配置了两部分,首先是扫描包中的注解注册成 Bean,另一个是 Thymeleaf 和 Spring6 整合的配置,由于没专门学习 Thymeleaf 怎么使用,这里就不做介绍了,看看注释简单理解下。
6.部署应用
怎么用 IDEA 部署在博客 Servlet 和 Maven 文章中讲过,不再赘述。
1.1 @RequestMapping 请求映射
前面使用 @RequestMapping 映射一个路径请求,但是这个注解还有更多元素可以用来做限制,比如用什么请求方法、参数、路径和头才能映射到这个方法上。
1.1.1 路径映射元素 value 和 path
value 和 path,它两互为别名,都是用来映射路径,要和别的元素一起使用时,一般来用 value 多。下面定义了一个路由 /,访问这个 / 就执行这个 index 方法,最终返回逻辑视图 index。
@RequestMapping("/")
public String index() {
return "index";
}
使用上不能定义一样的路径。
package com.raingray.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class IndexContrller {
@RequestMapping("/")
public String index() {
return "index";
}
@RequestMapping("/")
public String test() {
return "index";
}
}
java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'indexContrller' method
com.raingray.controller.IndexContrller#test()
to { [/]}: There is already 'indexContrller' bean method
com.raingray.controller.IndexContrller#index() mapped.
路径上还可以使用 Ant 风格 https://blog.csdn.net/islautao/article/details/131503390
另一个用法是放在类上,相当于统一加前缀,免得在方法上重复写。
@RequestMapping("/test")
@Controller
public class IndexContrller {
@RequestMapping(value = "/1az", method = RequestMethod.POST)
public String index() {
return "index";
}
@PostMapping
@RequestMapping("/2az")
public String test() {
return "index";
}
}
这里放在类上,相当于给所有方法的路径加了前缀 /test,避免下面这样的重复写相同路径。
@Controller
public class IndexContrller {
@RequestMapping(value = "/test/1az", method = RequestMethod.POST)
public String index() {
return "index";
}
@PostMapping
@RequestMapping("/test/2az")
public String test() {
return "index";
}
}
知道基本路径怎么配,这个路径中还支持类似于正则的语法,让路径匹配起来更灵活,匹配模式有 PathPattern 和 AntPathMatcher 两种,它两语法大部分类似,但从 Sprint MVC 6 开始默认使用性能更好的 PathPattern。
PathPattern 使用以下规则匹配 URL 路径:
?
,匹配一个字符*
,匹配一个路径段中的零个或多个字符**
, 匹配零个或多个路径,直到路径结束。只能在路径结尾使用,比如/1/**
正确,而/test/**/login
则错误{spring}
, 匹配一个路径,并将其放到为一个名为 "spring" 的变量{spring:[a-z]+}
,匹配路径段中的 regexp [a-z]+,并将其作为名为 "spring" 的路径变量捕获{*spring}
,匹配零个或多个路径段,直到路径结束,并将其存放到名为 "spring" 的路径变量。注:与 AntPathMatcher 不同的是,
**
仅在模式末尾受支持。例如/pages/{**}有效
,但/pages/{**}/details
无效。这同样适用于捕获变量{*spring}
。这样做的目的是在比较模式的特异性时消除歧义。示例
/pages/t?st.html
,与 /pages/test.html 和 /pages/tXst.html 匹配,但不匹配 /pages/toast.html/resources/*.png
,匹配资源目录中的所有 .png 文件/resources/**
,匹配 /resources/ 路径下的所有文件,包括 /resources/image.png 和 /resources/css/spring.css/resources/{*path}
, 匹配 /resources/ 路径下的所有文件,以及 /resources,并在名为 "path" 的变量中捕获其相对路径;/resources/image.png 将与 "path" → "/image.png" 匹配,而 /resources/css/spring.css 将与 "path" → "/css/spring.css" 匹配。但是/resources/{*path}/tets
会失败,这种语法不允许,因为 * 也必须放在后面。/resources/{filename:\\w+}.dat
,将匹配 /resources/spring.dat,并将值 "spring" 赋给 filename 变量
上面这些匹配规则除了 **
以外都类似于正则,结合例子不难理解。其中大括号 {}
说是可以将路径中的内容作为变量存储,这个变量需要结合 @PathVariable 注解获取,下面看看如何使用。
@RequestMapping("/resources/{*path}")
public String test1(@PathVariable("path") String pathName) {
System.out.println(path);
return "index";
}
通过定义路径模式 /resources/{*path}
,这样会匹配 /resources 后面所有内容放到变量 path 中。
@PathVariable 怎么读呢?它有 path 和 value 两个元素互为别名,只能在参数上用,我们给这两个元素传递的值就是路径模式中定义的变量名 path,这时候 Spring MVC 会自动给方法的参数 pathName 传递路径上的变量。
假如访问 localhost/resources/123ljksa/zsaklj,会得到 /123ljksa/zsaklj,把它放到 path 变量中再由 @PathVariable 注解复制给实例方法 String 类型形参上。
再进一步想想实例方法形参类型是怎么决定的?为什么这里要用 String 呢?我就不能指定其他类型吗?当然可以,这里用 String 的原因是因为获取到的内容 /123ljksa/zsaklj 本来根据类型传递过来就是字符,所以用 String 接收。假如用 int 接收那么它会尝试把 String 转换成 int,存在失败的可能,如果只是传递数字 id /resources/{id}
,那么我们当然就能用 int 接收。
1.1.2 method 元素
method 元素可以用来限制这个路径必须用什么请求方法才能访问,值填写 RequestMethod 枚举值。如果像前面不填 method 则支持 GET、HEAD、POST、PUT、PATCH、DELETE、OPTIONS 这些请求方法。
RequestMethod[] method() default {};
在使用的时候遵守注解单个值和多个值怎么填的规则就行。
// 单个值
@RequestMapping(value = "/", method = RequestMethod.POST)
// 多个值
@RequestMapping(value = "/", method = {RequestMethod.POST, RequestMethod.GET})
不符合请求规范的情况下会提示 405,方法不允许。
GET / HTTP/1.1
Host: localhost:8080
Accept: */*
HTTP/1.1 405
Allow: POST
Content-Type: text/html;charset=utf-8
Content-Language: zh-CN
Content-Length: 693
Date: Mon, 19 Aug 2024 11:50:08 GMT
<!doctype html>
<html lang="zh">
<head><title>HTTP状态 405 - 方法不允许</title>
......
1.1.3 params 元素
限制请求参数,参数不对不让访问。
下面 Mapping 就限制了参数必须包含 test,请求中不传递不让访问。同样的多个也是用大括号包裹起来 params = {"parm1", "parm2"}
@RequestMapping(value = "/2az", method = RequestMethod.POST, params = "test")
还有另外几种用法。
// 不允许传递参数 list
@RequestMapping(value = "/2az", method = RequestMethod.POST, params = "!list")
// 可以比较完成的参数,这里不允许传递参数 list=1。
@RequestMapping(value = "/2az", method = RequestMethod.POST, params = "!list=1")
// 必须传递参数 username=1。
@RequestMapping(value = "/2az", method = RequestMethod.POST, params = "username=1")
这里仅仅测下 params = "test"
必须传递 test 参数名的情况。
1.正确发送 POST 请求传递 test 参数
一个正常的 POST 请求,要求 Content-Type 必须是 application/x-www-form-urlencoded,POST 参数必须放在请求体上,这样请求才会被处理。需要注意的是它真的是检查你有没有这个 test 参数,你把它当做参数值传递是不生效的,比如 1=test,它会认为 1 才是参数。
POST /test/2az HTTP/1.1
Host: localhost:8080
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 6
test=1
如果把 test 参数放在请求行上也能成功,这时候 Content-Type 随便填都可以。
POST /test/2az?test HTTP/1.1
Host: localhost:8080
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
2.错误发送 POST 请求传递 test 参数
错误的请求,情况就不少了,比如 POST 提交的非文本数据经常把 Content-Type 为 multipart/form-data。
POST /test/2az HTTP/1.1
Host: localhost:8080
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
Connection: keep-alive
Content-type: multipart/form-data; boundary=----WebKitFormBoundarycno3JHBTkn8XUCrN
Content-Length: 133
------WebKitFormBoundarycno3JHBTkn8XUCrN
Content-Disposition: form-data; name="test"
------WebKitFormBoundarycno3JHBTkn8XUCrN--
或者你在请求体中传 POST 参数,但是 Content-Type 不是 application/x-www-form-urlencoded 也不行。
POST /test/2az HTTP/1.1
Host: localhost:8080
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
Connection: keep-alive
Content-type: text/plain
Content-Length: 4
test
POST /test/2az?test HTTP/1.1
Host: localhost:8080
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
Connection: keep-alive
Content-type: text/plain
Content-Length: 0
1.1.4 headers 元素
限制请求头,只要请求头不对不让访问。
String[] headers() default {};
用法跟 params 元素一致,不再多说。
// 请求头必须包含
// CustomHeader: 1
// TestEnv:
// 请求头中不能存在
// TempHeaderTest:
// TempHeaderTest: .*
// User-Agent: Google
@PostMapping(value = "/2az", headers = {"CustomHeader=1", "TestEnv", "!TempHeaderTest", "User-Agent!=Google"})
以下面请求头限制为例,说明请求头正确格式。
// 要求请求中包含请求头 TestEnv 和 CustomHeader: 1 这两个请求头
@PostMapping(value = "/2az", headers = {"CustomHeader=1", "TestEnv", "!TempHeaderTest", "User-Agent!=Google"})
下面请求头格式是正确的。
CustomHeader: 1
TestEnv:
如果请求头是空值可不能省略冒号,否则报错 500 说你请求头格式不对。
TestEnv
不满足请求头限制条件的情况下访问是 404。
HTTP/1.1 404
Content-Type: text/html;charset=utf-8
Content-Language: zh-CN
Content-Length: 704
Date: Tue, 20 Aug 2024 11:27:09 GMT
Keep-Alive: timeout=20
Connection: keep-alive
<!doctype html><html lang="zh"><head><title>HTTP状态 404 - 未找到</title><style type="text/css">body {font-family:Tahoma,Arial,sans-serif;} h1, h2, h3, b {color:white;background-color:#525D76;} h1 {font-size:22px;} h2 {font-size:16px;} h3 {font-size:14px;} p {font-size:12px;} a {color:black;} .line {height:1px;background-color:#525D76;border:none;}</style></head><body><h1>HTTP状态 404 - 未找到</h1><hr class="line" /><p><b>类型</b> 状态报告</p><p><b>消息</b> No endpoint GET /test/2az.</p><p><b>描述</b> 源服务器未能找到目标资源的表示或者是不愿公开一个已经存在的资源表示。</p><hr class="line" /><h3>Apache Tomcat/10.1.24</h3></body></html>
1.1.5 consumes
consumes 元素用于限制请求中 Content-Type 请求头的值。
String[] consumes() default {};
假如限制成 produces = "application/xml"
,如果传递的不是 Content-Type: application/xml 则返回状态码 415 不支持的媒体类型。需要注意它不区分大小写,哪怕是 Content-Type: application/xmL 也认为是正确的值。
HTTP/1.1 415
Accept: application/xml
Content-Type: text/html;charset=utf-8
Content-Language: zh-CN
Content-Length: 758
Date: Tue, 20 Aug 2024 16:05:54 GMT
Keep-Alive: timeout=20
Connection: keep-alive
<!doctype html><html lang="zh"><head><title>HTTP状态 415 - 不支持的媒体类型</title><style type="text/css">body {font-family:Tahoma,Arial,sans-serif;} h1, h2, h3, b {color:white;background-color:#525D76;} h1 {font-size:22px;} h2 {font-size:16px;} h3 {font-size:14px;} p {font-size:12px;} a {color:black;} .line {height:1px;background-color:#525D76;border:none;}</style></head><body><h1>HTTP状态 415 - 不支持的媒体类型</h1><hr class="line" /><p><b>类型</b> 状态报告</p><p><b>消息</b> Content-Type 'application/json' is not supported.</p><p><b>描述</b> 源服务器拒绝服务请求,因为有效负载的格式在目标资源上此方法不支持。</p><hr class="line" /><h3>Apache Tomcat/10.1.24</h3></body></html>
1.1.6 produces
String[] produces() default {};
1.produces 默认值是优先根据请求头 Accep 优先级,设置响应头 Content-Type 的值
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
如果请求头 Accept 不存在,则 Spring MVC 根据数据类型自动猜测内容类型来设置,这时候有可能会存在 XSS 漏洞。或者是 Content-Type 没有设置的情况下浏览器会自动根据响应体来猜测 Content-Type 设置上去,这叫 MIME 嗅探,也是可能存在 XSS 的。
2.主动设置响应 Content-Type 头为指定 MIME 类型 produces = "application/xml"
。
1.2 请求方法快速映射
前面用 @RequestMapping 需要手动限制请求方法,这里有些 Mapping 注解以请求方法作为注解命名,使用者应注解就无需我们手动指定 method 元素。
- @GetMapping
- @PostMapping
- @PutMapping
- @DeleteMapping
- @PatchMapping
比如使用 @GetMapping("/1az")
就等同于 @RequestMapping(value = "/1az", method = RequestMethod.GET)
。而且 @RequestMapping 中所有的元素它也都有。
这些请求方法都是供后面 RESTFUL API 风格准备的。
1.3 请求获取
1.3.1 HttpServletRequest 获取请求
可以传递 HtppServletRequest 和 HtppServletResponse 参数,Spring MVC 会帮你自动注入到形参。
package com.raingray.controller;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/api")
public class ExampleController {
@GetMapping("/fullPath")
public String getFullPath(HttpServletRequest request) {
String fullPath = request.getRequestURL().toString(); // 获取完整的 URL
String queryString = request.getQueryString(); // 获取查询参数
if (queryString != null) {
fullPath += "?" + queryString; // 将查询参数添加到 URL
}
System.out.println("Full Path: " + fullPath);
return "test";
}
}
发送 GET 请求 http://localhost/api/fullPath?asdf=asdo4&112312=$
,控制台可以输出完成 URL。
Full Path: http://localhost/api/fullPath?asdf=asdo4&112312=$
1.3.2 @RequestParam 获取请求参数
1.name 和 value 元素获取参数
获取参数需要给 name 或 value 元素指定参数名(它两互为别名),最后注解会自动把参数赋到对应形参变量上。这里就接收 name 和 ids 参数,其中 ids 参数是个数组,说明可以接收多个相同参数名的值,在实际传递的时候就是 ?name&ids=1&ids=2
。
public String test(
@RequestParam("name") String variableName
@RequestParam("ids") String[] ids
) {
......
}
它能获取到 POST 或者 GET 参数,但不能只获取指定位置参数,比如只获取请求体中的 POST 参数,你通过请求行传递 GET 参数我不处理,这种需要暂时做不到。
@PostMapping("/2az")
public String test(
@RequestParam("test") String test
) {
System.out.println(test);
return "index";
}
test 实例方法只允许用 POST 请求 /2az 访问,还接收 test 请求参数。这里在请求行上传递参数 POST /test/2az?test=1+1
,确实获取到 1 1
。
2.required 元素控制参数是否必须传递
required 元素用来控制是否强制传递此参数,默认值是 require = true
,表示必须提交参数,不提交就报 400 错。没有传递参数的形参变量 test 值是 null。
HTTP/1.1 400
Content-Type: text/html;charset=utf-8
Content-Language: zh-CN
Content-Length: 803
Date: Wed, 21 Aug 2024 07:26:37 GMT
Connection: close
<!doctype html><html lang="zh"><head><title>HTTP状态 400 - 错误的请求</title><style type="text/css">body {font-family:Tahoma,Arial,sans-serif;} h1, h2, h3, b {color:white;background-color:#525D76;} h1 {font-size:22px;} h2 {font-size:16px;} h3 {font-size:14px;} p {font-size:12px;} a {color:black;} .line {height:1px;background-color:#525D76;border:none;}</style></head><body><h1>HTTP状态 400 - 错误的请求</h1><hr class="line" /><p><b>类型</b> 状态报告</p><p><b>消息</b> Required parameter 'test' is not present.</p><p><b>描述</b> 由于被认为是客户端对错误(例如:畸形的请求语法、无效的请求信息帧或者虚拟的请求路由),服务器无法或不会处理当前请求。</p><hr class="line" /><h3>Apache Tomcat/10.1.24</h3></body></html>
3.defaultValue 元素在不传递参数的情况下设置默认值
defaultValue 元素用来给参数赋默认值,类型是 String,在不传递参数情况下又想有一个值可以使用此元素做到。简单来说你传递参数就用你的值,不传就用默认值,设置完默认值后 required 也会失效,你不能要求有默认值又要人传递参数,这是矛盾的。
4.省略 @RequestParam name 或 value 元素填写
如果请求参数名和形参名称一致,@RequestParam 可以省略不写。
public String test(String test) {
System.out.println(test);
return "index";
}
在 Spring6 中这么做需要前提是 javac 开启 -parameters 编译选项,这样 Spring MVC 就能通过反射找到形参进行赋值。而 Spring5 中不存在此问题。
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>21</source>
<target>21</target>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
没开这个选项仍然这样写会报 500 错误,不过在错误信息中也提示了要你开启 -parameters。
Request processing failed: java.lang.IllegalArgumentException: Name for argument of type [java.lang.String] not specified, and parameter name information not available via reflection. Ensure that the compiler uses the '-parameters' flag.
5.通过 POJO 类接收参数
创建 POJO 类
package com.raingray.pojo;
public class Test {
private Integer id;
public Test() {
}
public Test(Integer id) {
this.id = id;
}
public void setId(Integer id) {
this.id = id;
}
@Override
public String toString() {
return "Test{" +
"id=" + id +
'}';
}
}
开启 -parameters 编译选项后,编写一个控制器,这个控制器接收 Test 类型参数。
package com.raingray.controller;
import com.raingray.pojo.Test;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class TetstController {
@GetMapping("/")
public String index(Test test) {
System.out.println(test);
return "index";
}
}
直接发送 http://localhost/?id=1+1
,成功给 Test 对象 id 属性赋值为 11。
Test{id=11}
在背后 Spring MVC 收到请求后自动通过反射创建 Test 类对象,接着使用反射调用 Seter 方法 SetId 把值写入。
如果参数和 POJO 类的方法名对不上那就无法赋值,值是 null。比如 http://localhost/?teaaa=1+1
,Spring MVC 就在 Test 对象中找 SetTeaaa 方法找不到就不赋值,属性 id 的值自然是 null。
6.解决请求和响应乱码问题
GET 参数乱码这个情况已经在 Servlet 学习过如何解决,当时是设置 %CATALINA_HOME%/conf/server.xml 给 Connector 标签添加 URIEncoding 属性即可。
<!-- Tomcat 8 起默认值是 UTF-8 -->
<Connector URIEncoding="UTF-8" />
而请求体参数和响应内容乱码,则需要在获取和响应内容前设置编码。
但是 Spring MVC 里控制器里头没办法在获取参数前设置编码,因为参数已经注入到形参中,唯一的办法就是编写一个自定义的 Filter。
package com.raingray.filter;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter(value = "/*", servletNames = "springmvc")
public class CharsetSetting implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("CharsetSetting Filter is Running");
servletRequest.setCharacterEncoding("UTF-8");
servletResponse.setCharacterEncoding("UTF-8");
filterChain.doFilter(servletRequest, servletResponse);
}
}
不过 Spring MVC 编码 Filter 类已经帮我们写好了,叫 CharacterEncodingFilter,我们只需要启用。
<filter>
<filter-name>encoding</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>
<!--开启强制编码-->
<init-param>
<param-name>forceRequestEncoding</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>forceResponseEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encoding</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
7.解决 IDEA 的 Tomcat 日志乱码。
默认的 Java 类使用 System.out 输出中文到控制台没乱码,但是在 Spring MVC 中这样输出就是会乱码,参考了 https://blog.csdn.net/weixin_42127613/article/details/139746374 文中提到的解决方案,成功避免乱码问题。
首先 Tomcat 日志 java.util.logging.ConsoleHandler.encoding = GBK
要设置成 GBK,用 UTF-8 依然会乱码。其他的用 UTF-8。
1catalina.org.apache.juli.AsyncFileHandler.level = FINE
1catalina.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs
1catalina.org.apache.juli.AsyncFileHandler.prefix = catalina.
1catalina.org.apache.juli.AsyncFileHandler.maxDays = 90
1catalina.org.apache.juli.AsyncFileHandler.encoding = UTF-8
2localhost.org.apache.juli.AsyncFileHandler.level = FINE
2localhost.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs
2localhost.org.apache.juli.AsyncFileHandler.prefix = localhost.
2localhost.org.apache.juli.AsyncFileHandler.maxDays = 90
2localhost.org.apache.juli.AsyncFileHandler.encoding = UTF-8
3manager.org.apache.juli.AsyncFileHandler.level = FINE
3manager.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs
3manager.org.apache.juli.AsyncFileHandler.prefix = manager.
3manager.org.apache.juli.AsyncFileHandler.maxDays = 90
3manager.org.apache.juli.AsyncFileHandler.encoding = UTF-8
4host-manager.org.apache.juli.AsyncFileHandler.level = FINE
4host-manager.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs
4host-manager.org.apache.juli.AsyncFileHandler.prefix = host-manager.
4host-manager.org.apache.juli.AsyncFileHandler.maxDays = 90
4host-manager.org.apache.juli.AsyncFileHandler.encoding = UTF-8
java.util.logging.ConsoleHandler.level = FINE
java.util.logging.ConsoleHandler.formatter = org.apache.juli.OneLineFormatter
java.util.logging.ConsoleHandler.encoding = GBK
IDEA 用 Alt + Shift + S 打开设置 Editor -> Console -> Default Encoding: 把默认的 UTF-8 设置成 GBK。
1.3.3 获取请求头
获取请求头就下面两种注解,使用方式和 @requestParam 一模一样。
- @RequestHeader,获取任意请求头的值
- @CookieValue 获取请求请求头 Cookie 中指定 Key 的 Value。
这里就以获取 Cookie 为例。
@GetMapping("/getHeader")
public String index2(
@RequestHeader("Cookie") String test,
@CookieValue("PHPSESSID") String cookie) {
System.out.println(test);
System.out.println(cookie);
return "index";
}
发送请求
GET /getHeader HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.100 Safari/537.36
Accept-Encoding: gzip, deflate, br
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
Cookie: 97534f6a56f5aad123adsfasdff67__typecho_remember_remember=1; 97534f33ascd212a19159ef67__typecho_uid=1; 97534f6a5aacc212a19159ef67__typecho_authCode=%24T%24WE3T3C0Rt1234e1d0cfd69ca463c4c; PHPSESSID=nl918rabcdujqhjr
Connection: keep-alive
成功获取到对应 Cookie 的值和 Cookie Key 为 PHPSESSID 的值。
97534f6a56f5aad123adsfasdff67__typecho_remember_remember=1; 97534f33ascd212a19159ef67__typecho_uid=1; 97534f6a5aacc212a19159ef67__typecho_authCode=%24T%24WE3T3C0Rt1234e1d0cfd69ca463c4c; PHPSESSID=nl918rabcdujqhjr
nl918rabcdujqhjr
1.3.4 @RequestBody 获取请求体
1.以字符串类型获取请求体
@RequestBody 只能用在形参上,第一个作用是原封不动把请求参数赋给形参变量。
@RequestMapping("/requestBody")
public String requestBodyTransition(
@RequestBody String body
) {
System.out.println("获取到的请求体:" + body);
return "index";
}
下面这几种 Content-Type 请求都能获取到,不管你用什么请求方法,只要请求体有内容就能获取到。
POST /requestBody HTTP/1.1
Host: localhost
Content-Type: text/plain
Accept: */*
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 22
asdfasfASDSADASD中文
POST /requestBody HTTP/1.1
Host: localhost
Accept: */*
Cache-Control: no-cache
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: multipart/form-data; boundary=--------------------------922266162146705730210196
Content-Length: 165
----------------------------922266162146705730210196
Content-Disposition: form-data; name="test"
中文
----------------------------922266162146705730210196--
POST /requestBody HTTP/1.1
Host: localhost
Accept: */*
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 23
tset=%E4%B8%AD%E6%96%87
GET /requestBody HTTP/1.1
Host: localhost
Content-Type: application/x-www-form-urlencoded; charset=utf-8
Content-Length: 21
%E4%B8%AD%E6%96%87=1&
2.把请求体转换成实体类对象
第二种用法是请求体传的 JSON 参数,控制器方法接收的时候可以把 JSON 转换成对应实体类对象。POJO 类使用的是 "1.3.2 @RequestParam 获取请求参数" 中的 Test.java。
@RequestMapping({"/requestBodyToObject"})
public String jsonToObject(@RequestBody Test body) {
System.out.println("获取到的请求体:" + body);
System.out.println("获取到的请求体的类型:" + body.getClass());
return "index";
}
在以前需要我们编写对应方法解析 JSON,或者引入如 Fastjson 和 Jackson 之类的第三方 JSON 处理组件主动做转换,这里 Spring MVC 可以自动调用 Jackson 依赖帮你把 JSON 转换成对象,省去手动转换操作。
只需要引入 jackson-databind。
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.2</version>
</dependency>
最后在 Spring 配置文件注册对应消息转换器。
<mvc:annotation-driven/>
使用 @RequestMapping 的情况,使用任意请求方法在请求体携带 JSON 数据都可以获取到对象,需要注意请求头内容类型默认情况下必须是 Content-Type: application/json
或者带 UFT-8 编码的 application/json; charset=utf-8
,不然就不支持处理这种请求。
GET /requestBodyToObject HTTP/1.1
Host: localhost
Content-Type: application/json
Content-Length: 8
{"id":1}
POST /requestBodyToObject HTTP/1.1
Host: localhost
Content-Type: application/json; charset=utf-8
Content-Length: 8
{"id":1}
运行后控制台成功输出 Test 对象 ToString 方法,查看 body 参数是类型的确被转换成 Test 对象。
前端传来的数据Test{id=1}
前端传来的数据com.raingray.pojo.Test
1.3.5 RequestEntity 请求详情
在控制器方法形参上用 RequestEntity 接收,会把请求封装成 RequestEntity 对象赋值给形参,这个对象包含请求协议版本号以外所有信息。
@RequestMapping(value = "/requestEntity")
public String httpEntity(RequestEntity<String> req, HttpServletRequest rawReq) {
System.out.println("请求行:" + req.getMethod() + " " + req.getUrl() + " " + rawReq.getProtocol());
System.out.println("请求头:" + req.getHeaders());
System.out.println("请求体:" + req.getBody());
System.out.println("req 类型:" + req.getClass().getName());
return "test";
}
发送请求。
GET /requestEntity?a=1&b=2 HTTP/1.1
Host: localhost
sec-ch-ua: "Chromium";v="127", "Not)A;Brand";v="99"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Accept-Language: zh-CN
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.100 Safari/537.36
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
DNT: 1
Sec-GPC: 1
Accept-Encoding: gzip, deflate, br
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
Connection: keep-alive
Content-Length: 13
body=a&test=d
请求行:GET http://localhost/requestEntity?a=1&b=2 HTTP/1.1
请求头:[host:"localhost", sec-ch-ua:""Chromium";v="127", "Not)A;Brand";v="99"", sec-ch-ua-mobile:"?0", sec-ch-ua-platform:""Windows"", accept-language:"zh-CN", upgrade-insecure-requests:"1", user-agent:"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.100 Safari/537.36", sec-fetch-site:"none", sec-fetch-mode:"navigate", sec-fetch-user:"?1", sec-fetch-dest:"document", dnt:"1", sec-gpc:"1", accept-encoding:"gzip, deflate, br", accept:"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8", connection:"keep-alive", content-length:"13"]
请求体:body=a&test=d
req 类型:org.springframework.http.RequestEntity
1.3.6 处理 multipart/form-data 请求
这种一般在文件上传用的多。
先在本地创建 from 表单模拟前端上传文件。
<form method="post" action="http://localhost/uploadFile" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="hidden" name="test" value="123456" />
<input type="submit">
</form>
接着在后端 web.xml 中配置前端控制器 <multipart-config>
控制文件上传大小限制。
<!--前端控制器-->
<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>WEB-INF/springmvc-servlet.xml</param-value>
</init-param>
<!--启动后自动编译 Servlet-->
<load-on-startup>1</load-on-startup>
<multipart-config>
<!--设置单个文件上传最大字节数-->
<max-file-size>102400</max-file-size>
<!--设置整个表单所有文件上传的总字节数-->
<max-request-size>102400</max-request-size>
<!--设置最小上传文件字节大小-->
<file-size-threshold>0</file-size-threshold>
</multipart-config>
</servlet>
<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
通过 from 表单上传文件
POST /uploadFile HTTP/1.1
Host: localhost
Content-Length: 822678
Cache-Control: max-age=0
sec-ch-ua: "Chromium";v="127", "Not)A;Brand";v="99"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Accept-Language: zh-CN
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.100 Safari/537.36
Origin: null
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryBHAS60VofybvvfJh
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
DNT: 1
Sec-GPC: 1
Accept-Encoding: gzip, deflate, br
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
Connection: keep-alive
------WebKitFormBoundaryBHAS60VofybvvfJh
Content-Disposition: form-data; name="file"; filename="零日漏洞演练-统信UOS某服务进程本地提权漏洞-溯源反制组v1.docx"
Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document
PK
......
------WebKitFormBoundaryBHAS60VofybvvfJh
Content-Disposition: form-data; name="test"
123456
------WebKitFormBoundaryBHAS60VofybvvfJh--
后端解析请求内容,读取文件字节信息并写入到磁盘。
@PostMapping("/uploadFile")
public String uploadFile(@RequestParam("file") MultipartFile file, @RequestParam("test") String test, HttpServletRequest request) throws IOException {
// getName() 方法获取 Content-Disposition 中的 name 属性值,最终获取到 file
System.out.println(file.getName());
// 获取 Content-Disposition: form-data; name="test" 的值,最终获取到 123456
System.out.println(test);
// getOriginalFilename() 原封不动获取文件名
// Content-Disposition 中的 filename 属性值,最终获取到 微步在线POC.zip
String fileName = UUID.randomUUID() + file.getOriginalFilename();
// 文件后缀
String fileSuffix = fileName.substring(fileName.lastIndexOf("."));
File storeFileAbsolutePath = null;
InputStream in = null;
BufferedOutputStream out = null;
try {
// 设置要上传的目录
storeFileAbsolutePath = new File(request.getServletContext().getRealPath("WEB-INF/templates"));
if (!storeFileAbsolutePath.exists()) {
storeFileAbsolutePath.mkdirs();
}
// 获取文件输入流
in = file.getInputStream();
FileOutputStream fileOutputStream = new FileOutputStream(storeFileAbsolutePath.getAbsolutePath() + "/" + fileName);
out = new BufferedOutputStream(fileOutputStream);
// 每批以 100Kb 为准,分批读取写入文件到磁盘以缓解大文件占用内存压力
byte[] buffer = new byte[1024 * 100]; // 创建 Byte 数组最大能放 100MB 的 Byte
int readByteNum = 0;
while ((readByteNum = in.read(buffer)) != -1) { // 每次读取 100MB 字节的内容,存入 buffer 里。如果重新存入会把以前的内容覆盖掉。
out.write(buffer,0, readByteNum); // 把读取到的字节 buffer 数组,从 0 个字节开始写入,总共写 readByteNum 读取到的长度。,
}
// 返回上传结果
return fileName + "上传成功";
} catch (RuntimeException e) {
// 出现异常删除损坏文件
if (storeFileAbsolutePath != null && out != null) {
out.close(); // 防止文件占用
String errorFilePath = storeFileAbsolutePath.getAbsolutePath() + "\\" + fileName;
System.out.println("损坏文件" + errorFilePath + "删除状态:" + new File(errorFilePath).delete());
}
throw new RuntimeException(e);
} finally {
if (in != null) {
in.close();
}
if (out != null) {
out.flush();
out.close();
}
}
}
1.3.7 JSR-303 数据校验🔨
前面使用 POJO 类来接受参数,只要参数名一致就能做好映射,这种情况下想要对传入的参数做做格式检查,不可能真的编写一大堆条件判断,在这里有 JSR-303 可以通过注解的方式用来做数据检查。
JSR-303 只是规范编号,应该叫做 Bean Validation,它定义了规范的要求,具体实现是 Hibernate Validator 完成的,因此要使用需引入 API 接口和具体实现类。好在 hibernate-validator 默认情况下已经引入了 API 接口,我们无需关注规范怎么引入。
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>8.0.1.Final</version>
</dependency>
一些基础类型的参数能不能检查?是不可以的。
https://www.bilibili.com/video/BV1m3411Y7eu?p=24
https://www.bilibili.com/video/BV1AP411s7D7?p=126
https://www.bilibili.com/video/BV1Km4y1k7bn?p=117
1.4 三个域对象
还是原先 Servlet 中学到的 request、session、application 三个域对象,这里学习怎么在 Spring MVC 中设置、获取和删除属性。
1.4.1 request 域
1.jakarta.servlet.http.HttpRequest 类
/**
* 使用原生 Servlet 设置请求域数据
* @param request 请求对象
* @return 返回 share 逻辑视图名
*/
@GetMapping("/httpServletRequest")
public String requestDataShare(HttpServletRequest request) {
request.setAttribute("HttpServletRequestShareData","HttpServletRequest类设置的数据");
return "share";
}
在 share 视图中可以通过 key 获取到请求域对象。这里只演示一遍如何获取后面不再赘述。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>共享域数据操作</title>
</head>
<body>
<p th:text="${HttpServletRequestShareData}"></p>
</body>
</html>
2.org.springframework.ui.Model 接口
@GetMapping("/modelInterface")
public String modelInterface(Model model) {
model.addAttribute("modelInterfaceShareData", "org.springframework.ui.Model接口设置的数据");
return "share";
}
3.java.util.Map 接口
@GetMapping("/mapInterface")
public String mapInterface(Map<String, Object> object) {
object.put("mapInterfaceShareData", "java.util.Map接口设置的数据");
System.out.println(object.getClass().getName());
return "share";
}
4.org.springframework.ui.ModelMap 类
@GetMapping("/modelMapClass")
public String modelMapClass(ModelMap object) {
object.addAttribute("modelMapShareData", "org.springframework.ui.ModelMap类设置的数据");
System.out.println(object.getClass().getName());
return "share";
}
Model 接口、Map 接口、ModelMap 类这三个方式底层都使用的是 org.springframework.validation.support.BindingAwareModelMap 这个类帮你设置请求域属性。
5.ModelAndView 类
这是 Spring MVC 独有的设置方法。
@GetMapping("/modelAndViewClass")
public ModelAndView modelAndViewClass() {
// 创建 ModelAndView 对象
ModelAndView modelAndView = new ModelAndView();
//添加请求域数据
modelAndView.addObject("modelAndViewShareData", "org.springframework.web.servlet.ModelAndView.ModelAndView类设置的数据");
// 将数据传入到 share 视图中
modelAndView.setViewName("share");
return modelAndView;
}
1.4.2 session 域
/**
* 会话域
*/
@GetMapping("/httpSessionShareData")
public String httpSession(HttpServletRequest request) {
HttpSession session = request.getSession();
session.setAttribute("sessionShareData", "HttpSession接口设置会话域属性");
return "share";
}
thymeleaf 需要通过 session 来获取属性。
<p th:text="${session.get('sessionShareData')}"></p>
1.4.3 application 域
/**
* 应用域
*/
@GetMapping("/serverContextShareData")
public String serverContextInterface() {
servletContext.setAttribute("ServletContextShareData", "jakarta.servlet.ServletContext接口设置应用域属性");
return "share";
}
thymeleaf 通过 application.get 获取应用域属性。
<p th:text="${application.get('ServletContextShareData')}"></p>
2 响应处理
这里一般有两种处理方式,一个是以前的混合开发,需要用到视图模板,给它传数据。
如今流行前后端分离开发,前端和后端各自专注自己的内容,前端主要编写布局、样式等内容,后端提供 HTTP 接口给前端调用,前端自己处理接口响应数据展示在 HTML 上。
2.1 视图
我们前面处理响应一直是在使用视图,其实视图有很多种,JSP、Thymeleaf、FreeMaker、VelocityView 等等...... Spring MVC 支持配置的方式切换视图解析器,跟插件一样能够拔插。
一个视图要最终把模板解析成 HTML 需要用到 ViewResolver 视图解析器接口,通过执行 resolveViewName 方法将根据视图配置文件中的前缀和后缀模板字符串和逻辑视图名称组合起来,转换成物理视图,转换完成返回 View 接口实现类对象,后面根据 View 接口实现类对象调 render 方法渲染模板,返回 HTML 给浏览器。
下面看看 Spring MVC 渲染视图的简要流程。
1.doDispatch
再捋一个请求到响应的过程,首先请求会发送到 org.springframework.web.servlet.DispatcherServlet 中,由 org.springframework.web.servlet.DispatcherServlet#doDispatch 方法,根据 ha.handle 处理器判断具体访问的路径在那个控制器的控制器方法,一旦控制器方法执行完成返回一个逻辑视图名字符串,后面自动封装成一个 ModelAndView 对象。
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
2.processDispatchResult
紧接着在 org.springframework.web.servlet.DispatcherServlet#doDispatch 方法体中调用 org.springframework.web.servlet.DispatcherServlet#processDispatchResult 方法。
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
processDispatchResult 会自动把 ModelAndView 对象传给 org.springframework.web.servlet.DispatcherServlet#render 方法,转换成物理视图,把模板渲染成 HTML。
render(mv, request, response);
3.render
org.springframework.web.servlet.DispatcherServlet#render,通过调用 resolveViewName 方法把 ModelAndView 中的视图逻辑名称根据视图配置中的前缀和后缀模板字符串转换成物理视图名称,返回视图对象 View。
View view;
String viewName = mv.getViewName();
view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
这个 resolveViewName 方法里面是通过 ViewResolver 接口实现类(实际上就是我们 Spring 配置文件中配置的视图解析器实现类,这里我用的 Thymeleaf 因此是 ThymeleafViewResolver)解析成物理视图名。
if (this.viewResolvers != null) {
for (ViewResolver viewResolver : this.viewResolvers) {
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
return view;
}
}
}
返回的这个 View 对象是视图解析器 viewResolver 的 resolveViewName 方法创建出来的,下面是常见的视图类:
- InternalResourceView:Spring MVC 内置内部资源视图
- ThymeleafView:Thymeleaf视图
- FreeMarkerView:FreeMarker视图
- VelocityView:Velocity视图
最后在 org.springframework.web.servlet.DispatcherServlet#render 方法体里面调用 View 接口实现类的 render 方法,渲染模板为 HTML。
view.render(mv.getModelInternal(), request, response);
2.1.1 转发和重定向
转发和重定向原理在 Servlet 一文中已经讲过。
// 请求转发
req.getRequestDispatcher("/b").forward(req, resp);
// 响应重定向
resp.sendRedirect("/test");
在 Spring MVC 中操作也比较简单,可以选择转发到内部资源或者是重定向到内部资源或外部链接。
@GetMapping("/test")
public String testResource() {
return "index";
}
@GetMapping("/jump")
public String jumpResource(
@RequestParam(value = "jump", required = false, defaultValue = "") String jump
) {
switch (jump) {
case "redirect":
return "redirect:/test";
case "forward":
return "forward:/test";
default:
return "redirect:https://www.raingray.com";
}
}
这里定义了 /test 路径资源,把请求转发到模板 index 上,访问 localhost/test 后是一个普通模板。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>this is title</title>
</head>
<body>
<h1>test index template</h1>
</body>
</html>
这里通过 switch 判断 GET 参数 jump 值,从而选择是跳转还是重定向。
不带 jump 重定向到 https://www.raingray.com
GET /jump HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.100 Safari/537.36
Connection: keep-alive
HTTP/1.1 302
Location: https://www.raingray.com
Content-Language: zh-CN
Content-Length: 0
Date: Sat, 24 Aug 2024 07:49:49 GMT
Keep-Alive: timeout=20
Connection: keep-alive
jump 值为 forward 则转发请求到 /test 上。
GET /jump?jump=forward HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.100 Safari/537.36
Connection: keep-alive
HTTP/1.1 200
Content-Type: text/html;charset=UTF-8
Content-Language: zh-CN
Date: Sat, 24 Aug 2024 07:38:51 GMT
Keep-Alive: timeout=20
Connection: keep-alive
Content-Length: 189
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>this is title</title>
</head>
<body>
<h1>test index template</h1>
</body>
</html>
2.1.2 访问静态资源
1.使用 Web 容器默认 Servlet 处理静态资源
在 DispcherServlet 找不到这个控制器的时候就会交给 Web 容器默认 Servlet 处理,这样就能方便找到静态资源。使用上我们只需在 Spring 配置文件中启用 <mvc>
标签即可。
<mvc:default-servlet-handler/>
如果失败了,可能是你没有 mvc 标签添加命名空间。需要在 beans 添加属性。
xmlns:mvc="http://www.springframework.org/schema/mvc"
最后 beans 标签属性 xsi:schemaLocation 添加值,才能成功使用。
http://www.springframework.org/schema/mvc
https://www.springframework.org/schema/mvc/spring-mvc.xsd
默认 Servlet 每个容器可能实现不同,如果是 Tomcat 调用的就是 %CATALINA_HOME%/conf/web.xml 中注册的 org.apache.catalina.servlets.DefaultServlet。
<servlet>
<servlet-name>default</servlet-name>
<servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
<init-param>
<param-name>debug</param-name>
<param-value>0</param-value>
</init-param>
<init-param>
<param-name>listings</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
2.主动配置静态资源位置
Spring 配置文件中添加下面内容。这里配置 访问 /static 下任意资源,都会到 Web 目录 /static/ 下找。
<!-- 配置静态资源处理 -->
<mvc:resources mapping="/static/**" location="/static/"/>
随后必须开启 <mvc:annotation-driven/>
,才能让我们配置的 Controller 方法启用。避免能访问到静态资源无法访问 Controller。
<!-- 开启注解驱动 -->
<mvc:annotation-driven/>
2.1.3 路径绑定默认视图
有时候一个 Controll 方法只是为了返回一个视图,啥也没干,这时候就不用专门编写一个方法。直接在 Spring 配置文件可以用 <mvc:view-controller>
配置。
<mvc:view-controller path="/如何访问该页面" view-name="对应的逻辑视图名称" status-code="HTTP响应状态吗"/>
path 属性是指定要访问的路径,view-name 是访问到指定路径后要转发到那个视图,status-code 指定响应状态吗,比如访问
/404 我们状态码就可以设置成 404 表示这个资源找不到,这个属性不写默认响应 200。
配置完成需要开启下面注解,让 Controller 启用。
<mvc:annotation-driven/>
2.2 RESTFul API 风格
就介绍下 RESTFul 怎么写就好,跟传统的 API 风格做个对比。
使用请求方法作为 CRUD 的标志:
- GET,获取信息
- POST,新建信息,具体信息标识服务端决定,比如上传文件,客户端无法指定文件的唯一标识,那么就可以用 POST
PUT,新建信息,具体信息标识由客户端决定,比如创建文件,客户端可以自己指定文章的 id,服务端只需要接收这个 id 进行保存就好,无需自己操心去生成 id,这时用 PUT - DELETE,删除信息
- PATCH,修改信息
而传统方法只有 GET 获取信息,用 POST 删除、新建、修改信息。,
在路径中标识资源,要修改的目标信息如果是单个直接作为路径传递:
https://example.com/resources
,https://example.com/resources/142
,
多个参数直接在请求体传输信息用 JSON 或 XML。
传统方法通过参数标识资源:
https://example.com/resources
,https://example.com/resources?id=142
,
多个参数同样在请求体传输数据。
2.3 @ResponseBody
@ResponseBody 可以在响应体中返回指定的内容,本质上还是在用 HttpServletResponse 响应内容到响应体中。它可以在方法上和类上使用,在方法上用只对指定方法生效,在类上用对类下所有方法生效。
@Target({ElementType.TYPE, ElementType.METHOD})
1.响应体返回指定内容
控制器方法上用 @ResponseBody 可以在响应体返回具体内容。
@GetMapping(value = "/returnData")
@ResponseBody
public String returnData() {
return "<img src=#>";
}
发送请求发现确实响应体是我们主动返回的内容。
GET /returnData HTTP/1.1
Host: localhost
sec-ch-ua: "Chromium";v="127", "Not)A;Brand";v="99"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Accept-Language: zh-CN
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.100 Safari/537.36
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
DNT: 1
Sec-GPC: 1
Accept-Encoding: gzip, deflate, br
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
Connection: keep-alive
HTTP/1.1 200
Content-Type: text/html;charset=ISO-8859-1
Content-Length: 11
Date: Mon, 26 Aug 2024 15:43:53 GMT
Keep-Alive: timeout=20
Connection: keep-alive
<img src=#>
但是观察 Content-Type 会发现这样不是有 XSS 吗?为什么 Content-Type 是跟着请求头 Accept 在走.其实 produces 元素默认会先满足请求头 Accept,如果不想响应类型飘忽不定,可以主动设置 produces 元素值,这里我设置成 text/plain。
@GetMapping(value = "/returnData", produces = "text/plain")
@ResponseBody
public String returnData() {
return "<img src=#>";
}
发送请求,观察 Content-Type 确实已经被设置成 text/plain。
GET /returnData HTTP/1.1
Host: localhost
sec-ch-ua: "Chromium";v="127", "Not)A;Brand";v="99"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Accept-Language: zh-CN
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.100 Safari/537.36
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
DNT: 1
Sec-GPC: 1
Accept-Encoding: gzip, deflate, br
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
Connection: keep-alive
HTTP/1.1 200
Content-Type: text/plain;charset=ISO-8859-1
Content-Length: 11
Date: Mon, 26 Aug 2024 15:50:36 GMT
Keep-Alive: timeout=20
Connection: keep-alive
<img src=#>
2.响应体返回 JSON
控制器方法上用 @ResponseBody,如果返回的是 POJO 对象,就会自动把对象转换成 json 字符串返回。
@GetMapping(value = "/returnJson")
@ResponseBody
public Test returnJson() {
return new Test(11123);
}
这里我们转换的 Test 是个 POJO 类。
package com.raingray.pojo;
public class Test {
private Integer id;
public Test() {
System.out.println("Test(Integer id) 被调用了");
}
public Test(Integer id) {
this.id = id;
System.out.println("Test(Integer id) 被调用了");
}
public void setId(Integer id) {
this.id = id;
System.out.println("setId(Integer id) 被调用了");
}
public Integer getId() {
System.out.println("getId() 被调用了");
return id;
}
@Override
public String toString() {
System.out.println("toString() 被调用了");
return "Test{" +
"id=" + id +
'}';
}
}
自动转换肯定要调 JSON 处理库,这里默认是调的 Jackson,因此需要引入 jackson-databind 依赖。
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.2</version>
</dependency>
光有了依赖怎么自动转换呢?最后需要还要在 Spring 中启用注解驱动开启自动转换。
<mvc:annotation-driven/>
设置完成后,我们发送请求发现的确响应 JSON 数据,并且 Content-Type 也正确设置成 application/json
。
GET /returnJson HTTP/1.1
Host: localhost
Cache-Control: max-age=0
sec-ch-ua: "Chromium";v="127", "Not)A;Brand";v="99"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Accept-Language: zh-CN
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.100 Safari/537.36
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
DNT: 1
Sec-GPC: 1
Accept-Encoding: gzip, deflate, br
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
Connection: keep-alive
HTTP/1.1 200
Content-Type: application/json
Date: Mon, 26 Aug 2024 15:38:37 GMT
Keep-Alive: timeout=20
Connection: keep-alive
Content-Length: 12
{"id":11123}
在控制台上看到,Jackson 把对象 Test 序列化成 JSON 调用了有参构造方法 Test(Integer id),Getter 方法 getId() 和 toString() 方法。
23:38:37.510 [http-nio-80-exec-47] DEBUG org.springframework.web.servlet.DispatcherServlet -- GET "/returnJson", parameters={}
23:38:37.510 [http-nio-80-exec-47] DEBUG org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping -- Mapped to com.raingray.controller.ResponseData#returnJson()
Test(Integer id) 被调用了
23:38:37.510 [http-nio-80-exec-47] DEBUG org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor -- Using 'application/json;q=0.8', given [text/html, application/xhtml+xml, image/avif, image/webp, image/apng, application/xml;q=0.9, */*;q=0.8] and supported [application/json, application/*+json]
toString() 被调用了
23:38:37.510 [http-nio-80-exec-47] DEBUG org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor -- Writing [Test{id=11123}]
getId() 被调用了
23:38:37.511 [http-nio-80-exec-47] DEBUG org.springframework.web.servlet.DispatcherServlet -- Completed 200 OK
3.响应体返回文件
总共两步骤,第一步是设置响应头 Content-Disposition,第二部步在响应体中返回文件字节。
package com.raingray.controller;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
@Controller
public class FileUtil {
@GetMapping("/downloadFile")
public ResponseEntity<byte[]> downloadFile(HttpServletRequest request) throws IOException {
HttpHeaders httpHeaders = new HttpHeaders();
// 设置 fromdata,name 取自前端 name 参数
// httpHeaders.setContentDispositionFormData("at123tach123ment","filename");
// 获取文件字节
File file = new File(request.getServletContext().getRealPath("/WEB-INF/templates") + "/ubuntu-18.04.5-desktop-amd64.iso");
byte[] body = Files.readAllBytes(file.toPath());
// 设置文件下载响应头
ContentDisposition fileObj = ContentDisposition.attachment().filename(file.getName()).build();
httpHeaders.setContentDisposition(fileObj);
return new ResponseEntity<>(body, httpHeaders, HttpStatus.OK);
}
}
发请求,可以看到响应头 Content-Disposition 提示下载为 error.html,如果 Files.readAllBytes 读取的文件达到 2GB 则触发异常 jakarta.servlet.ServletException: Handler dispatch failed: java.lang.OutOfMemoryError: Required array size too large
。
GET /downloadFile HTTP/1.1
Host: localhost
Accept-Encoding: gzip, deflate, br
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
Connection: keep-alive
HTTP/1.1 200
Content-Disposition: attachment; filename="error.html"
Content-Type: text/html;charset=UTF-8
Content-Length: 212
Date: Thu, 29 Aug 2024 05:53:42 GMT
Keep-Alive: timeout=20
Connection: keep-alive
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>异常页面</title>
</head>
<body>
<p th:text="${errorMessage}"></p>
</body>
</html>
2.4 @RestController
控制器类上使用 @RestController,等同于在类上自动添加 @Controller 和 @ResponseBody,这对我们编写 RESTful API 很方便。
对 2.3 小节进行改写,代码简洁很多。
@RestController
public class ResponseData {
@GetMapping(value = "/returnData", produces = "text/plain")
// public Date returnData() {
public String returnData() {
return "<img src=#>";
}
@GetMapping(value = "/returnJson")
public Test returnJson() {
return new Test(11123);
}
}
2.5 ResponseEntity
可以控制整个响应的内容。
@GetMapping("/setResponse")
public ResponseEntity<Test> responseConfig() {
// 状态码
HttpStatus responseStatusCode = HttpStatus.PERMANENT_REDIRECT;
// 响应头
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setLocation(URI.create("https://www.raingray.com"));
// 响应体
Test test = new Test(1213);
// 完整响应
ResponseEntity<Test> response = new ResponseEntity<>(test, httpHeaders, responseStatusCode);
return response;
}
这里我就将响应状态码改成 308 永久重定向,顺便新增响应头 Location,响应体使用 Jackson 自动序列化对象到 JSON。
GET /setResponse HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.100 Safari/537.36
Accept-Encoding: gzip, deflate, br
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
Connection: keep-alive
HTTP/1.1 308
Location: https://www.raingray.com
Content-Type: application/json
Date: Tue, 27 Aug 2024 03:15:11 GMT
Keep-Alive: timeout=20
Connection: keep-alive
Content-Length: 11
{"id":1213}
如果不想写这么完整还可以使用链式写法。
@GetMapping("/setSimpleResponse")
public ResponseEntity<Test> responseSimple() {
// 响应体
Test test = new Test(1213);
return ResponseEntity
.status(HttpStatus.PERMANENT_REDIRECT)
.header("Location", "https://www.raingray.com")
.body(test);
}
2.3 HTTP 消息转换器
HttpMessageConverter 接口,是说 Spring MVC 收到请求消息或者要响应消息,把这些内容进行转换,有时候响应的是对象需要转换成 JSON 或者收到的请求 JSON 转换成对象,响应的是字符串就需要把对应内容转换成字符串输出。
- StringHttpMessageConverter,不管请求还是响应都转换成字符串。
- FormHttpMessageConverter,传递 multipart/form-data 和 application/x-www-form-urlencoded 类型的请求体后,自动转换成对象就是调的它。
- MappingJackson2HttpMessageConverter,把请求 JSON 转换成 Java 对象,或者返回对象自动转换成 JSON。
2.3.1 默认消息转换器用法
1.请求
当你把请求体(@RequestBody)或者请求参数(@RequestParam)赋值给 String 形参时调用的 FormHttpMessageConverter 类转换,如果是请求体传递的 JSON 或者 fromData,却使用 POJO 类接收,会调用的 MappingJackson2HttpMessageConverter 类自动转换成对应 Java 对象。
2.响应
控制器方法用 @ResponseBody 或者返回类型指定 ResponseEntity,尝试返回 String 类型就是调用的 StringHttpMessageConverter,如果返回的是对象,用的 jackson-databind 自动把对象转换成 JSON 字符串,是调 MappingJackson2HttpMessageConverter 类做的处理。
2.3.2 自定义消息转换器🔨
前面以及返回 JSON,Spring 内置的消息转换器 MappingJackson2HttpMessageConverter 调 Jackson,如果用 fastjson,这个消息转换器怎么切换,是需要自己写还是有其他方法?
目前学习阶段没有这个需求,无需给自己添麻烦。
3 异常处理器
以前 Controller 要求传递参数、请求头却没传,页面页面上会报对应 Tomcat 错误页面说有异常,这是 Spring MVC 默认请求走 HandlerExceptionResolver 异常接口实现类 DefaultHandlerExceptionResolver 处理异常。
如果不想走默认异常,可以通过下面方式去处理。
3.1 出现异常转发到视图
1.注册 SimpleMappingExceptionResolver 全局异常处理
可以手动在 Spring 配置文件中注册 SimpleMappingExceptionResolver 异常处理器。
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<!--定义优先级-->
<!--<property name="order" value="0"/>-->
<!-- 定义默认的异常处理页面,当该异常类型注册时使用 -->
<property name="defaultErrorView" value="index"></property>
<!-- 定义异常处理页面用来获取异常信息的变量名 -->
<property name="exceptionAttribute" value="errorMessage"></property>
<!-- 定义需要特殊处理的异常,用类名或完全路径名作为key,异常页名作为值 -->
<property name="exceptionMappings">
<props>
<!--异常名称也可以简写成类名-->
<prop key="java.lang.ArithmeticException">error</prop>
</props>
</property>
</bean>
exceptionMappings 是要指定的异常转发到指定视图, prop 标签属性 key 指定当出现哪些异常就转发到指定视图,这里写逻辑视图名称。
defaultErrorView 是在注册的异常范围外出现出现异常自动转发到指定视图,相当于兜底不管发生什么异常我都能处理。
exceptionAttribute 是指定异常的信息属性名称,这个属性是放到请求域内,后续可以通过模板读取。
order 是异常优先级,当使用了 <mvc:annotation-driven/>
后所有异常会默认走 DefaultHandlerExceptionResolver 处理,只有通过提高 SimpleMappingExceptionResolver 优先级我们的配置才会生效。
2.@ControllerAdvice 配合 @ExceptionHandler 处理所有控制器方法的异常
创建一个异常类专门处理异常,只要任何控制器出现异常都会执行这里面对应的方法。
@ControllerAdvice
public class ExeceptionController {
// @ExceptionHandler(Exception1.class, Exception2.class) // 匹配多个异常
@ExceptionHandler(ArithmeticException.class)
public String exceptionHandller(ArithmeticException exception) {
System.out.println(exception.getMessage());
return "index";
}
}
@ControllerAdvice 仅能在类上使用,本质上用的 @Component 注解注册的 Bean。ExeceptionController 类里的控制器方法上的 @ExceptionHandler 是指定这个控制器方法是处理什么异常,这里匹配的是 ArithmeticException 这个异常,当匹配异常到后给 exception 参数注入一个 ArithmeticException,这样就能通过 Model 把异常消息放到请求域,后面模板取出来展示。
如果想要匹配多个异常可以用花括号 @ExceptionHandler{Exception1.class, Exception2.class}
。
3.@Controller 配合 @ExceptionHandler 处理指定控制器方法的异常
在 @Controller 控制器类下写的 @ExceptionHandler 只对当前控制器类下的控制器方法生效。
@Controller
public class TestException {
@GetMapping("/testAPI")
public String testControllerMethod() {
if (1 == 1) {
throw new RuntimeException();
}
return "index";
}
@ExceptionHandler(Exception.class)
public String handleException(Exception ex, Model model) {
model.addAttribute("message", ex.getMessage());
return "error";
}
}
当访问 /testAPI 时 testControllerMethod 会自动出现抛出 RuntimeException 异常,此时会被当前控制器类 handleException 处理,因为 Exception 是 RuntimeException 的父类。
目前 /testAPI 只能接受 GET 请求,要是用 POST 请求会抛出 HttpRequestMethodNotSupportedException 异常,这种异常 handleException 不会处理,是默认交给 DefaultHandlerExceptionResolver 处理。
4.@ControllerAdvice 配合 @ExceptionHandler 处理指定控制器方法的异常
@ControllerAdvice 可以缩小作用范围,这样 @ExceptionHandler 的方法就不会对范围外的异常做处理。
// Target all Controllers annotated with @RestController @ControllerAdvice(annotations = RestController.class) public class ExampleAdvice1 {} // Target all Controllers within specific packages @ControllerAdvice("org.example.controllers") public class ExampleAdvice2 {} // Target all Controllers assignable to specific classes @ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class}) public class ExampleAdvice3 {}
比如下面代码就限制异常范围,只有 org.example.controllers 中的方法出现了异常才会被 globalException 方法处理。
// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {
@ExceptionHandler({NullPointerException.class, ClassCastException.class})
public String globalException(RuntimeException exception, Model model) {
model.addAttribute("message", ex.getMessage());
return "error";
}
}
3.2 出现异常返回响应体
1.@Controller 配合 @ExceptionHandler 处理指定控制器方法异常
package com.raingray.controller;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
public class RestApiController {
@GetMapping(value = "/api/error", produces = "text/plain; charset=utf-8")
public String error(@RequestParam("num") String num) {
if (num.equals("1")) {
throw new RuntimeException("运行异常");
}
return "String 中文";
}
@ExceptionHandler
public ResponseEntity<String> exception(Exception e) {
String body = "出异常了:" + e.getMessage();
HttpHeaders headers = new HttpHeaders();
// headers.setContentType(MediaType.APPLICATION_JSON_UTF8); // 设置 application/json;charset=UTF-8
headers.set(HttpHeaders.CONTENT_TYPE, "text/plain; charset=utf-8");
return new ResponseEntity<>(body, headers,HttpStatus.INTERNAL_SERVER_ERROR);
}
}
只有当前 RestApiController 控制器类下的方法出现异常时,会被 exception 进行处理。
GET /api/error?num=1 HTTP/1.1
Host: localhost
Accept-Encoding: gzip, deflate, br
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
Connection: keep-alive
HTTP/1.1 500
Content-Type: text/plain;charset=utf-8
Content-Length: 27
Date: Thu, 29 Aug 2024 02:26:10 GMT
Connection: close
出异常了:运行异常
GET /api/error?%ff HTTP/1.1
Host: localhost
Accept-Encoding: gzip, deflate, br
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
Connection: keep-alive
HTTP/1.1 500
Content-Type: text/plain;charset=utf-8
Content-Length: 95
Date: Thu, 29 Aug 2024 02:26:36 GMT
Connection: close
出异常了:Required request parameter 'num' for method parameter type String is not present
2.@RestControllerAdvice 配合 @ExceptionHandler 处理所有控制器方法异常
要想全局启用注册 Bean 得用 @RestControllerAdvice,这个注解本质上是使用 @ControllerAdvice 和 @ResponseBody 进行注解。@ControllerAdvice 默认情况下是全局启用,如果想缩小范围在返回视图小节已经说明过了。
package com.raingray.controller;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class RestGlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<String> globalExceptionHandler(Exception e) {
String body = "出异常了(全局处理):" + e.getMessage();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON_UTF8); // 设置 application/json;charset=UTF-8
return new ResponseEntity<>(body, headers, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
此时再用 POST 请求 /api/error,发现确实被全局异常处理。
POST /api/error HTTP/1.1
Host: localhost
Accept-Encoding: gzip, deflate, br
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 4
%ff=
HTTP/1.1 500
Content-Type: application/json;charset=UTF-8
Content-Length: 71
Date: Thu, 29 Aug 2024 02:37:12 GMT
Connection: close
出异常了(全局处理):Request method 'POST' is not supported
那如果局部异常处理和全局异常处理都同时存在,哪个优先级更高呢?再重放 /api/error?num=1 请求,发现是局部异常优先级最高。
GET /api/error?num=1 HTTP/1.1
Host: localhost
Accept-Encoding: gzip, deflate, br
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
Connection: keep-alive
HTTP/1.1 500
Content-Type: text/plain;charset=utf-8
Content-Length: 27
Date: Thu, 29 Aug 2024 02:26:10 GMT
Connection: close
出异常了:运行异常
4 拦截器
Spring MVC 拦截器和 Servlet 规范过滤器都是一样的作用,只是优先级和作用范围不同,Filter 优先级更高,所有请求一定是先经过它处理,才放给 Dispcheater 进行分发,在 Dispcheater#doDispatch 方法处理过程中去执行拦截器。
HTTP 请求 -> Servlet 过滤器 -> Spring MVC 前端控制器 -> Spring MVC 拦截器
HTTP 响应 -> Spring MVC 拦截器 -> Spring MVC 前端控制器 -> Servlet 过滤器
这只是一个大概的过程,更详细的请求流程分析在下面使用过程中展开。
4.1 拦截器配置
实现 HandlerInterceptor 接口方法:
- preHandle,在控制器方法执行前运行,当 preHandle 运行完成后会根据 boolean 类型返回值判断要不要执行控制器方法和后续拦截器方法(postHandle/afterCompletion),false 不执行,true 执行,接口默认实现是返回 true。
- postHandle,控制器方法执行后运行 postHandle,如果 preHandle 是 flase 控制器方法不会执行,那么 postHandle 它也不会运行。接口默认实现是空。
- afterCompletion,视图渲染完成后运行(渲染完成后其实就直接把内容座位响应体返回了,请求完成),preHandle 为 false 不会运行。
接口这三个方法默认都有实现,所以不是必须强制在拦截器实现所有方法。
创建拦截器 Interceptor1.java。
package com.raingray.interceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
@Component
public class Interceptor1 implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("interceptor1#preHandle");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("interceptor1#postHandle");
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("interceptor1#afterCompletion");
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
注册拦截器,首先要注册成 Bean,随后启用拦截器,直接写名字的启用方式,默认情况是对所有控制器方法生效。
<!--拦截器注册-->
<context:component-scan base-package="com.raingray.interceptor"/>
<mvc:interceptors>
<!--benan name 默认是类名首字母小写,这里是 interceptor1-->
<bean class="com.raingray.interceptor.Interceptor1"/>
</mvc:interceptors>
如果你要筛选,只想对某个路径生效,或者是排除某个路径,可以用 interceptor 标签对拦截器做更详细的配置。看到 interceptors 就说明可以配置多个拦截器,这在下个小节再讲。
<mvc:interceptors>
<mvc:interceptor>
<!--** 拦截所有路径,* 只拦截一个路径-->
<mvc:mapping path="/**"/>
<!-- /test 路径不拦截 -->
<mvc:exclude-mapping path="/test"/>
<!--拦截器-->
<bean class="com.raingray.interceptor.Interceptor1"/>
</mvc:interceptor>
</mvc:interceptors>
运行控制器。
@RestController
public class RestApiController {
@GetMapping(value = "/api/error", produces = "text/plain; charset=utf-8")
public String error(@RequestParam("num") String num) {
if (num.equals("1")) {
throw new RuntimeException("运行异常");
}
return "String 中文";
}
}
访问 /api/error?num 这些方法执行顺序:
1.执行 preHandle
2.由于 preHandle 返回 true,运行 RestApiController#error 控制器方法在响应体中返回 "String 中文"
3.RestApiController#error 控制器方法运行完毕后,执行 postHandle
4.postHandle 执行完毕后执行 afterCompletion
14:26:59.088 [http-nio-80-exec-23] DEBUG org.springframework.web.servlet.DispatcherServlet -- GET "/api/error?num", parameters={masked}
14:26:59.088 [http-nio-80-exec-23] DEBUG org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping -- Mapped to com.raingray.controller.RestApiController#error(String)
interceptor1#preHandle
14:27:04.657 [http-nio-80-exec-23] DEBUG org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor -- Using 'text/plain;charset=utf-8;q=0.8', given [text/html, application/xhtml+xml, image/avif, image/webp, image/apng, application/xml;q=0.9, */*;q=0.8] and supported [text/plain;charset=utf-8]
14:27:04.657 [http-nio-80-exec-23] DEBUG org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor -- Writing ["String 中文"]
interceptor1#postHandle
interceptor1#afterCompletion
14:27:35.651 [http-nio-80-exec-23] DEBUG org.springframework.web.servlet.DispatcherServlet -- Completed 200 OK
4.2 多个拦截器执行顺序
当注册多个拦截器执行顺序是怎么样的?
<!--拦截器注册-->
<context:component-scan base-package="com.raingray.interceptor"/>
<mvc:interceptors>
<bean class="com.raingray.interceptor.Interceptor1"/>
<bean class="com.raingray.interceptor.Interceptor2"/>
<bean class="com.raingray.interceptor.Interceptor3"/>
<bean class="com.raingray.interceptor.Interceptor4"/>
</mvc:interceptors>
Interceptor1/Interceptor2/Interceptor3/Interceptor4 这四个拦截器代码一样,preHandler 都返回 true。访问 /api/error?num 后是按照注册先后顺序开始执行,规律是先进后出(源码中就是这样循环处理的)。
interceptor1#preHandle
interceptor2#preHandle
interceptor3#preHandle
interceptor4#preHandle
interceptor4#postHandle
interceptor3#postHandle
interceptor2#postHandle
interceptor1#postHandle
interceptor4#afterCompletion
interceptor3#afterCompletion
interceptor2#afterCompletion
interceptor1#afterCompletion
把 Interceptor3 的 preHandle 返回 false 后执行顺序如下(这只是大概的理解方式,真正的源码处理流程不是这么走的)。
interceptor1#preHandle
interceptor2#preHandle
interceptor3#preHandle
interceptor4#preHandle❌跳过不运行
interceptor4#postHandle❌跳过不运行
interceptor3#postHandle❌跳过不运行
interceptor2#postHandle❌跳过不运行
interceptor1#postHandle❌跳过不运行
interceptor4#afterCompletion❌跳过不运行
interceptor3#afterCompletion❌跳过不运行
interceptor2#afterCompletion
interceptor1#afterCompletion
因为 interceptor3#preHandle 返回 false,所以后面的 interceptor4#preHandle 压根不会运行,连所有的 postHandle 也跳过,而是直接来到 afterCompletion 开始运行,因为前面 interceptor2 和 interceptor1 的 preHandle 都返回 true 都会执行 afterCompletion。
简单来说,只要当前拦截器 preHandle 返回 false:
- preHandle。当前拦截器 preHandle 被执行。当前拦截器后面注册的拦截器 preHandle 不会执行;
- postHandle。所有拦截器 postHandle 不会执行
- afterCompletion。所有拦截器 preHandle 方法返回 false 不运行 afterCompletion,只有返回 true 的,对应 afterCompletion 会按照注册的顺序从后向前挨个执行
5 Spring MVC 注解方式开发
和前面 Spring 全注解开发一样,Spring MVC 也可以用类替代配置文件。
不管是用类还是用配置文件,本质上就是 Spring 管理了 Spring MVC 的对象,也就是常说的 Spring 整合 Spring MVC。
5.1 替代 web.xml 注册 Servlet
以前的 Web 项目中都是 Web 容器通过读取 web.xml 来初始化容器信息,但 Servlet 3.0 新增 ServletContainerInitializer 接口,可以通过实现接口的 onStartup 方法来初始化 Web 容器。
当 Web 容器启动的时候会创建 ServletContainerInitializer 接口实现类对象,调用 onStartup 方法完成初始化,刚好 Spring SpringServletContainerInitializer 类实现了这个接口。
SpringServletContainerInitializer 类 onStartup 注册逻辑中,最终是自动注册 WebApplicationInitializer 接口实现类 AbstractAnnotationConfigDispatcherServletInitializer 抽象类的子类,因此只要我们编写一个类继承 AbstractAnnotationConfigDispatcherServletInitializer 抽象类实现里面的方法就可以完成 DispatcherServlet 的初始化。
创建 Spring 配置类 ServletInitializer.java 初始化 Web 应用。
package com.raingray;
import jakarta.servlet.Filter;
import jakarta.servlet.MultipartConfigElement;
import jakarta.servlet.ServletRegistration;
import org.springframework.web.filter.CharacterEncodingFilter;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
public class ServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
// Spring 配置类
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[0];
}
// Spring MVC 配置类
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{SpringMvcConfig.class};
}
// 设置 DispatcherServlet URL
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
// 设置 Filter 类
@Override
protected Filter[] getServletFilters() {
// 字符编码
CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter();
characterEncodingFilter.setEncoding("UTF-8");
characterEncodingFilter.setForceEncoding(true);
return new Filter[]{characterEncodingFilter};
}
// 配置 DispatcherServlet
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
// 设置上传文件文件配置
registration.setMultipartConfig(new MultipartConfigElement(
"/tmp", // 文件存储目录
1024000, // 单个文件最大字节数 (例如 1MB)
2048000, // 上传请求中所有文件和参数值的总字节数 (例如 2MB)
0 // 达到指定字节大小就把文件写入磁盘,没达到就暂时放在内票,设置为0表示每个上传的文件都写入磁盘
));
}
}
这些方法对应 web.xml 内容如下:
- getRootConfigClasses 方法用来指定 Spring 配置类 Class 对象,通常用来注册和配置 Service 和 Dao 类,比如 Spring.class。
- getServletConfigClasses 方法用来指定 Spring MVC 配置类 Class 对象,本质上就是个 Spring 配置类,一般用来配置视图,拦截器......跟 Spring MVC 有关的设置。相当于用类替代了原先的 Spring XML 配置方式。
- getServletMappings 方法用来配置 DispatcherServlet 的 Url-Pattern,等同于 web.xml
<servlet-mapping>
中的<url-pattern>
。 - getServletFilters 方法用来配置 Filter,等同于 web.xml 配置
<filter>
和Mfilter-mapping>
- customizeRegistration 方法用来配置 DispatcherServlet。比如 registration.setInitParameter 等同于 web.xml
<servlet>
中的<init-param>
。registration.setMultipartConfig 可以配置文件上传大小和位置,等同于<multipart-config>
。registration.setLoadOnStartup 等同于<load-on-startup>
,可以设置是否启动容器就创建 Servlet 对象,在 AbstractDispatcherServletInitializer#registerDispatcherServlet 方法中registration.setLoadOnStartup(1);
,不清楚默认值是不是就是 1。
5.2 替代 Spring MVC XML 配置
配好了 DispatcherServlet 和 Filter 后,可以创建 Spring MVC 配置类 SpringMvcConfig.java 实现 WebMvcConfigurer 接口去配置视图等内容。虽然使用类代替了 XML,但是在 IDEA 中部署方式依旧没什么不同。具体操作忘了可以参考 Servlet 和 Maven 文章。
package com.raingray;
import com.raingray.interceptors.TestInterceptor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;
import org.thymeleaf.spring6.SpringTemplateEngine;
import org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver;
import org.thymeleaf.spring6.view.ThymeleafViewResolver;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.ITemplateResolver;
import java.util.ArrayList;
@Configuration
@ComponentScan("com.raingray.controller") // 扫描控制器
@EnableWebMvc // 等同于 <mvc:annotation-driven/>
public class SpringMvcConfig implements WebMvcConfigurer {
// 配置视图解析器
@Bean
public ThymeleafViewResolver getViewResolver(SpringTemplateEngine springTemplateEngine) {
ThymeleafViewResolver resolver = new ThymeleafViewResolver();
resolver.setTemplateEngine(springTemplateEngine);
resolver.setCharacterEncoding("UTF-8");
resolver.setOrder(1);
return resolver;
}
@Bean
public SpringTemplateEngine templateEngine(ITemplateResolver iTemplateResolver) {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(iTemplateResolver);
return templateEngine;
}
@Bean
public ITemplateResolver templateResolver(ApplicationContext applicationContext) {
SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
resolver.setApplicationContext(applicationContext); // 为什么需要设置应用域对象,不知道原因,看官网文档就是这么做的。
resolver.setPrefix("/WEB-INF/templates/");
resolver.setSuffix(".html");
resolver.setTemplateMode(TemplateMode.HTML);
resolver.setCharacterEncoding("UTF-8");
resolver.setCacheable(false);// 默认是开启缓存的,开发时可以关闭缓存,这样改动模板内容会立即生效
return resolver;
}
// 配置 url-pattern 默认视图
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("index");
}
// 配置静态资源访问,交给默认 Servlet 处理。
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
// 配置静态资源访问,通过匹配路径的方式处理。
/*@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**").addResourceLocations("/static/");
// 可以设置多个路径映射,最后还能设置静态资源缓存响应头 Cache-Control: max-age=3600
registry.addResourceHandler("/static/js/*")
.addResourceLocations("/static/js/*")
.setCachePeriod(3600);
}*/
// 拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
ArrayList<String> pattern = new ArrayList<>();
pattern.add("/static/1.txt");
pattern.add("/api/error");
registry.addInterceptor(new TestInterceptor())
.addPathPatterns(pattern)
.excludePathPatterns("/api/error2");
}
}
5.2.1 启动注解驱动
@EnableWebMvc 等同于 <mvc:annotation-driven/>
。
......
@EnableWebMvc
public class SpringMvcConfig implements WebMvcConfigurer {
......
}
5.2.2 配置视图
这几个方法是配置视图解析器的,跟 XML 需要注册的对象一一对应,由于 XML 中注册视图是有用到内部 Bean,在配置类中可以使用 @Bean 把返回对象放入 IoC 容器进行管理,最后 @Bean 方法形参是自动根据类型注入就完成了嵌套。最后必须在配置类上启用 @EnableWebMvc 视图才能生效。
// 配置视图解析器
@Bean
public ThymeleafViewResolver getViewResolver(SpringTemplateEngine springTemplateEngine) {
ThymeleafViewResolver resolver = new ThymeleafViewResolver();
resolver.setTemplateEngine(springTemplateEngine);
resolver.setCharacterEncoding("UTF-8");
resolver.setOrder(1);
return resolver;
}
@Bean
public SpringTemplateEngine templateEngine(ITemplateResolver iTemplateResolver) {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(iTemplateResolver);
return templateEngine;
}
@Bean
public ITemplateResolver templateResolver(ApplicationContext applicationContext) {
SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
resolver.setApplicationContext(applicationContext);
resolver.setPrefix("/WEB-INF/templates/");
resolver.setSuffix(".html");
resolver.setTemplateMode(TemplateMode.HTML);
resolver.setCharacterEncoding("UTF-8");
resolver.setCacheable(false);// 默认是开启缓存的,开发时可以关闭缓存,这样改动模板内容会立即生效
return resolver;
}
对应 XML 配置。
<!--注册视图解析器-->
<bean id="thymeleafViewResolver" class="org.thymeleaf.spring6.view.ThymeleafViewResolver">
<!--作用于视图渲染的过程中,可以设置视图渲染后输出时采用的编码字符集。响应给客户端的字符集。-->
<property name="characterEncoding" value="UTF-8"/>
<!--如果配置多个视图解析器,它来决定优先使用哪个视图解析器,它的值越小优先级越高-->
<property name="order" value="1"/>
<!--当 ThymeleafViewResolver 渲染模板时,会使用该模板引擎来解析、编译和渲染模板-->
<property name="templateEngine">
<bean class="org.thymeleaf.spring6.SpringTemplateEngine">
<!--用于指定 Thymeleaf 模板引擎使用的模板解析器。模板解析器负责根据模板位置、模板资源名称、文件编码等信息,加载模板并对其进行解析-->
<property name="templateResolver">
<bean class="org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver">
<!--设置模板文件的位置(前缀)-->
<property name="prefix" value="/WEB-INF/templates/"/>
<!--设置模板文件后缀(后缀),Thymeleaf文件扩展名不一定是html,也可以是其他,例如txt,大部分都是html-->
<property name="suffix" value=".html"/>
<!--设置模板类型,例如:HTML,TEXT,JAVASCRIPT,CSS等-->
<property name="templateMode" value="HTML"/>
<!--用于模板文件在读取和解析过程中采用的编码字符集。模板文件中字符的字符集-->
<property name="characterEncoding" value="UTF-8"/>
</bean>
</property>
</bean>
</property>
</bean>
5.2.3 路径绑定默认视图
配置默认视图必须启用 @EnableWebMvc。
// 配置 url-pattern 默认视图
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("index");
}
对应 XML 配置。
<mvc:view-controller path="/" view-name="index"/>
5.2.4 访问静态资源
1.使用默认 Servlet 处理静态资源。
在类中启用,等同于 XML 配置 <mvc:default-servlet-handler/>
。
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
必须要启用 @EnableWebMvc 后自动引入才能生效。访问静态资源时走默认 Servlet 处理,所以不会经过拦截器进行限制。
2.匹配路径处理静态资源。
等同于替代 <mvc:resources mapping="/static/**" location="/static/"/>
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**").addResourceLocations("/static/");
// 可以设置多个路径映射,最后还能设置静态资源缓存响应头 Cache-Control: max-age=3600
registry.addResourceHandler("/static/js/*")
.addResourceLocations("/static/js/*")
.setCachePeriod(3600);
}
在使用时必须要启用 @EnableWebMvc 才能生效。这样设置静态资源访问,会经过拦截器处理。
5.2.5 拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
ArrayList<String> pattern = new ArrayList<>();
pattern.add("/static/1.txt");
pattern.add("/api/error");
registry.addInterceptor(new TestInterceptor())
.addPathPatterns(pattern)
.excludePathPatterns("/api/error2");
}
用于替代 XML 配置。
<mvc:interceptors>
<mvc:interceptor>
<!--** 拦截所有路径,* 只拦截一个路径-->
<mvc:mapping path="/**"/>
<!-- /test 路径不拦截 -->
<mvc:exclude-mapping path="/test"/>
<!--拦截器-->
<bean class="com.raingray.interceptor.Interceptor1"/>
</mvc:interceptor>
</mvc:interceptors>
5.2.3 Spring MVC 配置类拆分🔨
有时候类配置太多,可以多建立几个配置类,最后在注配置类使用 @Import 进行导入,这个
6 部署应用
用 Maven 把项目打成 war 包,将 war 包上传到 Tomcat 的 webapps 目录下,重启 Tomcat 就自动解压了,或者你手动解压重启也可以。一旦重启完毕就能访问项目。别忘了把 war 删除,解压不会自动删除。
最近更新:
发布时间: