后端接口返回什么,json串?xml文档?html网页?文件?还是重定向?
Web花样返回
- 1: 1.Freemaker环境搭建
- 2: 2.Thymeleaf环境搭建
- 3: 3.Beetl环境搭建
- 4: 4.返回文本、网页、图片的操作姿势
- 5: 5.请求重定向
- 6: 6.404、500异常页面配置
- 7: 7.全局异常处理
- 8: 8.自定义异常处理HandlerExceptionResolver
- 9: 9.开启GZIP数据压缩
- 10: 10.RestTemplate 4xx/5xx 异常信息捕获
- 11: 11.自定义返回Http Code的n种姿势
- 12: 12.异步请求知识点与使用姿势小结
- 13: 13.SSE服务器发送事件详解
- 14: 14.thymeleaf foreach踩坑记录
- 15: 15.如何支持下划线驼峰互转的传参与返回
- 16: 16.定义接口返回类型的几种方式
1 - 1.Freemaker环境搭建
现在的开发现状比较流行前后端分离,使用springboot搭建一个提供rest接口的后端服务特别简单,引入spring-boot-starter-web
依赖即可。那么在不分离的场景下,比如要开发一个后端使用的控制台,这时候可能并没有前端资源,由javaer自己来客串一把,我希望简单一点,前后端项目都集成在一起,一个jar包运行起来就完事,可以怎么搞呢?
本篇将介绍一下如何使用springboot集合freemaker引擎来搭建web应用
I. 准备
Freemaker是模板引擎,和jsp的作用差不多,对于它的不太清楚的同学可以参考一下官方文档 https://freemarker.apache.org/docs/index.html
1. 依赖
首先我们是需要一个springboot项目,基本的pom结构大都相似
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.RELEASE</version>
<relativePath/> <!-- lookup parent from update -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-cloud.version>Finchley.RELEASE</spring-cloud.version>
<java.version>1.8</java.version>
</properties>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</pluginManagement>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
在这个项目中,我们主要需要引入两个依赖包,一个web,一个freemaker
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
</dependencies>
2. 配置参数
通常我们直接使用默认的freemaker参数配置即可,下面给出几个常用的
spring:
freemarker:
charset: UTF-8
# 本机测试时建议设置为false,上线时设置为true
cache: true
# 表示模板文件(类html文件)的后缀
suffix: .ftl
freemaker的参数,主要对应的是org.springframework.boot.autoconfigure.freemarker.FreeMarkerProperties
II. 项目搭建演示
1. 项目结构
搭建一个web项目和我们之前的纯后端项目有点不一样,前端资源放在什么地方,依赖文件怎么处理都是有讲究的,下面是一个常规的项目结构
如上图,前端资源文件默认放在resources目录下,下面有两个目录
templates
:存放模板文件,可以理解为我们编写的html,注意这个文件名不能有问题static
: 存放静态资源文件,如js,css,image等
2. Rest服务
我们这里提供了三个接口,主要是为了演示三种不同的数据绑定方式
@Controller
public class IndexController {
@GetMapping(path = {"", "/", "/index"})
public ModelAndView index() {
Map<String, Object> data = new HashMap<>(2);
data.put("name", "YiHui Freemarker");
data.put("now", LocalDateTime.now().toString());
return new ModelAndView("index", data);
}
/**
* 一般不建议直接使用jdk的String.split来分割字符串,内部实现是根据正则来处理的,虽然更强大,但在简单的场景下,性能开销更大
*/
private static String[] contents =
("绿蚁浮觞香泛泛,黄花共荐芳辰。\n清霜天宇净无尘。\n登高宜有赋,拈笔戏成文。\n可奈园林摇落尽,悲秋意与谁论。\n眼中相识几番新。\n龙山高会处,落帽定何人。").split("\n");
private static Random random = new Random();
@GetMapping(path = "show1")
public String showOne(Model model) {
model.addAttribute("title", "临江仙");
model.addAttribute("content", contents[random.nextInt(6)]);
return "show1";
}
@GetMapping(path = "show2")
public String showTow(Map<String, Object> data) {
data.put("name", "Show2---->");
data.put("now", LocalDateTime.now().toString());
return "show2";
}
}
上面的三种case中
- 第一个是最好理解的,在创建
ModelAndView
时,传入viewName和数据 - 第二个是通过接口参数Model,设置传递给view的数据
- 第三种则直接使用Map来传递数据
三个接口,对应的三个html文件,如下
index.ftl
<!DOCTYPE html>
<html lang="ch">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="SpringBoot FreeMaker"/>
<meta name="author" content="YiHui"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>YiHui's SpringBoot Demo</title>
<link rel="stylesheet" href="index.css"/>
</head>
<body>
<div>
<div class="title">hello world!</div>
<br/>
<div class="content">欢迎访问 ${name}</div>
<br/>
<div class="sign">当前时间: ${now}</div>
<br/>
<a href="show1">传参2测试</a>
<a href="show2">传参3测试</a>
</div>
</body>
</html>
show1.ftl
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="SpringBoot thymeleaf"/>
<meta name="author" content="YiHui"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>YiHui's SpringBoot Demo</title>
<link rel="stylesheet" href="index.css"/>
</head>
<body>
<div>
<div class="title">${title}</div>
<div class="content">${content}</div>
</div>
</body>
</html>
show2.ft
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="SpringBoot thymeleaf"/>
<meta name="author" content="YiHui"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>YiHui's SpringBoot Demo</title>
<link rel="stylesheet" href="index.css"/>
</head>
<body>
<div>
<div class="title">${name}</div>
<div class="content">${now}</div>
</div>
</body>
</html>
在上面的模板文件中,需要注意引用css样式文件,路径前面并没有static,我们对应的css文件
index.css
.title {
color: #c00;
font-weight: normal;
font-size: 2em;
}
.content {
color: darkblue;
font-size: 1.2em;
}
.sign {
color: lightgray;
font-size: 0.8em;
font-style: italic;
}
3. 演示
启动项目后,可以看到三个页面的切换,模板中的数据根据后端的返回替换,特别是主页的时间,每次刷新都会随之改变
II. 其他
0. 项目
2 - 2.Thymeleaf环境搭建
上一篇博文介绍了如何使用Freemaker引擎搭建web项目,这一篇我们则看一下另外一个常见的页面渲染引擎Thymeleaf如何搭建一个web项目
推荐结合Freemaker博文一起查看,效果更佳 190816-SpringBoot系列教程web篇之Freemaker环境搭建
I. 准备
Thymeleaf 是现代化服务器端的Java模板引擎,不同与JSP和FreeMarker,Thymeleaf的语法更加接近HTML,关于它的使用说明,可以参考官方文档 https://www.thymeleaf.org/documentation.html
1. 依赖
首先我们是需要一个springboot项目,基本的pom结构大都相似
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.RELEASE</version>
<relativePath/> <!-- lookup parent from update -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-cloud.version>Finchley.RELEASE</spring-cloud.version>
<java.version>1.8</java.version>
</properties>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</pluginManagement>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
在这个项目中,我们主要需要引入两个依赖包,一个web,一个thymeleaf
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
2. 配置参数
通常我们直接使用默认的thymeleaf参数配置即可,下面给出几个常用的配置
spring:
thymeleaf:
mode: HTML
encoding: UTF-8
servlet:
content-type: text/html
cache: false
thymeleaf的参数,主要对应的是org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties
II. 项目搭建演示
1. 项目结构
搭建一个web项目和我们之前的纯后端项目有点不一样,前端资源放在什么地方,依赖文件怎么处理都是有讲究的,下面是一个常规的项目结构
如上图,前端资源文件默认放在resources目录下,下面有两个目录
templates
:存放模板文件,可以理解为我们编写的html,注意这个文件名不能有问题static
: 存放静态资源文件,如js,css,image等
2. Rest服务
我们这里提供了三个接口,主要是为了演示三种不同的数据绑定方式(和Freemaker这篇博文基本一样)
@Controller
public class IndexController {
@GetMapping(path = {"", "/", "/index"})
public ModelAndView index() {
Map<String, Object> data = new HashMap<>(2);
data.put("name", "YiHui Thymeleaf");
data.put("now", LocalDateTime.now().toString());
return new ModelAndView("index", data);
}
/**
* 一般不建议直接使用jdk的String.split来分割字符串,内部实现是根据正则来处理的,虽然更强大,但在简单的场景下,性能开销更大
*/
private static String[] contents =
("绿蚁浮觞香泛泛,黄花共荐芳辰。\n清霜天宇净无尘。\n登高宜有赋,拈笔戏成文。\n可奈园林摇落尽,悲秋意与谁论。\n眼中相识几番新。\n龙山高会处,落帽定何人。").split("\n");
private static Random random = new Random();
@GetMapping(path = "show1")
public String showOne(Model model) {
model.addAttribute("title", "临江仙");
model.addAttribute("content", contents[random.nextInt(6)]);
return "show1";
}
@GetMapping(path = "show2")
public String showTow(Map<String, Object> data) {
data.put("name", "Show2---->");
data.put("now", LocalDateTime.now().toString());
return "show2";
}
}
上面的三种case中
- 第一个是最好理解的,在创建
ModelAndView
时,传入viewName和数据 - 第二个是通过接口参数Model,设置传递给view的数据
- 第三种则直接使用Map来传递数据
三个接口,对应的三个html文件,如下
index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="SpringBoot thymeleaf"/>
<meta name="author" content="YiHui"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>YiHui's SpringBoot Demo</title>
<link rel="stylesheet" href="index.css"/>
</head>
<body>
<div>
<div class="title">hello world!</div>
<br/>
<div class="content" th:text="'欢迎访问' + ${name}">默认的内容</div>
<br/>
<div class="sign" th:text="'当前时间' + ${now}">默认的签名</div>
<br/>
<a href="show1">传参2测试</a>
<a href="show2">传参3测试</a>
</div>
</body>
</html>
show1.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="SpringBoot thymeleaf"/>
<meta name="author" content="YiHui"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>YiHui's SpringBoot Demo</title>
<link rel="stylesheet" href="index.css"/>
</head>
<body>
<div>
<div class="title" th:text="${title}">标题!</div>
<div class="content" th:text="${content}">内容</div>
</div>
</body>
</html>
show2.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="SpringBoot thymeleaf"/>
<meta name="author" content="YiHui"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>YiHui's SpringBoot Demo</title>
<link rel="stylesheet" href="index.css"/>
</head>
<body>
<div>
<div class="title" th:text="${name}">标题!</div>
<div class="content" th:text="${now}">内容</div>
</div>
</body>
</html>
在上面的模板文件中,需要注意引用css样式文件,路径前面并没有static,我们对应的css文件
index.css
.title {
color: #c00;
font-weight: normal;
font-size: 2em;
}
.content {
color: darkblue;
font-size: 1.2em;
}
.sign {
color: lightgray;
font-size: 0.8em;
font-style: italic;
}
3. 演示
启动项目后,可以看到三个页面的切换,模板中的数据根据后端的返回替换,特别是主页的时间,每次刷新都会随之改变
II. 其他
0. 项目
3 - 3.Beetl环境搭建
前面两篇分别介绍了目前流行的模板引擎Freemaker和Thymeleaf构建web应用的方式,接下来我们看一下号称性能最好的国产模板引擎Beetl,如何搭建web环境
本文主要来自官方文档,如有疑问,推荐查看: http://ibeetl.com/guide/#beetl
I. 准备
1. 依赖
首先我们是需要一个springboot项目,基本的pom结构大都相似
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.RELEASE</version>
<relativePath/> <!-- lookup parent from update -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-cloud.version>Finchley.RELEASE</spring-cloud.version>
<java.version>1.8</java.version>
</properties>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</pluginManagement>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
在这个项目中,我们主要需要引入两个依赖包,一个web,一个官方提供的beetl-framework-starter
,当前最新的版本为 1.2.12.RELEASE
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.ibeetl</groupId>
<artifactId>beetl-framework-starter</artifactId>
<version>1.2.12.RELEASE</version>
</dependency>
</dependencies>
2. 配置参数
通常我们直接使用默认的thymeleaf参数配置即可,下面给出几个常用的配置
beetl:
enabled: true
suffix: btl
beetl-beetlsql:
dev: true # 即自动检查模板变化
II. 项目搭建演示
1. 项目结构
搭建一个web项目和我们之前的纯后端项目有点不一样,前端资源放在什么地方,依赖文件怎么处理都是有讲究的,下面是一个常规的项目结构
如上图,前端资源文件默认放在resources目录下,下面有两个目录
templates
:存放模板文件,可以理解为我们编写的html,注意这个文件名不能有问题static
: 存放静态资源文件,如js,css,image等
2. Rest服务
我们这里提供了三个接口,主要是为了演示三种不同的数据绑定方式(和前面两篇博文基本一样)
@Controller
public class IndexController {
@GetMapping(path = {"", "/", "/index"})
public ModelAndView index() {
Map<String, Object> data = new HashMap<>(2);
data.put("name", "YiHui Beetl");
data.put("now", LocalDateTime.now().toString());
return new ModelAndView("index.btl", data);
}
private static String[] contents =
("绿蚁浮觞香泛泛,黄花共荐芳辰。\n清霜天宇净无尘。\n登高宜有赋,拈笔戏成文。\n可奈园林摇落尽,悲秋意与谁论。\n眼中相识几番新。\n龙山高会处,落帽定何人。").split("\n");
private static Random random = new Random();
@GetMapping(path = "show1")
public String showOne(Model model) {
model.addAttribute("title", "临江仙");
model.addAttribute("content", contents[random.nextInt(6)]);
return "show1.btl";
}
@GetMapping(path = "show2")
public String showTow(Map<String, Object> data) {
data.put("name", "Show2---->");
data.put("now", LocalDateTime.now().toString());
return "show2.btl";
}
}
上面的三种case中
- 第一个是最好理解的,在创建
ModelAndView
时,传入viewName和数据 - 第二个是通过接口参数Model,设置传递给view的数据
- 第三种则直接使用Map来传递数据
注意
如果和前面两篇博文进行对比,会发现一个显著的区别,之前的Freemaker
, Thymeleaf
指定视图名的时候,都不需要后缀,但是这里,必须带上后缀,否则会500错误
三个接口,对应的三个btl文件,如下
index.btl
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="SpringBoot Beetl"/>
<meta name="author" content="YiHui"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>YiHui's SpringBoot Beetl Demo</title>
<link rel="stylesheet" href="index.css"/>
</head>
<body>
<div>
<div class="title">hello world!</div>
<br/>
<div class="content">欢迎访问 ${name}</div>
<br/>
<div class="sign">当前时间 ${now}</div>
<br/>
<a href="show1">传参2测试</a>
<a href="show2">传参3测试</a>
</div>
</body>
</html>
show1.btl
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="SpringBoot Beetl"/>
<meta name="author" content="YiHui"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>YiHui's SpringBoot Beetl Demo</title>
<link rel="stylesheet" href="index.css"/>
</head>
<body>
<div>
<div class="title">${title}</div>
<div class="content">${content}</div>
</div>
</body>
</html>
show2.btl
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="SpringBoot Beetl"/>
<meta name="author" content="YiHui"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>YiHui's SpringBoot Beetl Demo</title>
<link rel="stylesheet" href="index.css"/>
</head>
<body>
<div>
<div class="title">${name}</div>
<div class="content">${now}</div>
</div>
</body>
</html>
在上面的模板文件中,需要注意引用css样式文件,路径前面并没有static,我们对应的css文件
index.css
.title {
color: #c00;
font-weight: normal;
font-size: 2em;
}
.content {
color: darkblue;
font-size: 1.2em;
}
.sign {
color: lightgray;
font-size: 0.8em;
font-style: italic;
}
3. 演示
启动项目后,可以看到三个页面的切换,模板中的数据根据后端的返回替换,特别是主页的时间,每次刷新都会随之改变
II. 其他
0. 项目
4 - 4.返回文本、网页、图片的操作姿势
前面几篇博文介绍了如何获取get/post传参,既然是http请求,一般也都是有来有往,有请求参数传递,就会有数据返回。那么我们通过springboot搭建的web应用,可以怎样返回数据呢?
本篇将主要介绍以下几种数据格式的返回实例
- 返回文本
- 返回数组
- 返回json串
- 返回静态网页
- 返回图片
I. 环境搭建
首先得搭建一个web应用才有可能继续后续的测试,借助SpringBoot搭建一个web应用属于比较简单的活;
创建一个maven项目,pom文件如下
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7</version>
<relativePath/> <!-- lookup parent from update -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-cloud.version>Finchley.RELEASE</spring-cloud.version>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</pluginManagement>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
依然是一般的流程,pom依赖搞定之后,写一个程序入口
/**
* Created by @author yihui in 15:26 19/9/13.
*/
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}
II. 数据返回姿势实例
以下返回实例都放在同一个Controller中,具体定义如下
@Controller
@RequestMapping(path = "data")
public class DataRespRest {
}
1. 文本返回
这个属于基础功能了,发起请求,返回一串文本,在SpringMVC的体系中,要实现这种通常的写法通常是直接定义方法的返回为String;当然还有另外一种非常基础的写法,直接将返回的数据通过HttpServletResponse
写入到输出流中
下面给出这两种写法的实例
@ResponseBody
@GetMapping(path = "str")
public String strRsp() {
return "hello " + UUID.randomUUID().toString();
}
@ResponseBody
@GetMapping(path = "str2")
public void strRsp2(HttpServletResponse response) throws IOException {
Map<String, String> ans = new HashMap<>(2);
ans.put("a", "b");
ans.put("b", "c");
response.getOutputStream().write(JSON.toJSONString(ans).getBytes());
response.getOutputStream().flush();
}
注意上面的实现中,方法上面多了一个注解@ResponseBody
,这个表示返回数据,而不是视图(后面会详细说明)
strRsp2
的输出借助了FastJson来实现将map序列化为json串,然后写入输出流
实例访问如下
从上面的输出也可以看出,第一种返回方式,ResponseHeaders
的Content-Type: text/html;charset=UTF-8
;而第二种方式则没有这个响应头,需要我们自己主动设置(这里注意一下即可,在后面的返回图片中有实例)
2,返回数组
前面请求参数的博文中,我们看到请求参数允许传入数组,那么我们返回可以直接返回数组么?讲道理的话,应该没啥问题
/**
* 返回数组
*
* @return
*/
@ResponseBody
@GetMapping(path = "ary")
public String[] aryRsp() {
return new String[]{UUID.randomUUID().toString(), LocalDateTime.now().toString()};
}
然后请求输出为
注意下响应头,为application/json
, 也就是说SpringMVC将数组当成json串进行返回了
3. Bean返回
在我们实际的业务开发中,这种应该属于非常常见的使用姿势了,直接返回一个POJO,调用者接收的是一个json串,可以很容易的反序列化为需要的对象
/**
* 返回POJO
*
* @return
*/
@ResponseBody
@GetMapping(path = "bean")
public DemoRsp beanRsp() {
return new DemoRsp(200, "success", UUID.randomUUID().toString() + "--->data");
}
4. 网页返回
前面都是直接返回数据,但是我们平常在使用浏览器,更多的是发起一个请求,然后返回一个网页啊,难道说springmvc不能直接返回网页么?
当然返回网页怎么可能会不支持,(题外话:个人感觉在前后端分离逐渐流行之后,直接由后端返回网页的case不太多了,前端和后端作为独立的项目部署,两者之间通过json串进行交流;这里扯远了),我们下面看一下SpringMVC中如何返回网页
我们可以从上面直接返回字符串的case中,得到一个思路,如果我直接返回一个html文本,会怎样?既然返回content-type
是text/html
,那浏览器应该可以解析为网页的,下面实测一下
@ResponseBody
@GetMapping(path = "html")
public String strHtmlRsp() {
return "<html>\n" + "<head>\n" + " <title>返回数据测试</title>\n" + "</head>\n" + "<body>\n" +
"<h1>欢迎欢迎,热烈欢迎</h1>\n" + "</body>\n" + "</html>";
}
测试如下
浏览器发起请求之后,将我们返回的html文本当做网页正常渲染了,所以我们如果想返回网页,就这么干,没毛病!
上面这种方式虽然说可以返回网页,然而在实际业务中,如果真要我们这么干,想想也是可怕,还干什么后端,分分钟全栈得了!!!
下面看一下更常规的写法,首先我们需要配置下返回视图的前缀、后缀, 在application.yml
配置文件中添加如下配置
spring:
mvc:
view:
prefix: /
suffix: .html
然后我们的静态网页,放在资源文件的static目录下,下面是我们实际的项目截图,index.html为我们需要返回的静态网页
接下来就是我们的服务接口
/**
* 返回视图
*
* @return
*/
@GetMapping(path = "view")
public String viewRsp() {
return "index";
}
注意下上面的接口,没有@ResponseBody
注解,表示这个接口返回的是一个视图,会从static目录下寻找名为index.html
(前缀路径和后缀是上面的application.yml中定义)的网页返回
实测case如下
5. 图片返回
图片返回与前面的又不太一样了,上面介绍的几种case中,要么是返回文本,要么返回视图,而返回图片呢,更多的是返回图片的字符数组,然后告诉浏览器这是个图片,老哥你按照图片渲染
直接返回二进制流,上面在介绍文本返回的两种方式中,有个直接通过HttpServletResponse
向输出流中写数据的方式,我们这里是不是可以直接这么用呢?
下面给出一个从网络下载图片并返回二进制流的实际case
/**
* 返回图片
*/
@GetMapping(path = "img")
public void imgRsp(HttpServletResponse response) throws IOException {
response.setContentType("image/png");
ServletOutputStream outStream = response.getOutputStream();
String path = "https://spring.hhui.top/spring-blog/imgs/info/info.png";
URL uri = new URL(path);
BufferedImage img = ImageIO.read(uri);
ImageIO.write(img, "png", response.getOutputStream());
System.out.println("--------");
}
注意下上面的实例case,首先设置了返回的ContentType
,然后借助ImateIO
来下载图片(个人不太建议这种写法,很容易出现403;这里演示主要是为了简单…),并将图片写入到输出流
实例演示如下
III 小结
1. 返回数据小结
本篇博文主要介绍了几种常见数据格式的返回使用姿势,本文更多的是一种使用方式的实例case演示,并没有涉及到底层的支持原理,也没有过多的提及如何设置响应头,web交互中常见的cookies/session也没有说到,这些将作为下篇的内容引入,恳请关注
下面做一个简单的小结
返回纯数据
- 添加
@ResponseBody
注解,则表示我们返回的是数据,而不需要进行视图解析渲染;- 如果一个controller中全部都是返回数据,不会返回视图时,我们可以在添加
@RestController
注解,然后这个类中的接口都不需要添加@ResponseBody
注解了
- 如果一个controller中全部都是返回数据,不会返回视图时,我们可以在添加
- 返回视图时,我们会根据接口返回的字符串,结合定义的前缀,后缀,到资源路径的static目录下寻找对应的静态文件返回
- 可以直接通过向
HttpServletResponse
的输出流中写数据的方式来返回数据,如返回图片常用这种case
2. 更多web系列博文
- 190905-SpringBoot系列教程web篇之中文乱码问题解决
- 190831-SpringBoot系列教程web篇之如何自定义参数解析器
- 190828-SpringBoot系列教程web篇之Post请求参数解析姿势汇总
- 190824-SpringBoot系列教程web篇之Get请求参数解析姿势汇总
- 190822-SpringBoot系列教程web篇之Beetl环境搭建
- 190820-SpringBoot系列教程web篇之Thymeleaf环境搭建
- 190816-SpringBoot系列教程web篇之Freemaker环境搭建
IV. 其他
0. 项目
5 - 5.请求重定向
前面介绍了spring web篇数据返回的几种常用姿势,当我们在相应一个http请求时,除了直接返回数据之外,还有另一种常见的case -> 重定向;
比如我们在逛淘宝,没有登录就点击购买时,会跳转到登录界面,这其实就是一个重定向。本文主要介绍对于后端而言,可以怎样支持302重定向
I. 环境搭建
首先得搭建一个web应用才有可能继续后续的测试,借助SpringBoot搭建一个web应用属于比较简单的活;
创建一个maven项目,pom文件如下
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7</version>
<relativePath/> <!-- lookup parent from update -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-cloud.version>Finchley.RELEASE</spring-cloud.version>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.45</version>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</pluginManagement>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
依然是一般的流程,pom依赖搞定之后,写一个程序入口
/**
* Created by @author yihui in 15:26 19/9/13.
*/
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}
II. 302重定向
1. 返回redirect
这种case通常适用于返回视图的接口,在返回的字符串前面添加redirect:
方式来告诉Spring框架,需要做302重定向处理
@Controller
@RequestMapping(path = "redirect")
public class RedirectRest {
@ResponseBody
@GetMapping(path = "index")
public String index(HttpServletRequest request) {
return "重定向访问! " + JSON.toJSONString(request.getParameterMap());
}
@GetMapping(path = "r1")
public String r1() {
return "redirect:/redirect/index?base=r1";
}
}
上面给出了一个简单的demo,当我们访问/redirect/r1
时,会重定向到请求/redirect/index?base=r1
,实际测试结果如下
注意上面的截图,我们实际访问的连接是 http://127.0.0.1:8080/redirect/index?base=r1
,在浏览器中的表现则是请求url变成了http://127.0.0.1:8080/redirect/index?base=r1
;通过控制台查看到的返回头状态码是302
说明
- 使用这种方式的前提是不能在接口上添加
@ResponseBody
注解,否则返回的字符串被当成普通字符串处理直接返回,并不会实现重定向
2. HttpServletResponse重定向
前面一篇说到SpringMVC返回数据的时候,介绍到可以直接通过HttpServletResponse
往输出流中写数据的方式,来返回结果;我们这里也是利用它,来实现重定向
@ResponseBody
@GetMapping(path = "r2")
public void r2(HttpServletResponse response) throws IOException {
response.sendRedirect("/redirect/index?base=r2");
}
从上面的demo中,也可以看出这个的使用方式很简单了,直接调用javax.servlet.http.HttpServletResponse#sendRedirect
,并传入需要重定向的url即可
3. 小结
这里主要介绍了两种常见的后端重定向方式,都比较简单,这两种方式也有自己的适用场景(当然并不绝对)
- 在返回视图的前面加上
redirect
的方式,更加适用于视图的跳转,从一个网页跳转到另一个网页 HttpServletResponse#sendRedirec
的方式更加灵活,可以在后端接收一次http请求生命周期中的任何一个阶段来使用,比如有以下几种常见的场景- 某个接口要求登录时,在拦截器层针对所有未登录的请求,重定向到登录页面
- 全局异常处理中,如果出现服务器异常,重定向到定制的500页面
- 不支持的请求,重定向到404页面
II. 其他
0. 项目
a. 系列博文
- 190913-SpringBoot系列教程web篇之返回文本、网页、图片的操作姿势
- 190905-SpringBoot系列教程web篇之中文乱码问题解决
- 190831-SpringBoot系列教程web篇之如何自定义参数解析器
- 190828-SpringBoot系列教程web篇之Post请求参数解析姿势汇总
- 190824-SpringBoot系列教程web篇之Get请求参数解析姿势汇总
- 190822-SpringBoot系列教程web篇之Beetl环境搭建
- 190820-SpringBoot系列教程web篇之Thymeleaf环境搭建
- 190816-SpringBoot系列教程web篇之Freemaker环境搭建
b. 项目源码
6 - 6.404、500异常页面配置
接着前面几篇web处理请求的博文,本文将说明,当出现异常的场景下,如404请求url不存在,,403无权,500服务器异常时,我们可以如何处理
I. 环境搭建
首先得搭建一个web应用才有可能继续后续的测试,借助SpringBoot搭建一个web应用属于比较简单的活;
创建一个maven项目,pom文件如下
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7</version>
<relativePath/> <!-- lookup parent from update -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-cloud.version>Finchley.RELEASE</spring-cloud.version>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.45</version>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</pluginManagement>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
依然是一般的流程,pom依赖搞定之后,写一个程序入口
/**
* Created by @author yihui in 15:26 19/9/13.
*/
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}
II. 异常页面配置
在SpringBoot项目中,本身提供了一个默认的异常处理页面,当我们希望使用自定义的404,500等页面时,可以如何处理呢?
1. 默认异常页面配置
在默认的情况下,要配置异常页面非常简单,在资源路径下面,新建 error
目录,在下面添加400.html
, 500html
页面即可
项目结构如上,注意这里的实例demo是没有使用模板引擎的,所以我们的异常页面放在static目录下;如果使用了如FreeMaker模板引擎时,可以将错误模板页面放在template目录下
接下来实际测试下是否生效, 我们先定义一个可能出现服务器500的服务
@Controller
@RequestMapping(path = "page")
public class ErrorPageRest {
@ResponseBody
@GetMapping(path = "divide")
public int divide(int sub) {
System.out.println("divide1");
return 1000 / sub;
}
}
请求一个不存在的url,返回我们定义的400.html
页面
<html>
<head>
<title>404页面</title>
</head>
<body>
<h3>页面不存在</h3>
</body>
</html>
请求一个服务器500异常,返回我们定义的500.html
页面
<html>
<head>
<title>500页面</title>
</head>
<body>
<h2 style="color: red;">服务器出现异常!!!</h2>
</body>
</html>
2. BasicErrorController
看上面的使用比较简单,自然会有个疑问,这个异常页面是怎么返回的呢?
从项目启动的日志中,注意一下RequestMappingHandlerMapping
可以发现里面有个/error
的路径不是我们自己定义的,从命名上来看,这个多半就是专门用来处理异常的Controller -> BasicErrorController
, 部分代码如下
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
@Override
public String getErrorPath() {
return this.errorProperties.getPath();
}
@RequestMapping(produces = "text/html")
public ModelAndView errorHtml(HttpServletRequest request,
HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
@RequestMapping
@ResponseBody
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.ALL));
HttpStatus status = getStatus(request);
return new ResponseEntity<>(body, status);
}
}
这个Controller中,一个返回网页的接口,一个返回Json串的接口;我们前面使用的应该是第一个,那我们什么场景下会使用到第二个呢?
- 通过制定请求头的
Accept
,来限定我们只希望获取json的返回即可
3. 小结
本篇内容比较简单,归纳为两句话如下
- 将自定义的异常页面根据http状态码命名,放在
/error
目录下 - 在异常状况下,根据返回的http状态码找到对应的异常页面返回
II. 其他
0. 项目
a. 系列博文
- 190929-SpringBoot系列教程web篇之重定向
- 190913-SpringBoot系列教程web篇之返回文本、网页、图片的操作姿势
- 190905-SpringBoot系列教程web篇之中文乱码问题解决
- 190831-SpringBoot系列教程web篇之如何自定义参数解析器
- 190828-SpringBoot系列教程web篇之Post请求参数解析姿势汇总
- 190824-SpringBoot系列教程web篇之Get请求参数解析姿势汇总
- 190822-SpringBoot系列教程web篇之Beetl环境搭建
- 190820-SpringBoot系列教程web篇之Thymeleaf环境搭建
- 190816-SpringBoot系列教程web篇之Freemaker环境搭建
b. 项目源码
7 - 7.全局异常处理
当我们的后端应用出现异常时,通常会将异常状况包装之后再返回给调用方或者前端,在实际的项目中,不可能对每一个地方都做好异常处理,再优雅的代码也可能抛出异常,那么在Spring项目中,可以怎样优雅的处理这些异常呢?
本文将介绍一种全局异常处理方式,主要包括以下知识点
- @ControllerAdvice Controller增强
- @ExceptionHandler 异常捕获
- @ResponseStatus 返回状态码
- NoHandlerFoundException处理(404异常捕获)
I. 环境搭建
首先得搭建一个web应用才有可能继续后续的测试,借助SpringBoot搭建一个web应用属于比较简单的活;
创建一个maven项目,pom文件如下
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7</version>
<relativePath/> <!-- lookup parent from update -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-cloud.version>Finchley.RELEASE</spring-cloud.version>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.45</version>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</pluginManagement>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
依然是一般的流程,pom依赖搞定之后,写一个程序入口
/**
* Created by @author yihui in 15:26 19/9/13.
*/
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}
II. 异常处理
1. @ControllerAdvice
我们通常利用@ControllerAdvice
配合注解@ExceptionHandler
来实现全局异常捕获处理
@ControllerAdvice
为所有的Controller织入增强方法@ExceptionHandler
标记在方法上,表示当出现对应的异常抛出到上层时(即没有被业务捕获),这个方法会被触发
下面我们通过实例进行功能演示
a. 异常捕获
我们定义两个异常捕获的case,一个是除0,一个是数组越界异常
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
public static String getThrowableStackInfo(Throwable e) {
ByteArrayOutputStream buf = new ByteArrayOutputStream();
e.printStackTrace(new java.io.PrintWriter(buf, true));
String msg = buf.toString();
try {
buf.close();
} catch (Exception t) {
return e.getMessage();
}
return msg;
}
@ResponseBody
@ExceptionHandler(value = ArithmeticException.class)
public String handleArithmetic(HttpServletRequest request, HttpServletResponse response, ArithmeticException e)
throws IOException {
log.info("divide error!");
return "divide 0: " + getThrowableStackInfo(e);
}
@ResponseBody
@ExceptionHandler(value = ArrayIndexOutOfBoundsException.class)
public String handleArrayIndexOutBounds(HttpServletRequest request, HttpServletResponse response,
ArrayIndexOutOfBoundsException e) throws IOException {
log.info("array index out error!");
return "aryIndexOutOfBounds: " + getThrowableStackInfo(e);
}
}
在上面的测试中,我们将异常堆栈返回调用方
b. 示例服务
增加几个测试方法
@Controller
@RequestMapping(path = "page")
public class ErrorPageRest {
@ResponseBody
@GetMapping(path = "divide")
public int divide(int sub) {
return 1000 / sub;
}
private int[] ans = new int[]{1, 2, 3, 4};
@ResponseBody
@GetMapping(path = "ary")
public int ary(int index) {
return ans[index];
}
}
c. 测试说明
实例测试如下,上面我们声明捕获的两种异常被拦截并输出对应的堆栈信息;
但是需要注意
- 404和未捕获的500异常则显示的SpringBoot默认的错误页面;
- 此外我们捕获返回的http状态码是200
2. @ResponseStatus
上面的case中捕获的异常返回的状态码是200,但是在某些case中,可能更希望返回更合适的http状态码,此时可以使用ResponseStatus
来指定
使用方式比较简单,加一个注解即可
@ResponseBody
@ExceptionHandler(value = ArithmeticException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public String handleArithmetic(HttpServletRequest request, HttpServletResponse response, ArithmeticException e)
throws IOException {
log.info("divide error!");
return "divide 0: " + getThrowableStackInfo(e);
}
3. 404处理
通过@ControllerAdvice
配合@ExceptionHandler
可以拦截500异常,如果我希望404异常也可以拦截,可以如何处理?
首先修改配置文件application.properties
,将NoHandlerFoundException
抛出来
# 出现错误时, 直接抛出异常
spring.mvc.throw-exception-if-no-handler-found=true
# 设置静态资源映射访问路径,下面两个二选一,
spring.mvc.static-path-pattern=/statics/**
# spring.resources.add-mappings=false
其次是定义异常捕获
@ResponseBody
@ExceptionHandler(value = NoHandlerFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public String handleNoHandlerError(NoHandlerFoundException e, HttpServletResponse response) {
return "noHandlerFound: " + getThrowableStackInfo(e);
}
再次测试如下,404被我们捕获并返回堆栈信息
II. 其他
0. 项目
web系列博文
- 190930-SpringBoot系列教程web篇之404、500异常页面配置
- 190929-SpringBoot系列教程web篇之重定向
- 190913-SpringBoot系列教程web篇之返回文本、网页、图片的操作姿势
- 190905-SpringBoot系列教程web篇之中文乱码问题解决
- 190831-SpringBoot系列教程web篇之如何自定义参数解析器
- 190828-SpringBoot系列教程web篇之Post请求参数解析姿势汇总
- 190824-SpringBoot系列教程web篇之Get请求参数解析姿势汇总
- 190822-SpringBoot系列教程web篇之Beetl环境搭建
- 190820-SpringBoot系列教程web篇之Thymeleaf环境搭建
- 190816-SpringBoot系列教程web篇之Freemaker环境搭建
- 190421-SpringBoot高级篇WEB之websocket的使用说明
- 190327-Spring-RestTemplate之urlencode参数解析异常全程分析
- 190317-Spring MVC之基于java config无xml配置的web应用构建
- 190316-Spring MVC之基于xml配置的web应用构建
- 190213-SpringBoot文件上传异常之提示The temporary upload location xxx is not valid
项目源码
8 - 8.自定义异常处理HandlerExceptionResolver
关于Web应用的全局异常处理,上一篇介绍了ControllerAdvice
结合@ExceptionHandler
的方式来实现web应用的全局异常管理;
本篇博文则带来另外一种并不常见的使用方式,通过实现自定义的HandlerExceptionResolver
,来处理异常状态
上篇博文链接: SpringBoot系列教程web篇之全局异常处理
I. 环境搭建
首先得搭建一个web应用才有可能继续后续的测试,借助SpringBoot搭建一个web应用属于比较简单的活;
创建一个maven项目,pom文件如下
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7</version>
<relativePath/> <!-- lookup parent from update -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-cloud.version>Finchley.RELEASE</spring-cloud.version>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.45</version>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</pluginManagement>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
II. HandlerExceptionResolver
1. 自定义异常处理
HandlerExceptionResolver
顾名思义,就是处理异常的类,接口就一个方法,出现异常之后的回调,四个参数中还携带了异常堆栈信息
@Nullable
ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);
我们自定义异常处理类就比较简单了,实现上面的接口,然后将完整的堆栈返回给调用方
public class SelfExceptionHandler implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) {
String msg = GlobalExceptionHandler.getThrowableStackInfo(ex);
try {
response.addHeader("Content-Type", "text/html; charset=UTF-8");
response.getWriter().append("自定义异常处理!!! \n").append(msg).flush();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
// 堆栈信息打印方法如下
public static String getThrowableStackInfo(Throwable e) {
ByteArrayOutputStream buf = new ByteArrayOutputStream();
e.printStackTrace(new java.io.PrintWriter(buf, true));
String msg = buf.toString();
try {
buf.close();
} catch (Exception t) {
return e.getMessage();
}
return msg;
}
仔细观察上面的代码实现,有下面几个点需要注意
- 为了确保中文不会乱码,我们设置了返回头
response.addHeader("Content-Type", "text/html; charset=UTF-8");
如果没有这一行,会出现中文乱码的情况 - 我们纯后端应用,不想返回视图,直接想Response的输出流中写入数据返回
response.getWriter().append("自定义异常处理!!! \n").append(msg).flush();
; 如果项目中有自定义的错误页面,可以通过返回ModelAndView
来确定最终返回的错误页面 - 上面一个代码并不会直接生效,需要注册,可以在
WebMvcConfigurer
的子类中实现注册,实例如下
@SpringBootApplication
public class Application implements WebMvcConfigurer {
@Override
public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
resolvers.add(0, new SelfExceptionHandler());
}
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}
2. 测试case
我们依然使用上篇博文的用例来测试
@Controller
@RequestMapping(path = "page")
public class ErrorPageRest {
@ResponseBody
@GetMapping(path = "divide")
public int divide(int sub) {
return 1000 / sub;
}
}
下面分别是404异常和500异常的实测情况
500异常会进入我们的自定义异常处理类, 而404依然走的是默认的错误页面,所以如果我们需要捕获404异常,依然需要在配置文件中添加
# 出现错误时, 直接抛出异常
spring.mvc.throw-exception-if-no-handler-found=true
# 设置静态资源映射访问路径
spring.mvc.static-path-pattern=/statics/**
# spring.resources.add-mappings=false
为什么404需要额外处理?
下面尽量以通俗易懂的方式说明下这个问题
- java web应用,除了返回json类数据之外还可能返回网页,js,css
- 我们通过
@ResponseBody
来表明一个url返回的是json数据(通常情况下是这样的,不考虑自定义实现) - 我们的
@Controller
中通过@RequestMapping
定义的REST服务,返回的是静态资源 - 那么js,css,图片这些文件呢,在我们的web应用中并不会定义一个REST服务
- 所以当接收一个http请求,找不到url关联映射时,默认场景下不认为这是一个
NoHandlerFoundException
,不抛异常,而是到静态资源中去找了(静态资源中也没有,为啥不抛NoHandlerFoundException呢?这个异常表示这个url请求没有对应的处理器,但是我们这里呢,给它分配到了静态资源处理器了ResourceHttpRequestHandler
)
针对上面这点,如果有兴趣深挖的同学,这里给出关键代码位置
// 进入方法: `org.springframework.web.servlet.DispatcherServlet#doDispatch`
// debug 节点
Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// 核心逻辑
// org.springframework.web.servlet.DispatcherServlet#getHandler
@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping hm : this.handlerMappings) {
if (logger.isTraceEnabled()) {
logger.trace(
"Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName() + "'");
}
HandlerExecutionChain handler = hm.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
3. 小结
本篇博文虽然也介绍了一种新的全局异常处理方式,实现效果和ControllerAdvice
也差不多,但是并不推荐用这种方法, 原因如下
HandlerExceptionResolver
的方式没有ControllerAdvice
方式简介优雅- 官方提供的
DefaultHandlerExceptionResolver
已经非常强大了,基本上覆盖了http的各种状态码,我们自己再去定制的必要性不大
II. 其他
web系列博文
- 191010-SpringBoot系列教程web篇之全局异常处理
- 190930-SpringBoot系列教程web篇之404、500异常页面配置
- 190929-SpringBoot系列教程web篇之重定向
- 190913-SpringBoot系列教程web篇之返回文本、网页、图片的操作姿势
- 190905-SpringBoot系列教程web篇之中文乱码问题解决
- 190831-SpringBoot系列教程web篇之如何自定义参数解析器
- 190828-SpringBoot系列教程web篇之Post请求参数解析姿势汇总
- 190824-SpringBoot系列教程web篇之Get请求参数解析姿势汇总
- 190822-SpringBoot系列教程web篇之Beetl环境搭建
- 190820-SpringBoot系列教程web篇之Thymeleaf环境搭建
- 190816-SpringBoot系列教程web篇之Freemaker环境搭建
- 190421-SpringBoot高级篇WEB之websocket的使用说明
- 190327-Spring-RestTemplate之urlencode参数解析异常全程分析
- 190317-Spring MVC之基于java config无xml配置的web应用构建
- 190316-Spring MVC之基于xml配置的web应用构建
- 190213-SpringBoot文件上传异常之提示The temporary upload location xxx is not valid
项目源码
9 - 9.开启GZIP数据压缩
本篇可以归纳在性能调优篇,虽然内容非常简单,但效果可能出乎预料的好;
分享一个真实案例,我们的服务部署在海外,国内访问时访问服务时,响应有点夸张;某些返回数据比较大的接口,耗时在600ms+上,然而我们的服务rt却是在20ms以下,绝大部分的开销都花在了网络传输上
针对这样的场景,除了买云服务商的网络通道之外,另外一个直观的想法就是减少数据包的大小,直接在nginx层配置gzip压缩是一个方案,本文主要介绍下,SpringBoot如何开启gzip压缩
I. gizp压缩配置
1. 配置
SpringBoot默认是不开启gzip压缩的,需要我们手动开启,在配置文件中添加两行
server:
compression:
enabled: true
mime-types: application/json,application/xml,text/html,text/plain,text/css,application/x-javascript
注意下上面配置中的mime-types
,在spring2.0+的版本中,默认值如下,所以一般我们不需要特意添加这个配置
// org.springframework.boot.web.server.Compression#mimeTypes
/**
* Comma-separated list of MIME types that should be compressed.
*/
private String[] mimeTypes = new String[] { "text/html", "text/xml", "text/plain",
"text/css", "text/javascript", "application/javascript", "application/json",
"application/xml" };
2. 测试
写一个测试的demo
@RestController
public class HelloRest {
@GetMapping("bigReq")
public String bigReqList() {
List<String> result = new ArrayList<>(2048);
for (int i = 0; i < 2048; i++) {
result.add(UUID.randomUUID().toString());
}
return JSON.toJSONString(result);
}
}
下面是开启压缩前后的数据报对比
3. 说明
虽然加上了上面的配置,开启了gzip压缩,但是需要注意并不是说所有的接口都会使用gzip压缩,默认情况下,仅会压缩2048字节以上的内容
如果我们需要修改这个值,通过修改配置即可
server:
compression:
min-response-size: 1024
II. 其他
0. 项目
web系列博文
- 191018-SpringBoot系列教程web篇之过滤器Filter使用指南扩展篇
- 191016-SpringBoot系列教程web篇之过滤器Filter使用指南
- 191012-SpringBoot系列教程web篇之自定义异常处理HandlerExceptionResolver
- 191010-SpringBoot系列教程web篇之全局异常处理
- 190930-SpringBoot系列教程web篇之404、500异常页面配置
- 190929-SpringBoot系列教程web篇之重定向
- 190913-SpringBoot系列教程web篇之返回文本、网页、图片的操作姿势
- 190905-SpringBoot系列教程web篇之中文乱码问题解决
- 190831-SpringBoot系列教程web篇之如何自定义参数解析器
- 190828-SpringBoot系列教程web篇之Post请求参数解析姿势汇总
- 190824-SpringBoot系列教程web篇之Get请求参数解析姿势汇总
- 190822-SpringBoot系列教程web篇之Beetl环境搭建
- 190820-SpringBoot系列教程web篇之Thymeleaf环境搭建
- 190816-SpringBoot系列教程web篇之Freemaker环境搭建
- 190421-SpringBoot高级篇WEB之websocket的使用说明
- 190327-Spring-RestTemplate之urlencode参数解析异常全程分析
- 190317-Spring MVC之基于java config无xml配置的web应用构建
- 190316-Spring MVC之基于xml配置的web应用构建
- 190213-SpringBoot文件上传异常之提示The temporary upload location xxx is not valid
项目源码
10 - 10.RestTemplate 4xx/5xx 异常信息捕获
近期使用RestTemplate访问外部资源时,发现一个有意思的问题。因为权限校验失败,对方返回的401的http code,此外返回数据中也会包含一些异常提示信息;然而在使用RestTemplate访问时,却是直接抛了如下提示401的异常,并不能拿到提示信息
那么RestTemplate如果希望可以获取到非200状态码返回数据时,可以怎么操作呢?
I. 异常捕获
1. 问题分析
RestTemplate的异常处理,是借助org.springframework.web.client.ResponseErrorHandler
来做的,先看一下两个核心方法
- 下面代码来自 spring-web.5.0.7.RELEASE版本
public interface ResponseErrorHandler {
// 判断是否有异常
boolean hasError(ClientHttpResponse response) throws IOException;
// 如果有问题,进入这个方法,处理问题
void handleError(ClientHttpResponse response) throws IOException;
}
简单来讲,当RestTemplate发出请求,获取到对方相应之后,会交给ResponseErrorHandler
来判断一下,返回结果是否ok
因此接下来将目标瞄准到RestTemplate默认的异常处理器: org.springframework.web.client.DefaultResponseErrorHandler
a. 判定返回结果是否ok
从源码上看,主要是根据返回的http code来判断是否ok
// 根据返回的http code判断有没有问题
@Override
public boolean hasError(ClientHttpResponse response) throws IOException {
HttpStatus statusCode = HttpStatus.resolve(response.getRawStatusCode());
return (statusCode != null && hasError(statusCode));
}
// 具体的判定逻辑,简单来讲,就是返回的http code是标准的4xx, 5xx,那么就认为有问题了
protected boolean hasError(HttpStatus statusCode) {
return (statusCode.series() == HttpStatus.Series.CLIENT_ERROR ||
statusCode.series() == HttpStatus.Series.SERVER_ERROR);
}
请注意上面的实现,自定义的某些http code是不会被认为是异常的,因为无法转换为对应的HttpStatus
(后面实例进行说明)
b. 异常处理
当上面的 hasError
返回ture的时候,就会进入异常处理逻辑
@Override
public void handleError(ClientHttpResponse response) throws IOException {
HttpStatus statusCode = HttpStatus.resolve(response.getRawStatusCode());
if (statusCode == null) {
throw new UnknownHttpStatusCodeException(response.getRawStatusCode(), response.getStatusText(),
response.getHeaders(), getResponseBody(response), getCharset(response));
}
handleError(response, statusCode);
}
protected void handleError(ClientHttpResponse response, HttpStatus statusCode) throws IOException {
switch (statusCode.series()) {
case CLIENT_ERROR:
throw new HttpClientErrorException(statusCode, response.getStatusText(),
response.getHeaders(), getResponseBody(response), getCharset(response));
case SERVER_ERROR:
throw new HttpServerErrorException(statusCode, response.getStatusText(),
response.getHeaders(), getResponseBody(response), getCharset(response));
default:
throw new UnknownHttpStatusCodeException(statusCode.value(), response.getStatusText(),
response.getHeaders(), getResponseBody(response), getCharset(response));
}
}
从上面也可以看到,异常处理逻辑很简单,直接抛异常
2. 异常捕获
定位到上面的问题之后,再想解决问题就相对简单了,自定义一个异常处理类,不管状态码返回是啥,全都认为正常即可
RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(new DefaultResponseErrorHandler(){
@Override
protected boolean hasError(HttpStatus statusCode) {
return super.hasError(statusCode);
}
@Override
public void handleError(ClientHttpResponse response) throws IOException {
}
});
3. 实测
首先写两个结果,返回的http状态码非200;针对返回非200状态码的case,有多种写法,下面演示两种常见的
@RestController
public class HelloRest {
@GetMapping("401")
public ResponseEntity<String> _401(HttpServletResponse response) {
ResponseEntity<String> ans =
new ResponseEntity<>("{\"code\": 401, \"msg\": \"some error!\"}", HttpStatus.UNAUTHORIZED);
return ans;
}
@GetMapping("525")
public String _525(HttpServletResponse response) {
response.setStatus(525);
return "{\"code\": 525, \"msg\": \"自定义错误码!\"}";
}
}
首先来看一下自定义的525和标准的401 http code,直接通过RestTemplate
访问的case
@Test
public void testCode() {
RestTemplate restTemplate = new RestTemplate();
HttpEntity<String> ans = restTemplate.getForEntity("http://127.0.0.1:8080/525", String.class);
System.out.println(ans);
ans = restTemplate.getForEntity("http://127.0.0.1:8080/401", String.class);
System.out.println(ans);
}
从上面的输出结果也可以看出来,非标准http code不会抛异常(原因上面有分析),接下来看一下即便是标准的http code也不希望抛异常的case
@Test
public void testSend() {
String url = "http://127.0.0.1:8080/401";
RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(new DefaultResponseErrorHandler(){
@Override
protected boolean hasError(HttpStatus statusCode) {
return super.hasError(statusCode);
}
@Override
public void handleError(ClientHttpResponse response) throws IOException {
}
});
HttpEntity<String> ans = restTemplate.getForEntity(url, String.class);
System.out.println(ans);
}
II. 其他
0. 项目
11 - 11.自定义返回Http Code的n种姿势
虽然http的提供了一整套完整、定义明确的状态码,但实际的业务支持中,后端并不总会遵守这套规则,更多的是在返回结果中,加一个code字段来自定义业务状态,即便是后端5xx了,返回给前端的http code依然是200
那么如果我想遵守http的规范,不同的case返回不同的http code在Spring中可以做呢?
本文将介绍四种设置返回的HTTP CODE的方式
@ResponseStatus
注解方式HttpServletResponse#sendError
HttpServletResponse#setStatus
ResponseEntity
I. 返回Http Code的n中姿势
0. 环境
进入正文之前,先创建一个SpringBoot项目,本文示例所有版本为 spring-boot.2.1.2.RELEASE
(需要测试的小伙伴,本机创建一个maven项目,在pom.xml
文件中,拷贝下面的配置即可)
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</pluginManagement>
</build>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/libs-snapshot-local</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/libs-milestone-local</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-releases</id>
<name>Spring Releases</name>
<url>https://repo.spring.io/libs-release-local</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
下面所有的方法都放在ErrorCodeRest这个类中
@RestController
@RequestMapping(path = "code")
public class ErrorCodeRest {
}
1. ResponseStatus使用姿势
通过注解@ResponseStatus
,来指定返回的http code, 一般来说,使用它有两种姿势,一个是直接加在方法上,一个是加在异常类上
a. 装饰方法
直接在方法上添加注解,并制定对应的code
/**
* 注解方式,只支持标准http状态码
*
* @return
*/
@GetMapping("ano")
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "请求参数异常!")
public String ano() {
return "{\"code\": 400, \"msg\": \"bad request!\"}";
}
实测一下,返回结果如下
➜ ~ curl 'http://127.0.0.1:8080/code/ano' -i
HTTP/1.1 400
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 05 Jan 2020 01:29:04 GMT
Connection: close
{"timestamp":"2020-01-05T01:29:04.673+0000","status":400,"error":"Bad Request","message":"请求参数异常!","path":"/code/ano"}%
当我们发起请求时,返回的状态码为400,返回的数据为springboot默认的错误信息格式
虽然上面这种使用姿势可以设置http code,但是这种使用姿势有什么意义呢?
如果看过web系列教程中的:SpringBoot系列教程web篇之全局异常处理 可能就会有一些映象,配合@ExceptionHandler
来根据异常返回对应的状态码
一个推荐的使用姿势,下面表示当你的业务逻辑中出现数组越界时,返回500的状态码以及完整的堆栈信息
@ResponseBody
@ExceptionHandler(value = ArrayIndexOutOfBoundsException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public String handleArrayIndexOutBounds(HttpServletRequest request, HttpServletResponse response,
ArrayIndexOutOfBoundsException e) throws IOException {
log.info("array index out conf!");
return "aryIndexOutOfBounds: " + getThrowableStackInfo(e);
}
b. 装饰异常类
另外一种使用姿势就是直接装饰在异常类上,然后当你的业务代码中,抛出特定的异常类,返回的httpcode就会设置为注解中的值
/**
* 异常类 + 注解方式,只支持标准http状态码
*
* @return
*/
@GetMapping("exception/500")
public String serverException() {
throw new ServerException("内部异常哦");
}
@GetMapping("exception/400")
public String clientException() {
throw new ClientException("客户端异常哦");
}
@ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR, reason = "服务器失联了,请到月球上呼叫试试~~")
public static class ServerException extends RuntimeException {
public ServerException(String message) {
super(message);
}
}
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "老哥,你的请求有问题~~")
public static class ClientException extends RuntimeException {
public ClientException(String message) {
super(message);
}
}
测试结果如下,在异常类上添加注解的方式,优点在于不需要配合@ExceptionHandler
写额外的逻辑了;缺点则在于需要定义很多的自定义异常类型
➜ ~ curl 'http://127.0.0.1:8080/code/exception/400' -i
HTTP/1.1 400
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 05 Jan 2020 01:37:07 GMT
Connection: close
{"timestamp":"2020-01-05T01:37:07.662+0000","status":400,"error":"Bad Request","message":"老哥,你的请求有问题~~","path":"/code/exception/400"}%
➜ ~ curl 'http://127.0.0.1:8080/code/exception/500' -i
HTTP/1.1 500
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 05 Jan 2020 01:37:09 GMT
Connection: close
{"timestamp":"2020-01-05T01:37:09.389+0000","status":500,"error":"Internal Server Error","message":"服务器失联了,请到月球上呼叫试试~~","path":"/code/exception/500"}%
注意
- ResponseStatus注解的使用姿势,只支持标准的Http Code(必须是枚举类
org.springframework.http.HttpStatus
)
2. ResponseEntity
这种使用姿势就比较简单了,方法的返回结果必须是ResponseEntity
,下面给出两个实际的case
@GetMapping("401")
public ResponseEntity<String> _401() {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("{\"code\": 401, \"msg\": \"未授权!\"}");
}
@GetMapping("451")
public ResponseEntity<String> _451() {
return ResponseEntity.status(451).body("{\"code\": 451, \"msg\": \"自定义异常!\"}");
}
实测结果
➜ ~ curl 'http://127.0.0.1:8080/code/401' -i
HTTP/1.1 401
Content-Type: text/plain;charset=UTF-8
Content-Length: 34
Date: Sun, 05 Jan 2020 01:40:10 GMT
{"code": 401, "msg": "未授权!"}
➜ ~ curl 'http://127.0.0.1:8080/code/451' -i
HTTP/1.1 451
Content-Type: text/plain;charset=UTF-8
Content-Length: 40
Date: Sun, 05 Jan 2020 01:40:19 GMT
{"code": 451, "msg": "自定义异常!"}
从上面的使用实例上看,可以知道这种使用方式,不仅仅支持标准的http code,也支持自定义的code(如返回code 451)
3. HttpServletResponse
这种使用姿势则是直接操作HttpServletResponse
对象,手动录入返回的结果
a. setStatus
/**
* response.setStatus 支持自定义http code,并可以返回结果
*
* @param response
* @return
*/
@GetMapping("525")
public String _525(HttpServletResponse response) {
response.setStatus(525);
return "{\"code\": 525, \"msg\": \"自定义错误码 525!\"}";
}
输出结果
➜ ~ curl 'http://127.0.0.1:8080/code/525' -i
HTTP/1.1 525
Content-Type: text/plain;charset=UTF-8
Content-Length: 47
Date: Sun, 05 Jan 2020 01:45:38 GMT
{"code": 525, "msg": "自定义错误码 525!"}%
使用方式比较简单,直接设置status即可,支持自定义的Http Code返回
b. sendError
使用这种姿势的时候需要注意一下,只支持标准的http code,而且response body中不会有你的业务返回数据,如
/**
* send error 方式,只支持标准http状态码; 且不会带上返回的结果
*
* @param response
* @return
* @throws IOException
*/
@GetMapping("410")
public String _410(HttpServletResponse response) throws IOException {
response.sendError(410, "send 410");
return "{\"code\": 410, \"msg\": \"Gone 410!\"}";
}
@GetMapping("460")
public String _460(HttpServletResponse response) throws IOException {
response.sendError(460, "send 460");
return "{\"code\": 460, \"msg\": \"Gone 460!\"}";
}
输出结果
➜ ~ curl 'http://127.0.0.1:8080/code/410' -i
HTTP/1.1 410
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 05 Jan 2020 01:47:52 GMT
{"timestamp":"2020-01-05T01:47:52.300+0000","status":410,"error":"Gone","message":"send 410","path":"/code/410"}%
➜ ~ curl 'http://127.0.0.1:8080/code/460' -i
HTTP/1.1 500
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 05 Jan 2020 01:47:54 GMT
Connection: close
{"timestamp":"2020-01-05T01:47:54.719+0000","status":460,"error":"Http Status 460","message":"send 460","path":"/code/460"}%
从上面的case也可以看出,当我们使用send error时,如果是标准的http code,会设置对响应头;如果是自定义的不被识别的code,那么返回的http code是500
4, 小结
上面介绍了几种常见的设置响应http code的姿势,下面小结一下使用时的注意事项
ResponseStatus
- 只支持标准的http code
- 装饰自定义异常类,使用时抛出对应的异常类,从而达到设置响应code的效果
- 缺点对非可控的异常类不可用
- 结合
@ExceptionHandler
,用来装饰方法
ResponseEntity
形如:
return ResponseEntity.status(451).body("{\"code\": 451, \"msg\": \"自定义异常!\"}");
- 我个人感觉是最强大的使用姿势,就是写起来没有那么简洁
- 支持自定义code,支持设置 response body
HttpServletResponse
- setStatus: 设置响应code,支持自定义code,支持返回response body
- sendError: 只支持标准的http code,如果传入自定义的code,返回的http code会是500
II. 其他
web系列博文
- 191222-SpringBoot系列教程web篇之自定义请求匹配条件RequestCondition
- 191206-SpringBoot系列教程web篇Listener四种注册姿势
- 191122-SpringBoot系列教程web篇Servlet 注册的四种姿势
- 191120-SpringBoot系列教程Web篇之开启GZIP数据压缩
- 191018-SpringBoot系列教程web篇之过滤器Filter使用指南扩展篇
- 191016-SpringBoot系列教程web篇之过滤器Filter使用指南
- 191012-SpringBoot系列教程web篇之自定义异常处理HandlerExceptionResolver
- 191010-SpringBoot系列教程web篇之全局异常处理
- 190930-SpringBoot系列教程web篇之404、500异常页面配置
- 190929-SpringBoot系列教程web篇之重定向
- 190913-SpringBoot系列教程web篇之返回文本、网页、图片的操作姿势
- 190905-SpringBoot系列教程web篇之中文乱码问题解决
- 190831-SpringBoot系列教程web篇之如何自定义参数解析器
- 190828-SpringBoot系列教程web篇之Post请求参数解析姿势汇总
- 190824-SpringBoot系列教程web篇之Get请求参数解析姿势汇总
- 190822-SpringBoot系列教程web篇之Beetl环境搭建
- 190820-SpringBoot系列教程web篇之Thymeleaf环境搭建
- 190816-SpringBoot系列教程web篇之Freemaker环境搭建
- 190421-SpringBoot高级篇WEB之websocket的使用说明
- 190327-Spring-RestTemplate之urlencode参数解析异常全程分析
- 190317-Spring MVC之基于java config无xml配置的web应用构建
- 190316-Spring MVC之基于xml配置的web应用构建
- 190213-SpringBoot文件上传异常之提示The temporary upload location xxx is not valid
项目源码
12 - 12.异步请求知识点与使用姿势小结
在Servlet3.0就引入了异步请求的支持,但是在实际的业务开发中,可能用过这个特性的童鞋并不多?
本篇博文作为异步请求的扫盲和使用教程,将包含以下知识点
- 什么是异步请求,有什么特点,适用场景
- 四种使用姿势:
- AsyncContext方式
- Callable
- WebAsyncTask
- DeferredResult
I. 异步请求
异步对于我们而言,应该属于经常可以听到的词汇了,在实际的开发中多多少少都会用到,那么什么是异步请求呢
1. 异步请求描述
先介绍一下同步与异步:
一个正常调用,吭哧吭哧执行完毕之后直接返回,这个叫同步;
接收到调用,自己不干,新开一个线程来做,主线程自己则去干其他的事情,等后台线程吭哧吭哧的跑完之后,主线程再返回结果,这个就叫异步
异步请求:
我们这里讲到的异步请求,主要是针对web请求而言,后端响应请求的一种手段,同步/异步对于前端而言是无感知、无区别的
同步请求,后端接收到请求之后,直接在处理请求线程中,执行业务逻辑,并返回
异步请求,后端接收到请求之后,新开一个线程,来执行业务逻辑,释放请求线程,避免请求线程被大量耗时的请求沾满,导致服务不可用
2. 特点
通过上面两张图,可以知道异步请求的最主要特点
- 业务线程,处理请求逻辑
- 请求处理线程立即释放,通过回调处理线程返回结果
3. 场景分析
从特点出发,也可以很容易看出异步请求,更适用于耗时的请求,快速的释放请求处理线程,避免web容器的请求线程被打满,导致服务不可用
举一个稍微极端一点的例子,比如我以前做过的一个多媒体服务,提供图片、音视频的编辑,这些服务接口有同步返回结果的也有异步返回结果的;同步返回结果的接口有快有慢,大部分耗时可能<10ms
,而有部分接口耗时则在几十甚至上百
这种场景下,耗时的接口就可以考虑用异步请求的方式来支持了,避免占用过多的请求处理线程,影响其他的服务
II. 使用姿势
接下来介绍四种异步请求的使用姿势,原理一致,只是使用的场景稍有不同
1. AsyncContext
在Servlet3.0+之后就支持了异步请求,第一种方式比较原始,相当于直接借助Servlet的规范来实现,当然下面的case并不是直接创建一个servlet,而是借助AsyncContext
来实现
@RestController
@RequestMapping(path = "servlet")
public class ServletRest {
@GetMapping(path = "get")
public void get(HttpServletRequest request) {
AsyncContext asyncContext = request.startAsync();
asyncContext.addListener(new AsyncListener() {
@Override
public void onComplete(AsyncEvent asyncEvent) throws IOException {
System.out.println("操作完成:" + Thread.currentThread().getName());
}
@Override
public void onTimeout(AsyncEvent asyncEvent) throws IOException {
System.out.println("超时返回!!!");
asyncContext.getResponse().setCharacterEncoding("utf-8");
asyncContext.getResponse().setContentType("text/html;charset=UTF-8");
asyncContext.getResponse().getWriter().println("超时了!!!!");
}
@Override
public void onError(AsyncEvent asyncEvent) throws IOException {
System.out.println("出现了m某些异常");
asyncEvent.getThrowable().printStackTrace();
asyncContext.getResponse().setCharacterEncoding("utf-8");
asyncContext.getResponse().setContentType("text/html;charset=UTF-8");
asyncContext.getResponse().getWriter().println("出现了某些异常哦!!!!");
}
@Override
public void onStartAsync(AsyncEvent asyncEvent) throws IOException {
System.out.println("开始执行");
}
});
asyncContext.setTimeout(3000L);
asyncContext.start(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(Long.parseLong(request.getParameter("sleep")));
System.out.println("内部线程:" + Thread.currentThread().getName());
asyncContext.getResponse().setCharacterEncoding("utf-8");
asyncContext.getResponse().setContentType("text/html;charset=UTF-8");
asyncContext.getResponse().getWriter().println("异步返回!");
asyncContext.getResponse().getWriter().flush();
// 异步完成,释放
asyncContext.complete();
} catch (Exception e) {
e.printStackTrace();
}
}
});
System.out.println("主线程over!!! " + Thread.currentThread().getName());
}
}
完整的实现如上,简单的来看一下一般步骤
javax.servlet.ServletRequest#startAsync()
获取AsyncContext
- 添加监听器
asyncContext.addListener(AsyncListener)
(这个是可选的)- 用户请求开始、超时、异常、完成时回调
- 设置超时时间
asyncContext.setTimeout(3000L)
(可选) - 异步任务
asyncContext.start(Runnable)
2. Callable
相比较于上面的复杂的示例,SpringMVC可以非常easy的实现,直接返回一个Callable
即可
@RestController
@RequestMapping(path = "call")
public class CallableRest {
@GetMapping(path = "get")
public Callable<String> get() {
Callable<String> callable = new Callable<String>() {
@Override
public String call() throws Exception {
System.out.println("do some thing");
Thread.sleep(1000);
System.out.println("执行完毕,返回!!!");
return "over!";
}
};
return callable;
}
@GetMapping(path = "exception")
public Callable<String> exception() {
Callable<String> callable = new Callable<String>() {
@Override
public String call() throws Exception {
System.out.println("do some thing");
Thread.sleep(1000);
System.out.println("出现异常,返回!!!");
throw new RuntimeException("some error!");
}
};
return callable;
}
}
请注意上面的两种case,一个正常返回,一个业务执行过程中,抛出来异常
分别请求,输出如下
# http://localhost:8080/call/get
do some thing
执行完毕,返回!!!
异常请求: http://localhost:8080/call/exception
do some thing
出现异常,返回!!!
2020-03-29 16:12:06.014 ERROR 24084 --- [nio-8080-exec-5] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] threw exception
java.lang.RuntimeException: some error!
at com.git.hui.boot.async.rest.CallableRest$2.call(CallableRest.java:40) ~[classes/:na]
at com.git.hui.boot.async.rest.CallableRest$2.call(CallableRest.java:34) ~[classes/:na]
at org.springframework.web.context.request.async.WebAsyncManager.lambda$startCallableProcessing$4(WebAsyncManager.java:328) ~[spring-web-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) ~[na:1.8.0_171]
at java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:266) ~[na:1.8.0_171]
at java.util.concurrent.FutureTask.run(FutureTask.java) ~[na:1.8.0_171]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) ~[na:1.8.0_171]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) ~[na:1.8.0_171]
at java.lang.Thread.run(Thread.java:748) [na:1.8.0_171]
3. WebAsyncTask
callable的方式,非常直观简单,但是我们经常关注的超时+异常的处理却不太好,这个时候我们可以用WebAsyncTask
,实现姿势也很简单,包装一下callable
,然后设置各种回调事件即可
@RestController
@RequestMapping(path = "task")
public class WebAysncTaskRest {
@GetMapping(path = "get")
public WebAsyncTask<String> get(long sleep, boolean error) {
Callable<String> callable = () -> {
System.out.println("do some thing");
Thread.sleep(sleep);
if (error) {
System.out.println("出现异常,返回!!!");
throw new RuntimeException("异常了!!!");
}
return "hello world";
};
// 指定3s的超时
WebAsyncTask<String> webTask = new WebAsyncTask<>(3000, callable);
webTask.onCompletion(() -> System.out.println("over!!!"));
webTask.onTimeout(() -> {
System.out.println("超时了");
return "超时返回!!!";
});
webTask.onError(() -> {
System.out.println("出现异常了!!!");
return "异常返回";
});
return webTask;
}
}
4. DeferredResult
DeferredResult
与WebAsyncTask
最大的区别就是前者不确定什么时候会返回结果,
DeferredResult
的这个特点,可以用来做实现很多有意思的东西,如后面将介绍的SseEmitter
就用到了它
下面给出一个实例
@RestController
@RequestMapping(path = "defer")
public class DeferredResultRest {
private Map<String, DeferredResult> cache = new ConcurrentHashMap<>();
@GetMapping(path = "get")
public DeferredResult<String> get(String id) {
DeferredResult<String> res = new DeferredResult<>();
cache.put(id, res);
res.onCompletion(new Runnable() {
@Override
public void run() {
System.out.println("over!");
}
});
return res;
}
@GetMapping(path = "pub")
public String publish(String id, String content) {
DeferredResult<String> res = cache.get(id);
if (res == null) {
return "no consumer!";
}
res.setResult(content);
return "over!";
}
}
在上面的实例中,用户如果先访问http://localhost:8080/defer/get?id=yihuihui
,不会立马有结果,直到用户再次访问http://localhost:8080/defer/pub?id=yihuihui&content=哈哈
时,前面的请求才会有结果返回
那么这个可以设置超时么,如果一直把前端挂住,貌似也不太合适吧
- 在构造方法中指定超时时间:
new DeferredResult<>(3000L)
- 设置全局的默认超时时间
@Configuration
@EnableWebMvc
public class WebConf implements WebMvcConfigurer {
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
// 超时时间设置为60s
configurer.setDefaultTimeout(TimeUnit.SECONDS.toMillis(10));
}
}
II. 其他
0. 项目
相关博文
- 007-优化web请求三-异步调用【WebAsyncTask】
- 高性能关键技术之—体验Spring MVC的异步模式(Callable、WebAsyncTask、DeferredResult) 基础使用篇
系列博文
- 200105-SpringBoot系列web篇之自定义返回Http-Code的n种姿势
- 191222-SpringBoot系列教程web篇之自定义请求匹配条件RequestCondition
- 191206-SpringBoot系列教程web篇Listener四种注册姿势
- 191122-SpringBoot系列教程web篇Servlet 注册的四种姿势
- 191120-SpringBoot系列教程Web篇之开启GZIP数据压缩
- 191018-SpringBoot系列教程web篇之过滤器Filter使用指南扩展篇
- 191016-SpringBoot系列教程web篇之过滤器Filter使用指南
- 191012-SpringBoot系列教程web篇之自定义异常处理HandlerExceptionResolver
- 191010-SpringBoot系列教程web篇之全局异常处理
- 190930-SpringBoot系列教程web篇之404、500异常页面配置
- 190929-SpringBoot系列教程web篇之重定向
- 190913-SpringBoot系列教程web篇之返回文本、网页、图片的操作姿势
- 190905-SpringBoot系列教程web篇之中文乱码问题解决
- 190831-SpringBoot系列教程web篇之如何自定义参数解析器
- 190828-SpringBoot系列教程web篇之Post请求参数解析姿势汇总
- 190824-SpringBoot系列教程web篇之Get请求参数解析姿势汇总
- 190822-SpringBoot系列教程web篇之Beetl环境搭建
- 190820-SpringBoot系列教程web篇之Thymeleaf环境搭建
- 190816-SpringBoot系列教程web篇之Freemaker环境搭建
- 190421-SpringBoot高级篇WEB之websocket的使用说明
- 190327-Spring-RestTemplate之urlencode参数解析异常全程分析
- 190317-Spring MVC之基于java config无xml配置的web应用构建
- 190316-Spring MVC之基于xml配置的web应用构建
- 190213-SpringBoot文件上传异常之提示The temporary upload location xxx is not valid
源码
13 - 13.SSE服务器发送事件详解
SSE全称Server Sent Event
,直译一下就是服务器发送事件,一般的项目开发中,用到的机会不多,可能很多小伙伴不太清楚这个东西,到底是干啥的,有啥用
本文主要知识点如下:
- SSE扫盲,应用场景分析
- 借助异步请求实现sse功能,加深概念理解
- 使用
SseEmitter
实现一个简单的推送示例
I. SSE扫盲
对于sse基础概念比较清楚的可以跳过本节
1. 概念介绍
sse(Server Sent Event
),直译为服务器发送事件,顾名思义,也就是客户端可以获取到服务器发送的事件
我们常见的http交互方式是客户端发起请求,服务端响应,然后一次请求完毕;但是在sse的场景下,客户端发起请求,连接一直保持,服务端有数据就可以返回数据给客户端,这个返回可以是多次间隔的方式
2. 特点分析
SSE最大的特点,可以简单规划为两个
- 长连接
- 服务端可以向客户端推送信息
了解websocket的小伙伴,可能也知道它也是长连接,可以推送信息,但是它们有一个明显的区别
sse是单通道,只能服务端向客户端发消息;而webscoket是双通道
那么为什么有了webscoket还要搞出一个sse呢?既然存在,必然有着它的优越之处
sse | websocket |
---|---|
http协议 | 独立的websocket协议 |
轻量,使用简单 | 相对复杂 |
默认支持断线重连 | 需要自己实现断线重连 |
文本传输 | 二进制传输 |
支持自定义发送的消息类型 | - |
3. 应用场景
从sse的特点出发,我们可以大致的判断出它的应用场景,需要轮询获取服务端最新数据的case下,多半是可以用它的
比如显示当前网站在线的实时人数,法币汇率显示当前实时汇率,电商大促的实时成交额等等…
II. 手动实现sse功能
sse本身是有自己的一套玩法的,后面会进行说明,这一小节,则主要针对sse的两个特点长连接 + 后端推送数据
,如果让我们自己来实现这样的一个接口,可以怎么做?
1. 项目创建
借助SpringBoot 2.2.1.RELEASE
来创建一个用于演示的工程项目,核心的xml依赖如下
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</pluginManagement>
</build>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/libs-snapshot-local</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/libs-milestone-local</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-releases</id>
<name>Spring Releases</name>
<url>https://repo.spring.io/libs-release-local</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
2. 功能实现
在Http1.1支持了长连接,请求头添加一个Connection: keep-alive
即可
在这里我们借助异步请求来实现sse功能,至于什么是异步请求,推荐查看博文: 【WEB系列】异步请求知识点与使用姿势小结
因为后端可以不定时返回数据,所以我们需要注意的就是需要保持连接,不要返回一次数据之后就断开了;其次就是需要设置请求头Content-Type: text/event-stream;charset=UTF-8
(如果不是流的话会怎样?)
// 新建一个容器,保存连接,用于输出返回
private Map<String, PrintWriter> responseMap = new ConcurrentHashMap<>();
// 发送数据给客户端
private void writeData(String id, String msg, boolean over) throws IOException {
PrintWriter writer = responseMap.get(id);
if (writer == null) {
return;
}
writer.println(msg);
writer.flush();
if (over) {
responseMap.remove(id);
}
}
// 推送
@ResponseBody
@GetMapping(path = "subscribe")
public WebAsyncTask<Void> subscribe(String id, HttpServletResponse response) {
Callable<Void> callable = () -> {
response.setHeader("Content-Type", "text/event-stream;charset=UTF-8");
responseMap.put(id, response.getWriter());
writeData(id, "订阅成功", false);
while (true) {
Thread.sleep(1000);
if (!responseMap.containsKey(id)) {
break;
}
}
return null;
};
// 采用WebAsyncTask 返回 这样可以处理超时和错误 同时也可以指定使用的Excutor名称
WebAsyncTask<Void> webAsyncTask = new WebAsyncTask<>(30000, callable);
// 注意:onCompletion表示完成,不管你是否超时、是否抛出异常,这个函数都会执行的
webAsyncTask.onCompletion(() -> System.out.println("程序[正常执行]完成的回调"));
// 这两个返回的内容,最终都会放进response里面去===========
webAsyncTask.onTimeout(() -> {
responseMap.remove(id);
System.out.println("超时了!!!");
return null;
});
// 备注:这个是Spring5新增的
webAsyncTask.onError(() -> {
System.out.println("出现异常!!!");
return null;
});
return webAsyncTask;
}
看一下上面的实现,基本上还是异步请求的那一套逻辑,请仔细看一下callable
中的逻辑,有一个while循环,来保证长连接不中断
接下来我们新增两个接口,用来模拟后端给客户端发送消息,关闭连接的场景
@ResponseBody
@GetMapping(path = "push")
public String pushData(String id, String content) throws IOException {
writeData(id, content, false);
return "over!";
}
@ResponseBody
@GetMapping(path = "over")
public String over(String id) throws IOException {
writeData(id, "over", true);
return "over!";
}
我们简单的来演示下操作过程
III. SseEmitter
上面只是简单实现了sse的长连接 + 后端推送消息,但是与标准的SSE还是有区别的,sse有自己的规范,而我们上面的实现,实际上并没有管这个,导致的问题是前端按照sse的玩法来请求数据,可能并不能正常工作
1. sse规范
在html5的定义中,服务端sse,一般需要遵循以下要求
请求头
开启长连接 + 流方式传递
Content-Type: text/event-stream;charset=UTF-8
Cache-Control: no-cache
Connection: keep-alive
数据格式
服务端发送的消息,由message组成,其格式如下:
field:value\n\n
其中field有五种可能
- 空: 即以
:
开头,表示注释,可以理解为服务端向客户端发送的心跳,确保连接不中断 - data:数据
- event: 事件,默认值
- id: 数据标识符用id字段表示,相当于每一条数据的编号
- retry: 重连时间
2. 实现
SpringBoot利用SseEmitter来支持sse,可以说非常简单了,直接返回SseEmitter
对象即可;重写一下上面的逻辑
@RestController
@RequestMapping(path = "sse")
public class SseRest {
private static Map<String, SseEmitter> sseCache = new ConcurrentHashMap<>();
@GetMapping(path = "subscribe")
public SseEmitter push(String id) {
// 超时时间设置为1小时
SseEmitter sseEmitter = new SseEmitter(3600_000L);
sseCache.put(id, sseEmitter);
sseEmitter.onTimeout(() -> sseCache.remove(id));
sseEmitter.onCompletion(() -> System.out.println("完成!!!"));
return sseEmitter;
}
@GetMapping(path = "push")
public String push(String id, String content) throws IOException {
SseEmitter sseEmitter = sseCache.get(id);
if (sseEmitter != null) {
sseEmitter.send(content);
}
return "over";
}
@GetMapping(path = "over")
public String over(String id) {
SseEmitter sseEmitter = sseCache.get(id);
if (sseEmitter != null) {
sseEmitter.complete();
sseCache.remove(id);
}
return "over";
}
}
上面的实现,用到了SseEmitter的几个方法,解释如下
send()
: 发送数据,如果传入的是一个非SseEventBuilder
对象,那么传递参数会被封装到data中complete()
: 表示执行完毕,会断开连接onTimeout()
: 超时回调触发onCompletion()
: 结束之后的回调触发
同样演示一下访问请求
上图总的效果和前面的效果差不多,而且输出还待上了前缀,接下来我们写一个简单的html消费端,用来演示一下完整的sse的更多特性
<!doctype html>
<html lang="en">
<head>
<title>Sse测试文档</title>
</head>
<body>
<div>sse测试</div>
<div id="result"></div>
</body>
</html>
<script>
var source = new EventSource('http://localhost:8080/sse/subscribe?id=yihuihui');
source.onmessage = function (event) {
text = document.getElementById('result').innerText;
text += '\n' + event.data;
document.getElementById('result').innerText = text;
};
<!-- 添加一个开启回调 -->
source.onopen = function (event) {
text = document.getElementById('result').innerText;
text += '\n 开启: ';
console.log(event);
document.getElementById('result').innerText = text;
};
</script>
将上面的html文件放在项目的resources/static
目录下;然后修改一下前面的SseRest
@Controller
@RequestMapping(path = "sse")
public class SseRest {
@GetMapping(path = "")
public String index() {
return "index.html";
}
@ResponseBody
@GetMapping(path = "subscribe", produces = {MediaType.TEXT_EVENT_STREAM_VALUE})
public SseEmitter push(String id) {
// 超时时间设置为3s,用于演示客户端自动重连
SseEmitter sseEmitter = new SseEmitter(1_000L);
// 设置前端的重试时间为1s
sseEmitter.send(SseEmitter.event().reconnectTime(1000).data("连接成功"));
sseCache.put(id, sseEmitter);
System.out.println("add " + id);
sseEmitter.onTimeout(() -> {
System.out.println(id + "超时");
sseCache.remove(id);
});
sseEmitter.onCompletion(() -> System.out.println("完成!!!"));
return sseEmitter;
}
}
我们上面超时时间设置的比较短,用来测试下客户端的自动重连,如下,开启的日志不断增加
其次将SseEmitter的超时时间设长一点,再试一下数据推送功能
请注意上面的演示,当后端结束了长连接之后,客户端会自动重新再次连接,不用写外的重试逻辑了,就这么神奇
3. 小结
本篇文章介绍了SSE的相关知识点,并对比websocket给出了sse的优点(至于啥优点请往上翻)
请注意,本文虽然介绍了两种sse的方式,第一种借助异步请求来实现,如果需要完成sse的规范要求,需要自己做一些适配,如果需要了解sse底层实现原理的话,可以参考一下;在实际的业务开发中,推荐使用SseEmitter
IV. 其他
0. 项目
系列博文
- 200329-SpringBoot系列教程web篇之异步请求知识点与使用姿势小结
- 200105-SpringBoot系列教程web篇之自定义返回Http-Code的n种姿势
- 191222-SpringBoot系列教程web篇之自定义请求匹配条件RequestCondition
- 191206-SpringBoot系列教程web篇Listener四种注册姿势
- 191122-SpringBoot系列教程web篇Servlet 注册的四种姿势
- 191120-SpringBoot系列教程Web篇之开启GZIP数据压缩
- 191018-SpringBoot系列教程web篇之过滤器Filter使用指南扩展篇
- 191016-SpringBoot系列教程web篇之过滤器Filter使用指南
- 191012-SpringBoot系列教程web篇之自定义异常处理HandlerExceptionResolver
- 191010-SpringBoot系列教程web篇之全局异常处理
- 190930-SpringBoot系列教程web篇之404、500异常页面配置
- 190929-SpringBoot系列教程web篇之重定向
- 190913-SpringBoot系列教程web篇之返回文本、网页、图片的操作姿势
- 190905-SpringBoot系列教程web篇之中文乱码问题解决
- 190831-SpringBoot系列教程web篇之如何自定义参数解析器
- 190828-SpringBoot系列教程web篇之Post请求参数解析姿势汇总
- 190824-SpringBoot系列教程web篇之Get请求参数解析姿势汇总
- 190822-SpringBoot系列教程web篇之Beetl环境搭建
- 190820-SpringBoot系列教程web篇之Thymeleaf环境搭建
- 190816-SpringBoot系列教程web篇之Freemaker环境搭建
- 190421-SpringBoot高级篇WEB之websocket的使用说明
- 190327-Spring-RestTemplate之urlencode参数解析异常全程分析
- 190317-Spring MVC之基于java config无xml配置的web应用构建
- 190316-Spring MVC之基于xml配置的web应用构建
- 190213-SpringBoot文件上传异常之提示The temporary upload location xxx is not valid
源码
14 - 14.thymeleaf foreach踩坑记录
话说自从前后端分离之后,前后端放在一起的场景就很少了,最近写个简单的后台,突然踩坑了,使用themeleaf模板渲染时,发现th:each
来遍历生成表单数据,一直抛异常,提示Property or field 'xxx' cannot be found on null
接下来看一下这个问题到底是个什么情况
I. 项目搭建
1. 项目依赖
本项目借助SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ IDEA
进行开发
开一个web服务用于测试
<dependencies>
<!-- 邮件发送的核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
配置文件application.yml
server:
port: 8080
spring:
thymeleaf:
mode: HTML
encoding: UTF-8
servlet:
content-type: text/html
cache: false
II. 问题复现与处理
1. 场景复现
一个最基础的demo,来演示一下问题
@Controller
public class IndexController {
public Map<String, Object> newMap(String key, Object val, Object... kv) {
Map<String, Object> map = new HashMap<>();
map.put(key, val);
for (int i = 0; i < kv.length; i += 2) {
map.put(String.valueOf(kv[i]), kv[i + 1]);
}
return map;
}
@GetMapping(path = "list")
public String list(Model model) {
List<Map> list = new ArrayList<>();
list.add(newMap("user", "yh", "name", "一灰"));
list.add(newMap("user", "2h", "name", "2灰"));
list.add(newMap("user", "3h", "name", "3灰"));
model.addAttribute("list", list);
return "list";
}
}
对应的html文件如下(注意,放在资源目录 templates
下)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
</head>
<body>
<div>
<div th:each="item: ${list}">
<span th:text="${item.user}"></span>
<span th:text="${item.name}"></span>
</div>
<hr/>
<p th:each="item: ${list}">
<p th:text="${item.user}"></p>
<p th:text="${item.name}"></p>
</p>
</div>
</body>
</html>
注意上面的模板,有两个each遍历,出现问题的是第二个
2. 原因说明
上面提示user没有,那么是否是语法问题呢?将html改成下面这个时
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
</head>
<body>
<div>
<div th:each="item: ${list}">
<span th:text="${item.user}"></span>
<span th:text="${item.name}"></span>
</div>
</div>
</body>
</html>
相同的写法,上面这个就可以,经过多方尝试,发现出现问题的原因居然是<p>
这个标签
简单来讲,就是<p>
标签不能使用th:each
,测试一下其他的标签之后发现<img>
,<input>
标签也不能用
那么问题来了,为啥这几个标签不能使用each呢?
这个原因可能就需要去瞅一下实现逻辑了,有知道的小伙伴可以科普一下
III. 不能错过的源码和相关知识点
0. 项目
- 工程:https://github.com/liuyueyi/spring-boot-demo
- 源码:https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-boot/
1. 微信公众号: 一灰灰Blog
尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
- 一灰灰Blog个人博客 https://blog.hhui.top
- 一灰灰Blog-Spring专题博客 http://spring.hhui.top
15 - 15.如何支持下划线驼峰互转的传参与返回
SpringBoot系列之Web如何支持下划线驼峰互转的传参与返回
接下来介绍一个非常现实的应用场景,有些时候后端接口对外定义的传参/返回都是下划线命名风格,但是Java本身是推荐驼峰命名方式的,那么必然就存在一个传参下换线,转换成驼峰的场景;以及在返回时,将驼峰命名的转换成下划线
那么如何支持上面这种应用场景呢?
本文介绍几种常见的手段
I. 项目搭建
1. 项目依赖
本项目借助SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ IDEA
进行开发
开一个web服务用于测试
<dependencies>
<!-- 邮件发送的核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
配置文件application.yml
server:
port: 8080
2. 需求拆分
接下来为了更方便的理解我们要做的事情,对上面的应用场景进行一些拆分,方便理解
2.1 请求参数解析
对于请求参数,外部传递是下划线命名格式的方式,需要与项目中驼峰命名的对象进行映射,所以这里的问题点就是无法走默认的绑定规则,需要我们进行兼容处理
比如传参是 user_name = 一灰灰
,但是我们接收的参数是 userName
2.2 返回结果处理
返回结果的处理,这里单指返回json对象的场景,一个普通的POJO对象,正常序列化为json字符串时,key实际上与对象的成员名是一致的,而现在则希望将key统一成下划线风格的方式
如,返回一个简单的实体对象
public class ViewDo {
private Integer userId;
private string userName;
}
对应期待返回的json串为
{
"user_name" : "一灰灰",
"user_id" : 110
}
II. 支持方式
为了简化后续的流程,我们这里的传参都确定两个userName + userId,对应项目中的实体类如
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class ViewDo {
private Integer userId;
private String userName;
}
1. 请求参数解析
1.1 @RequestParam注解方式
最简单也是最容易想到的方式自然是直接使用RequestParam
注解,将所有的请求参数都通过它来重命名
@GetMapping(path = "getV3")
public ViewDo getV3(@RequestParam("user_id") Integer userId, @RequestParam("user_name") String userName) {
String str = "userId: " + userId + " userName: " + userName;
System.out.println(str);
return new ViewDo(userId, userName);
}
使用上面直接来写参数映射关系的方式属于比较常见的方法了,但是存在一个问题
- 通用性差(每个接口的每个参数都要这么整,如果工资是按照代码来付费的话,那还是可以接收的;否则这个写法,就真的有点难受了)
- 若接口参数定义的是Map、Java bean实体(POJO),这个映射关联就不太好处理了
除了上面这个问题之外,有个不是问题的问题(为什么这么说,且看下面的说法)
- 如果我的接口传参,希望同时接收驼峰和下划线命名的传参(现实中还真有这种神经病似的场景,别问我怎么知道的),上面这个是不行的
1.2 Json传参指定命名策略
上面的case,适用于常见的get请求,post表单传参,然后在接口处一一定义参数;对于post json传参时,我们可以考虑通过定义json序列化的命名策略,来支持下划线与驼峰的互转
比如SpringMVC默认使用的jackson来实现json序列化,那么我们可以直接通过指定jackson的PropertyNamingStrategy来完成
配置文件中 application.yml,添加下面这行
spring:
jackson:
# 使用jackson进行json序列化时,可以将下划线的传参设置给驼峰的非简单对象成员上;并返回下划线格式的json串
# 特别注意。使用这种方式的时候,要求不能有自定义的WebMvcConfigurationSupport,因为会覆盖默认的处理方式
# 解决办法就是 拿到ObjectMapper的bean对象,手动塞入进去
property-naming-strategy: SNAKE_CASE
对应的接口定义如下
/**
* post json串
* curl 'http://127.0.0.1:8080/postV2' -X POST -H 'content-type:application/json' -d '{"user_id": 123, "user_name": "一灰灰"}'
* @param viewDo
* @return
*/
@PostMapping(path = "postV2")
public ViewDo postV2(@RequestBody ViewDo viewDo) {
System.out.println(viewDo);
return viewDo;
}
实际请求之后,看一下效果
注意
- 使用上面这种配置的方式,需要特比注意的,如果在项目中自己定义了
WebMvcConfigurationSupport
,那么上面的配置将不会生效(至于具体的原因,后面有机会单独说明)
当我们实际的项目中,无法直接使用上面这种配置时,可以考虑使用下面的方式
@SpringBootApplication
public class Application extends WebMvcConfigurationSupport {
/**
* 下面这个设置,可以实现json参数解析/返回时,传入的下划线转驼峰;输出的驼峰转下划线
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
ObjectMapper objectMapper = converter.getObjectMapper();
// 设置驼峰标志转下划线
objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
// 设置格式化内容
converter.setObjectMapper(objectMapper);
converters.add(0, converter);
super.extendMessageConverters(converters);
}
}
使用jackson的命名策略来支持驼峰下划线的转换虽好,但是存在一个非常明显的缺陷
- 它只适用于json传参
1.3 自定义DataBinder
对于非json的传承,比如普通的get请求,post表单传参,然后在接口处通过定义一个POJO参数类来接收,此时又应该怎么处理呢?
比如接口定义如下
/**
* POJO 对应Spring中的参数转换是 ServletModelAttributeMethodProcessor | RequestParamMethodArgumentResolver
* @param viewDo
* @return
*/
@GetMapping(path = "getV5")
public ViewDo getV5(ViewDo viewDo) {
System.out.println("v5: " + viewDo);
return viewDo;
}
/**
* curl 'http://127.0.0.1:8080/postV1' -X POST -d 'user_id=123&user_name=一灰灰'
* 注意:非json传参,jackson的配置将不会生效,即上面这个请求是不会实现下划线转驼峰的; 但是返回结果会是下划线的
* @param viewDo
* @return
*/
@PostMapping(path = "postV1")
public ViewDo post(ViewDo viewDo) {
System.out.println(viewDo);
return viewDo;
}
对于上面这种场景,一个想法就是是否可以在ViewDo的成员上,添加一个注解,指定参数名,一如RequestParam
,不过Spring貌似并没有提供这种支持能力
因此我们可以考虑自己来实现数据绑定,下面提供一个基础的实现, 来演示这种方式改怎么玩(相对完整的基于注解的映射方式,下篇博文介绍)
public class SimpleDataBinder extends ExtendedServletRequestDataBinder {
public SimpleDataBinder(Object target, String objectName) {
super(target, objectName);
}
@Override
protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
super.addBindValues(mpvs, request);
if (!mpvs.contains("userName")) {
mpvs.add("userName", getVal(mpvs, "user_name"));
}
if (!mpvs.contains("userId")) {
mpvs.add("userId", getVal(mpvs, "user_id"));
}
}
private Object getVal(MutablePropertyValues mpvs, String key) {
PropertyValue pv = mpvs.getPropertyValue(key);
return pv != null ? pv.getValue() : null;
}
}
然后在参数解析中,使用这个DataBinder
public class SimpleArgumentProcessor extends ServletModelAttributeMethodProcessor {
public SimpleArgumentProcessor(boolean annotationNotRequired) {
super(annotationNotRequired);
}
@Override
protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest nativeWebRequest) {
Object target = binder.getTarget();
SimpleDataBinder dataBinder = new SimpleDataBinder(target, binder.getObjectName());
super.bindRequestParameters(dataBinder, nativeWebRequest);
}
}
接着就是注册这个参数解析
@SpringBootApplication
public class Application extends WebMvcConfigurationSupport {
@Override
protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new SimpleArgumentProcessor(true));
}
}
再次请求时,可以发现下划线的传参也可以映射到ViewDo对象上(无论是get请求还是post请求,都可以正确映射)
2.返回结果
对于返回结果,希望返回下划线格式的json串,除了上面介绍到的设置json序列化的命名策略之外,还有下面几种配置方式
2.1 属性注解 @JsonProperty
直接在POJO对象的成员上,指定希望输出的name
public static class ViewDo {
@JsonProperty("user_id")
private Integer userId;
@JsonProperty("user_name")
private String userName;
}
2.2 实体类注解 @JsonNaming
直接在类上添加注解,指定驼峰策略
@JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class)
public static class ViewDo {
private Integer userId;
private String userName;
}
2.3 全局配置
上面两种缺点比较明显,不太通用;更通用的选择和前面传参的json序列化配置方式一样,两种姿势
配置文件指定
spring:
jackson:
# 使用jackson进行json序列化时,可以将下划线的传参设置给驼峰的非简单对象成员上;并返回下划线格式的json串
# 特别注意。使用这种方式的时候,要求不能有自定义的WebMvcConfigurationSupport,因为会覆盖默认的处理方式
# 解决办法就是 拿到ObjectMapper的bean对象,手动塞入进去
property-naming-strategy: SNAKE_CASE
前面也说到,上面这种配置可能会失效(比如你设置了自己的WebMvcConfig),推荐使用下面的方式
public class Application extends WebMvcConfigurationSupport {
/**
* 下面这个设置,可以实现json参数解析/返回时,传入的下划线转驼峰;输出的驼峰转下划线
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
ObjectMapper objectMapper = converter.getObjectMapper();
// 设置驼峰标志转下划线
objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
// 设置格式化内容
converter.setObjectMapper(objectMapper);
converters.add(0, converter);
super.extendMessageConverters(converters);
}
}
3. 小结
本文主要介绍了几种实例case,用于实现传参/返回的驼峰与下划线的互转,核心策略,有下面几种
- 传参:
@RequestParam
指定真正的传参name - Json传参、返回:通过定义json序列化框架的PropertyNamingStrategy,来实现
- 普通表单传参/get传参,映射POJO时:通过自定义的DataBinder,来实现映射
虽然上面几种姿势,可以满足我们的基本诉求,但是如果我希望实现一个通用的下划线/驼峰互转策略,即不管传参是下划线还是驼峰,都可以正确无误的绑定到接口的参数变量上,可以怎么实现呢?
最后再抛出一个问题,如果接收参数是Map,上面的几种实现姿势会生效么?又可以如何怎么处理map这种场景呢?
III. 不能错过的源码和相关知识点
0. 项目
- 工程:https://github.com/liuyueyi/spring-boot-demo
- 源码:https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-boot/202-web-params-camel
1. 微信公众号: 一灰灰Blog
尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
- 一灰灰Blog个人博客 https://blog.hhui.top
- 一灰灰Blog-Spring专题博客 http://spring.hhui.top
16 - 16.定义接口返回类型的几种方式
实现一个web接口返回json数据,基本上是每一个javaer非常熟悉的事情了;那么问题来了,如果我有一个接口,除了希望返回json格式的数据之外,若也希望可以返回xml格式数据可行么?
答案当然是可行的,接下来我们将介绍一下,一个接口的返回数据类型,可以怎么处理
I. 项目搭建
本文创建的实例工程采用SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ idea
进行开发
1. pom依赖
具体的SpringBoot项目工程创建就不赘述了,对于pom文件中,需要重点关注下面两个依赖类
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
</dependencies>
注意 jackson-datafromat-xml
这个依赖,加上这个主要时为了支持返回xml格式的数据
II. 返回类型设置的多种方式
正常来讲,一个RestController的接口,默认返回的是Json格式数据,当我们引入了上面的xml包之后,会怎样呢?返回的还是json么?
1.通过produce设置返回类型
如果一个接口希望返回json或者xml格式的数据,最容易想到的方式就是直接设置RequestMapping
注解中的produce属性
这个值主要就是用来设置这个接口响应头中的content-type
; 如我们现在有两个接口,一个指定返回json格式数据,一个指定返回xml格式数据,可以如下写
@RestController
public class IndexRest {
@Data
public static class ResVo<T> {
private int code;
private String msg;
private T data;
public ResVo(int code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
}
@GetMapping(path = "/xml", produces = {MediaType.APPLICATION_XML_VALUE})
public ResVo<String> xml() {
return new ResVo<>(0, "ok", "返回xml");
}
@GetMapping(path = "/json", produces = {MediaType.APPLICATION_JSON_VALUE})
public ResVo<String> json() {
return new ResVo<>(0, "ok", "返回json");
}
}
上面的实现中
- xml接口,指定
produces = application/xml
- json接口,指定
produces = applicatin/json
接下来我们访问一下看看返回的是否和预期一致
从上面截图也可以看出,xml接口返回的是xml格式数据;json接口返回的是json格式数据
2. 通过请求头accept设置返回类型
上面的方式,非常直观,自然我们就会有一个疑问,当接口上不指定produces属性时,直接访问会怎么表现呢?
@GetMapping(path = "/")
public ResVo<String> index() {
return new ResVo<>(0, "ok", "简单的测试");
}
请注意上面的截图,两种访问方式返回的数据类型不一致
- curl请求:返回json格式数据
- 浏览器请求:返回
application/xhtml+xml
响应头的数据(实际上还是xml格式)
那么问题来了,为什么两者的表现形式不一致呢?
对着上面的图再看三秒,会发现主要的差别点就在于请求头Accept
不同;我们可以通过这个请求头参数,来要求服务端返回我希望的数据类型
如指定返回json格式数据
curl 'http://127.0.0.1:8080' -H 'Accept:application/xml' -iv
curl 'http://127.0.0.1:8080' -H 'Accept:application/json' -iv
从上面的执行结果也可以看出,返回的类型与预期的一致;
说明
请求头可以设置多种MediaType,用英文逗号分割,后端接口会根据自己定义的produce与请求头希望的mediaType取交集,至于最终选择的顺序则以accept中出现的顺序为准
看一下实际的表现来验证下上面的说法
通过请求头来控制返回数据类型的方式可以说是非常经典的策略了,(遵循html协议还有什么好说的呢!)
3. 请求参数来控制返回类型
除了上面介绍的两种方式之外,还可以考虑为所有的接口,增加一个根据特定的请求参数来控制返回的类型的方式
比如我们现在定义,所有的接口可以选传一个参数 mediaType
,如果值为xml,则返回xml格式数据;如果值为json,则返回json格式数据
当不传时,默认返回json格式数据
基于此,我们主要借助mvc配置中的内容协商ContentNegotiationConfigurer
来实现
@SpringBootApplication
public class Application implements WebMvcConfigurer {
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.favorParameter(true)
// 禁用accept协商方式,即不关心前端传的accept值
// .ignoreAcceptHeader(true)
// 哪个放在前面,哪个的优先级就高; 当上面这个accept未禁用时,若请求传的accept不能覆盖下面两种,则会出现406错误
.defaultContentType(MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML)
// 根据传参mediaType来决定返回样式
.parameterName("mediaType")
// 当acceptHeader未禁用时,accept的值与mediaType传参的值不一致时,以mediaType的传值为准
// mediaType值可以不传,为空也行,但是不能是json/xml之外的其他值
.mediaType("json", MediaType.APPLICATION_JSON)
.mediaType("xml", MediaType.APPLICATION_XML);
}
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}
上面的实现中,添加了很多注释,先别急;我来逐一进行说明
.parameterName("mediaType")
// 当acceptHeader未禁用时,accept的值与mediaType传参的值不一致时,以mediaType的传值为准
// mediaType值可以不传,为空也行,但是不能是json/xml之外的其他值
.mediaType("json", MediaType.APPLICATION_JSON)
.mediaType("xml", MediaType.APPLICATION_XML);
上面这三行代码,主要就是说,现在可以根据传参 mediaType 来控制返回的类型,我们新增一个接口来验证一下
@GetMapping(path = "param")
public ResVo<String> params(@RequestParam(name = "mediaType", required = false) String mediaType) {
return new ResVo<>(0, "ok", String.format("基于传参来决定返回类型:%s", mediaType));
}
我们来看下几个不同的传参表现
# 返回json格式数据
curl 'http://127.0.0.1:8080/param?mediaType=json' -iv
# 返回xml格式数据
curl 'http://127.0.0.1:8080/param?mediaType=xml' -iv
# 406错误
curl 'http://127.0.0.1:8080/param?mediaType=text' -iv
# 走默认的返回类型,json在前,所以返回json格式数据(如果将xml调整到前面,则返回xml格式数据,主要取决于 `.defaultContentType(MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML)`)
curl 'http://127.0.0.1:8080/param' -iv
疑问:若请求头中传递了Accept或者接口上定义了produce,会怎样?
当指定了accept时,并且传参中指定了mediaType,则以传参为准
- 如
accept: application/json,application.xml
, 此时mediaType=json
, 返回json格式 - 如
accept: application/json
, 此时mediaTyep=xml
, 返回xml格式 - 如
accept: text/html
,此时mediaType=xml
,此时返回的也是xml格式 - 如
accept: text/html
,此时mediaType
不传时 ,因为无法处理text/html
类型,所以会出现406 - 如
accept: application/xml
, 但是mediaType
不传,虽然默认优先是json,此时返回的也是xml格式,与请求头希望的保持一致
但是若传参与produce冲突了,那么就直接406异常,不会选择mediaType设置的类型
- 如
produce = applicatin/json
, 但是mediaType=xml
,此时就会喜提406
细心的小伙伴可能发现了上面的配置中,注释了一行 .ignoreAcceptHeader(true)
,当我们把它打开之后,前面说的Accept请求头可以随意传,我们完全不care,当做没有传这个参数进行处理即可开
4.小结
本文介绍了三种方式,控制接口返回数据类型
方式一
接口上定义produce, 如 @GetMapping(path = "p2", produces = {"application/xml", "application/json"})
注意produces属性值是有序的,即先定义的优先级更高;当一个请求可以同时接受xml/json格式数据时,上面这个定义会确保这个接口现有返回xml格式数据
方式二
借助标准的请求头accept,控制希望返回的数据类型;但是需要注意的时,使用这种方式时,要求后端不能设置ContentNegotiationConfigurer.ignoreAcceptHeader(true)
在实际使用这种方式的时候,客户端需要额外注意,Accept请求头中定义的MediaType的顺序,是优于后端定义的produces顺序的,因此用户需要将自己实际希望接受的数据类型放在前面,或者干脆就只设置一个
方式三
借助ContentNegotiationConfigurer
实现通过请求参数来决定返回类型,常见的配置方式形如
configurer.favorParameter(true)
// 禁用accept协商方式,即不关心前端传的accept值
// .ignoreAcceptHeader(true)
// 哪个放在前面,哪个的优先级就高; 当上面这个accept未禁用时,若请求传的accept不能覆盖下面两种,则会出现406错误
.defaultContentType(MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML)
// 根据传参mediaType来决定返回样式
.parameterName("mediaType")
// 当acceptHeader未禁用时,accept的值与mediaType传参的值不一致时,以mediaType的传值为准
// mediaType值可以不传,为空也行,但是不能是json/xml之外的其他值
.mediaType("json", MediaType.APPLICATION_JSON)
.mediaType("xml", MediaType.APPLICATION_XML);
即添加这个设置之后,最终的表现为:
- 请求参数指定的返回类型,优先级最高,返回指定参数对应的类型
- 没有指定参数时,选择defaultContentType定义的默认返回类型与接口
produce
中支持的求交集,优先级则按照defaultContentType中定义的顺序来选择 - 没有指定参数时,若此时还有accept请求头,则请求头中定义顺序的优先级高于 defaultContentType, 高于 produce
注意注意:当配置中忽略了AcceptHeader时,.ignoreAcceptHeader(true)
,上面第三条作废
最后的最后,本文所有的源码可以再下面的git中获取;本文的知识点已经汇总在《一灰灰的Spring专栏》 两百多篇的原创系列博文,你值得拥有;我是一灰灰,咱们下次再见