SpringBoot3 基础教程之四 Web 开发

前言

本章节所需的案例代码,可以直接从 GitHub 下载对应章节 spring-boot3-04

国际化

实现步骤

国际化的实现步骤如下:

  • 1、Spring Boot 在类路径根下查找 messages 资源绑定文件,文件名默认为 messages.properties
  • 2、多语言环境可以定义多个资源文件,命名规则为 messages_区域代码.properties,如:
    • messages.properties:默认环境
    • messages_zh_CN.properties:中文环境
    • messages_en_US.properties:英文环境
  • 3、在程序中可以自动注入 MessageSource 组件,获取国际化的配置项值
  • 4、在页面中可以使用表达式 #{} 获取国际化的配置项值

常用配置

国际化的自动配置可参考 MessageSourceAutoConfiguration 自动配置类。

1
2
3
4
# 字符集编码
spring.messages.encoding=UTF-8
# 资源文件名的前缀
spring.messages.basename=messages

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Slf4j
@Controller
public class LoginController {

/**
* 获取国际化消息的组件
*/
@Autowired
public MessageSource messageSource;

@GetMapping("/login")
public String login(HttpServletRequest request) {
Locale local = request.getLocale();
// 通过代码的方式获取国际化配置文件中指定的配置项的值
String login = messageSource.getMessage("login", null, local);
log.info("login: {}", login);
return "login";
}

}

路径匹配

Spring 5.3 之后加入了更多的请求路径匹配的实现策略,以前只支持 AntPathMatcher 策略,现在额外提供了 PathPatternParser 策略(默认),并且支持指定使用哪种策略。

Ant 风格

Ant 风格的路径模式语法具有以下规则:

  • *:表示任意数量的字符。
  • ?:表示任意一个字符。
  • **:表示任意数量的目录。
  • {}:表示一个命名的模式占位符。
  • []:表示字符集合,例如 [a-z] 表示小写字母。

Ant 风格的路径模式使用例子:

  • *.html:匹配任意名称,扩展名为 .html 的文件
  • /folder1/*/*.java:匹配在 folder1 目录下的任意两级目录下的 .java 文件。
  • /folder2/**/*.jsp:匹配在 folder2 目录下任意目录深度的 .jsp 文件。
  • /{type}/{id}.html:匹配任意文件名为 {id}.html,且在任意命名的 {type} 目录下的文件。

特别注意

  • Ant 风格的路径模式语法中的特殊字符需要转义,如:
  • 要匹配文件路径中的星号,则需要转义为 \\*
  • 要匹配文件路径中的问号,则需要转义为 \\?

模式切换

  • AntPathMatcher 与 PathPatternParser

    • PathPatternParser 在 jmh 基准测试下,有 6 - 8 倍吞吐量提升,降低了 30% - 40% 空间分配率
    • PathPatternParser 兼容 AntPathMatcher 语法,并支持更多类型的路径模式
    • PathPatternParser 多段匹配 ** 的支持仅允许在模式末尾使用
  • PathPatternParser 路径匹配策略的使用

1
2
3
4
5
@GetMapping("/a*/b?/{p1:[a-f]+}/**")
public String hello(HttpServletRequest request, @PathVariable("p1") String path) {
log.info("路径变量: {}", path);
return request.getRequestURI();
}
  • 切换路径匹配策略,ant_path_matcher 是旧版策略,path_pattern_parser 是新版策略
1
2
# 切换路径匹配策略
spring.mvc.pathmatch.matching-strategy=ant_path_matcher

总结

  • SpringBoot 默认的路径匹配策略是由 PathPatternParser 提供的
  • 如果路径中间需要有 **,则需要切换为 Ant 风格的路径匹配策略 AntPathMatcher

内容协商

内容协商指的是一套系统适配多端数据的返回。

多端内容适配

默认规则

  • 基于请求头内容协商(默认开启)
    • 客户端向服务端发送请求,携带 HTTP 标准的 Accept 请求头
    • 客户端的请求头类型:Accept: application/jsonAccept: text/xmlAccept: text/yaml
    • 服务端根据客户端请求头期望的数据类型进行动态返回
  • 基于请求参数内容协商(手动开启)
    • 发送请求 GET /projects/spring-boot?format=json
    • 匹配到 @GetMapping("/projects/spring-boot")
    • 根据请求参数协商,决定返回哪种数据类型,优先返回 JSON 类型数据
    • 发送请求 GET /projects/spring-boot?format=xml,优先返回 XML 类型数据

XML 内容协商案例

这里演示如何在请求同一个接口时,支持根据请求参数返回 JSON 或者 XML 格式的数据。

  • 引入支持输出 XML 内容的依赖
1
2
3
4
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
  • 标注 XML 注解
1
2
3
4
5
6
7
8
@JacksonXmlRootElement  // 支持输出 XML 格式的数据
@Data
public class Person {
private Long id;
private String userName;
private String email;
private Integer age;
}
  • 编写控制器
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
import com.clay.boot.web.domain.Person;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/person")
public class PersonController {

/**
* 添加 @ResponseBody 注解
*/
@ResponseBody
@GetMapping("/get")
public Person get() {
Person person = new Person();
person.setId(1L);
person.setAge(18);
person.setUserName("张三");
person.setEmail("aaa@qq.com");
return person;
}

}
  • 开启基于请求参数的内容协商
1
2
3
4
5
# 开启基于请求参数的内容协商功能,此功能默认不开启,默认的参数名为 format
spring.mvc.contentnegotiation.favor-parameter=true

# 指定内容协商时使用的参数名,默认的参数名为 format
spring.mvc.contentnegotiation.parameter-name=type
  • 代码测试结果

提示

  • SpringBoot 默认支持接口返回 JSON 数据,因为 Web 场景启动器默认引入了 Jackson 处理 JSON 的包。

配置协商规则与支持类型

  • 更改内容协商方式
1
2
3
4
5
# 开启基于请求参数的内容协商功能,此功能默认不开启,默认的参数名为 format
spring.mvc.contentnegotiation.favor-parameter=true

# 指定内容协商时使用的参数名,默认的参数名为 format
spring.mvc.contentnegotiation.parameter-name=type
  • 大多数 MediaType 都是开箱即用的,也可以自定义内容类型,如:
1
spring.mvc.contentnegotiation.media-types.yaml=text/yaml

自定义内容返回

YAML 内容协商案例

  • 引入支持输出 YAML 内容的依赖
1
2
3
4
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>
  • 新增一种媒体类型(MediaType),并开启基于请求参数的内容协商
1
2
3
4
5
6
7
8
# 新增一种媒体类型
spring.mvc.contentnegotiation.media-types.yaml=text/yaml

# 开启基于请求参数的内容协商功能,此功能默认不开启,默认的参数名为 format
spring.mvc.contentnegotiation.favor-parameter=true

# 指定内容协商时使用的参数名,默认的参数名为 format
spring.mvc.contentnegotiation.parameter-name=type
  • 编写可以支持 YAML 格式数据的 HttpMessageConverter
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
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;

import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;

public class YamlHttpMessageConverter extends AbstractHttpMessageConverter<Object> {

private final ObjectMapper objectMapper;

public YamlHttpMessageConverter() {
// 指定支持的媒体类型
super(new MediaType("text", "yaml", StandardCharsets.UTF_8));
// 初始化YAML工具
YAMLFactory yamlFactory = new YAMLFactory().disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER);
this.objectMapper = new ObjectMapper(yamlFactory);
}

/**
* 支持的类
*/
@Override
protected boolean supports(Class<?> clazz) {
// TODO 只处理对象类型,不处理基本类型(如 int)
return true;
}

/**
* 处理方法参数(@RequestBody)
*/
@Override
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return null;
}

/**
* 处理方法返回值(@ResponseBody)
*/
@Override
protected void writeInternal(Object returnValue, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
try (OutputStream outputStream = outputMessage.getBody()) {
this.objectMapper.writeValue(outputStream, returnValue);
}
}

}
  • 添加 HttpMessageConverter 组件,专门负责把返回值对象输出为 YAML 格式的数据
1
2
3
4
5
6
7
8
9
10
@Configuration
public class WebConfiguration implements WebMvcConfigurer {

@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// 添加一个支持将返回值对象转为 YAML 格式的 MessageConverter
converters.add(new YamlHttpMessageConverter());
}

}
  • 代码测试结果

自定义内容返回总结

  • 如何自定义内容返回
    • 1、配置媒体类型支持: spring.mvc.contentnegotiation.media-types.yaml=text/yaml
    • 2、编写对应的 HttpMessageConverter,在内部要告诉 SpringBoot 支持的媒体类型
    • 3、往容器中放一个 WebMvcConfigurer 组件,并添加自定义的 HttpMessageConverter

内容协商原理分析

1、@ResponseBody 的底层由 HttpMessageConverter 处理数据,即标注了 @ResponseBody 的返回值,将会由支持它的 HttpMessageConverter 将数据返回给浏览器。

  • 如果 Controller 方法的返回值标注了 @ResponseBody 注解

    • 请求进来先来到 DispatcherServletdoDispatch() 进行处理
    • 找到一个 HandlerAdapter 适配器,利用适配器执行目标方法
    • RequestMappingHandlerAdapter 会执行,调用 invokeHandlerMethod() 来执行目标方法
    • 在目标方法执行之前,需要准备好两样东西
      • HandlerMethodArgumentResolver:参数解析器,确定目标方法的每个参数值
      • HandlerMethodReturnValueHandler:返回值处理器,确定目标方法的返回值该怎么处理
    • RequestMappingHandlerAdapter 里面的 invokeAndHandle() 真正执行目标方法
    • 目标方法执行完成,会返回返回值的对象
    • 去找一个合适的返回值处理器 HandlerMethodReturnValueHandler
    • 最终找到 RequestResponseBodyMethodProcessor,它能处理标注了 @ResponseBody 注解的方法
    • RequestResponseBodyMethodProcessor 调用 writeWithMessageConverters(),利用 MessageConverter 把返回值输出给浏览器
  • HttpMessageConverter 会先进行内容协商

    • 遍历所有的 MessageConverter,看哪个支持这种内容类型的数据
    • 默认的 MessageConverter这些
    • 最终因为需要返回 JSON 数据,所以通过 MappingJackson2HttpMessageConverter 输出 JSON 数据
    • Jackson 利用 ObjectMapper 把返回值对象写出去

2、WebMvcAutoConfiguration 提供了 6 种 默认的 HttpMessageConverters

  • EnableWebMvcConfiguration 通过 addDefaultHttpMessageConverters 添加了默认的 MessageConverter,如下:
    • ByteArrayHttpMessageConverter:支持字节数据读写
    • StringHttpMessageConverter:支持字符串读写
    • ResourceHttpMessageConverter:支持资源读写
    • ResourceRegionHttpMessageConverter:支持分区资源写出
    • AllEncompassingFormHttpMessageConverter:支持表单 XML/JSON 读写
    • MappingJackson2HttpMessageConverter:支持请求响应体 JSON 读写

提示

SpringBoot 提供默认的 MessageConverter 功能有限,仅用于 JSON 或者普通的返回数据。如果需要增加新的内容协商功能,必须添加新的 HttpMessageConverter

模板引擎

模板引擎介绍

由于 SpringBoot 使用了嵌入式 Servlet 容器,所以 JSP 默认是不能使用的。如果需要服务端页面渲染,优先考虑使用模板引擎技术。

SpringBoot 默认包含了以下模板引擎的自动配置,模板引擎的页面默认放在 src/main/resources/templates 目录下。

Thymeleaf 整合

  • 引入 Maven 依赖
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
  • 添加配置内容(可选)
1
2
3
4
5
6
7
8
9
spring:
thymeleaf:
enabled: true
cache: true
mode: HTML
suffix: .html
encoding: UTF-8
prefix: classpath:/templates/
check-template-location: true
  • 编写 Controller 类,往模板文件中存放值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.thymeleaf.util.StringUtils;

@Controller
public class WelcomeController {

@GetMapping("/")
public String welcome(@RequestParam(name = "name", required = false) String name, Model model) {
if (StringUtils.isEmpty(name)) {
name = "Thymeleaf";
}
model.addAttribute("name", name);
return "welcome";
}

}
  • 编写 HTML 模板页面,显示在 Controller 类中设置的值
1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Welcome</title>
</head>
<body>
<h1>Hello <span th:text="${name}"></span></h1>
</body>
</html>
  • 浏览器访问 http://127.0.0.1:8080/?name=Peter,显示的页面内容如下:

  • 自动配置原理
    • 自动配置类是 org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration
    • 配置属性绑定在 ThymeleafProperties 中,对应配置文件中以 spring.thymeleaf 开始前缀的内容
    • 所有的模板页面默认都放在 classpath:/templates 文件夹下
    • 自动配置实现的默认效果
    • 所有的模板页面都在 classpath:/templates/ 文件夹下面找
    • 找后缀名为 .html 的模板页面进行渲染

Thymeleaf 基础语法

核心用法

  • th:xxx:动态渲染指定的 HTML 标签属性值或者 th 指令(遍历、判断等)
    • th:text:渲染 HTML 标签体内的内容
    • th:utext:渲染 HTML 标签体内的内容,但不会转义,显示为 HTML 原本的样子
    • th:任意HTML属性:标签指定属性渲染
    • th:attr:标签任意属性渲染
    • th:ifth:each:其他 th 指令
1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
</head>
<body>
<p th:text="${content}">原内容</p>
<a th:href="${url}">登录</a>
<img th:src="${imgUrl}" style="width:300px; height: 200px">
<img th:attr="src=${imgUrl},style=${imgStyle},title=#{logo},alt=#{logo}">
<img th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" th:if="${imgShow}">
</body>
</html>
  • 表达式:用来动态取值

    • ${}:变量取值,使用 Model 共享给页面的值都可以直接用 ${} 获取
    • @{}:URL 路径
    • #{}:国际化消息
    • ~{}:片段引用
    • *{}:变量选择:需要配合 th:object 绑定对象
  • 系统工具 & 内置对象:官方文档

    • param:请求参数对象
    • session:Session 对象
    • application:Application 对象
    • #execInfo:模板执行信息
    • #messages:国际化消息
    • #uris:URI/URL 工具
    • #conversions:类型转换工具
    • #dates:日期工具,是 java.util.Date 对象的工具类
    • #calendars:类似 #dates,只不过是 java.util.Calendar 对象的工具类
    • #temporals: JDK 8+ 的 java.time API 工具类
    • #numbers:数字操作工具
    • #strings:字符串操作
    • #objects:对象操作
    • #bools:布尔操作
    • #arrays:Array 工具
    • #lists:List 工具
    • #sets:Set 工具
    • #maps:Map 工具
    • #aggregates:集合聚合工具(sum、avg)
    • #ids:ID 生成工具

语法示例

  • 表达式:

    • 变量取值:${...}
    • URL 取值:@{...}
    • 国际化消息:#{...}
    • 变量选择:*{...}
    • 片段引用: ~{...}
  • 常见:

    • 文本:one textanother one!,…
    • 数字:0,34,3.0,12.3,…
    • 布尔:truefalse
    • Null:null
    • 变量名:one,sometext,main…
  • 文本操作:

    • 拼接字符串: +
    • 文本内容替换:| The name is ${name} |
  • 布尔操作:

    • 二进制运算: andor
    • 取反:!not
  • 比较运算:

    • 比较:><<=>=gtltgele
    • 等值运算:==!=eqne
  • 条件运算:

    • if-then: (if)?(then)
    • if-then-else:(if)?(then):(else)
    • default:(value)?:(defaultValue)
  • 特殊语法:

    • 无操作:_

以上所有语法都可以嵌套组合

1
'User is of type ' + (${user.isAdmin()} ? 'Administrator' : (${user.type} ?: 'Unknown'))

集合遍历

  • 语法:th:each="元素名, 迭代状态 : ${集合}"
  • 迭代状态有以下属性:
    • index:当前遍历元素的索引,从 0 开始
    • count:当前遍历元素的索引,从 1 开始
    • size:需要遍历元素的总数量
    • current:当前正在遍历的元素对象
    • even/odd:是否偶数 / 奇数行
    • first:是否第一个元素
    • last:是否最后一个元素
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
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Welcome</title>
</head>
<body>
<table border="1" cellspacing="0">
<thead>
<tr>
<th>ID</th>
<th>姓名</th>
<th>年龄</th>
<th>邮箱</th>
</tr>
</thead>
<tbody>
<tr th:each="person : ${persons}">
<td th:text="${person.id}"></td>
<td th:text="${person.userName}"></td>
<td th:text="${person.age}"></td>
<td th:text="${person.email}"></td>
</tr>
</tbody>
</table>

<table border="1" cellspacing="0">
<thead>
<tr>
<th>ID</th>
<th>姓名</th>
<th>年龄</th>
<th>邮箱</th>
</tr>
</thead>
<tbody>
<tr th:each="person, iterStat : ${persons}" th:class="${iterStat.odd} ? 'odd'" th:index="${iterStat.index}">
<td th:text="${person.id}"></td>
<td th:text="${person.userName}"></td>
<td th:text="${person.age}"></td>
<td th:text="${person.email}"></td>
</tr>
</tbody>
</table>
</body>
</html>

条件判断

  • 表达式判断
1
2
3
<tr th:each="person : ${persons}">
<td th:text="${person.age >= 18 ? '成年人' : '未成年人'}"></td>
</tr>
  • th:if 判断
1
<a href="@{/comments.html}" th:if="${not #lists.isEmpty(prod.comments)}">view</a>
  • th:switch 判断
1
2
3
4
5
6
<div th:switch="${person.role}">
<span th:case="pm">项目经理</span>
<span th:case="admin">管理员</span>
<span th:case="hr">HR</span>
<span th:case="*">其他</span>
</div>

属性优先级

优先级(值越小优先级越高)功能属性
1 片段包含th:insertth:replace
2 遍历th:each
3 判断th:ifth:unlessth:switchth:case
4 定义本地变量th:objectth:with
5 通用方式属性修改th:attrth:attrprependth:attrappend
6 指定属性修改th:valueth:hrefth:src
7 文本值th:textth:utext
8 片段指定th:fragment
9 片段移除th:remove
1
2
3
<ul>
<li th:each="item : ${items}" th:text="${item.description}">Item description here...</li>
</ul>

行内写法

语法:[[...]] or [(...)]

1
<p>[[${session.user.name}]]</p>

等同于

1
<p th:text="${session.user.name}"></p>

变量选择

1
2
3
4
5
<div th:object="${session.user}">
<p>Name: <span th:text="*{firstName}">Sebastian</span>.</p>
<p>Surname: <span th:text="*{lastName}">Pepper</span>.</p>
<p>Nationality: <span th:text="*{nationality}">Saturn</span>.</p>
</div>

等同于

1
2
3
4
5
<div>
<p>Name: <span th:text="${session.user.firstName}">Sebastian</span>.</p>
<p>Surname: <span th:text="${session.user.lastName}">Pepper</span>.</p>
<p>Nationality: <span th:text="${session.user.nationality}">Saturn</span>.</p>
</div>

模板布局

  • 模版布局的语法
    • 定义模板:th:fragment
    • 引用模板:~{templateName::selector}
    • 插入模板:th:insertth:replace

提示

  • 模板布局一般用于实现代码片段的重复使用。
  • 定义模板布局(如 footer.html
1
<span th:fragment="copy">&copy; 2011 The Good Thymes Virtual Grocery</span>
  • 引用模板布局(如在 index.html 中引用)
1
2
3
4
<body>
<div th:insert="~{footer :: copy}"></div>
<div th:replace="~{footer :: copy}"></div>
</body>
  • 实现的效果(如 index.html 最终的渲染结果)
1
2
3
4
5
6
<body>
<div>
<span>&copy; 2011 The Good Thymes Virtual Grocery</span>
</div>
<span>&copy; 2011 The Good Thymes Virtual Grocery</span>
</body>

Devtools

SpringBoot 提供了 Devtools 工具用于代码的热加载,首先引入依赖:

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>

更改 Thymeleaf 的模板页面后,使用快捷键 Ctrl + F9 就可以让页面的更改立即生效。值得一提的是,对于 Java 代码的修改,如果 Devtools 热启动了,可能会引起一些 Bug,且难以排查。

最新特性

Problemdetails

Problemdetails 实现了 RFC 7807 规范,用于返回新格式的错误信息。

源码介绍

  • ProblemDetailsExceptionHandler 是一个 @ControllerAdvice,用于集中处理系统异常
1
2
3
4
5
6
7
8
9
10
11
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "spring.mvc.problemdetails", name = "enabled", havingValue = "true")
static class ProblemDetailsErrorHandlingConfiguration {

@Bean
@ConditionalOnMissingBean(ResponseEntityExceptionHandler.class)
ProblemDetailsExceptionHandler problemDetailsExceptionHandler() {
return new ProblemDetailsExceptionHandler();
}

}
1
2
3
4
@ControllerAdvice
final class ProblemDetailsExceptionHandler extends ResponseEntityExceptionHandler {

}
  • ProblemDetailsExceptionHandler 默认可以处理以下异常,如果系统出现以下异常,会被 SpringBoot 以 RFC 7807 规范的方式返回错误数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@ExceptionHandler({
HttpRequestMethodNotSupportedException.class,
HttpMediaTypeNotSupportedException.class,
HttpMediaTypeNotAcceptableException.class,
MissingPathVariableException.class,
MissingServletRequestParameterException.class,
MissingServletRequestPartException.class,
ServletRequestBindingException.class,
MethodArgumentNotValidException.class,
NoHandlerFoundException.class,
AsyncRequestTimeoutException.class,
ErrorResponseException.class,
ConversionNotSupportedException.class,
TypeMismatchException.class,
HttpMessageNotReadableException.class,
HttpMessageNotWritableException.class,
BindException.class
})

使用说明

  • ProblemDetails 功能默认是关闭的,需要手动开启
1
spring.mvc.problemdetails.enabled=true
  • ProblemDetails 启用后,当 SpringBoot 捕获到异常后,默认会响应 JSON 数据和返回 HTTP 状态码 405,且响应的 Header 是 Content-Type: application/problem+json
1
2
3
4
5
6
7
{
"type": "about:blank",
"title": "Method Not Allowed",
"status": 405,
"detail": "Method 'POST' is not supported.",
"instance": "/list"
}

函数式 Web

Spring MVC 5.2 以后,允许使用函数式的方式定义 Web 的请求处理流程。

Web 请求处理的两种方式

  • 1、@Controller + @RequestMapping:耦合式(路由和业务耦合)
  • 2、函数式 Web:分离式(路由和业务分离)

使用场景

  • 场景:User RESTful - CRUD
    • GET /user/1:获取 1 号用户
    • GET /users:获取所有用户
    • POST /user:请求体携带 JSON,新增一个用户
    • PUT /user/1:请求体携带 JSON,修改 1 号用户
    • DELETE /user/1:删除 1 号用户

核心类

  • RouterFunction:定义路由信息,即发送什么请求,由谁来处理
  • RequestPredicate:请求方式(如 GET、POST)、请求参数
  • ServerRequest:封装请求数据
  • ServerResponse:封装响应数据

使用案例

  • 实体类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class User {

private Long id;
private String userName;
private String email;
private Integer age;
private String role;

}
  • 定义路由信息
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
import com.clay.boot.web.biz.UserBizHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.function.RequestPredicate;
import org.springframework.web.servlet.function.RequestPredicates;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.RouterFunctions;
import org.springframework.web.servlet.function.ServerResponse;

@Configuration
public class RoutingConfiguration {

private static final RequestPredicate ACCEPT_ALL = RequestPredicates.accept(MediaType.ALL);
private static final RequestPredicate ACCEPT_JSON = RequestPredicates.accept(MediaType.APPLICATION_JSON);

@Bean
public RouterFunction<ServerResponse> userRoute(UserBizHandler userBizHandler) {
return RouterFunctions.route()
.GET("/user/{id}", ACCEPT_ALL, userBizHandler::getUser)
.GET("/users", ACCEPT_ALL, userBizHandler::listUser)
.POST("/user", ACCEPT_JSON, userBizHandler::addUser)
.PUT("/user/{id}", ACCEPT_JSON, userBizHandler::updateUser)
.DELETE("/user/{id}", ACCEPT_ALL, userBizHandler::deleteUser)
.build();
}

}
  • 业务处理
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
import com.clay.boot.web.domain.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.servlet.function.ServerResponse;

import java.util.ArrayList;
import java.util.List;

@Slf4j
@Component
public class UserBizHandler {

public ServerResponse getUser(ServerRequest serverRequest) throws Exception {
Long id = Long.parseLong(serverRequest.pathVariable("id"));
User user = new User(id, "Peter1", "peter@gmail.com", 18, "pm");
return ServerResponse.ok().body(user);
}

public ServerResponse listUser(ServerRequest serverRequest) throws Exception {
List<User> users = new ArrayList<>();
users.add(new User(1L, "Peter1", "peter@gmail.com", 18, "pm"));
users.add(new User(2L, "Peter2", "peter@gmail.com", 16, "admin"));
users.add(new User(3L, "Peter3", "peter@gmail.com", 18, "pm"));
return ServerResponse.ok().body(users);
}

public ServerResponse addUser(ServerRequest serverRequest) throws Exception {
User user = serverRequest.body(User.class);
log.info("user save success, {}", user.toString());
return ServerResponse.ok().build();
}

public ServerResponse deleteUser(ServerRequest serverRequest) throws Exception {
Long id = Long.parseLong(serverRequest.pathVariable("id"));
log.info("user {} delete success", id);
return ServerResponse.ok().build();
}

public ServerResponse updateUser(ServerRequest serverRequest) throws Exception {
User user = serverRequest.body(User.class);
Long id = Long.parseLong(serverRequest.pathVariable("id"));
log.info("user {} update success, {}", id, user.toString());
return ServerResponse.ok().build();
}

}