因为最近事比较杂,很多文章都没有连续性,想到哪写到哪,大家海涵,后面理清思绪,会连贯输出,形成系列,供大家找茬。
今天说一下 Spring Boot 如何实现优雅的数据响应:统一的结果响应格式、简单的数据封装。
前提
无论系统规模大小,大部分 Spring Boot 项目是提供 Restful + json 接口,供前端或其他服务调用,格式统一规范,是程序猿彼此善待彼此的象征,也是减少联调挨骂的基本保障。
通常响应结果中需要包含业务状态码、响应描述、响应时间戳、响应内容,比如:
{
"code": 200,
"desc": "查询成功",
"timestamp": "2020-08-12 14:37:11",
"data": {
"uid": "1597242780874",
"name": "测试 1"
}
}
对于业务状态码分为两个派系:一个是推荐使用 HTTP 响应码作为接口业务返回;另一种是 HTTP 响应码全部返回 200,在响应体中通过单独的字段表示响应状态。两种方式各有优劣,个人推荐使用第二种,因为很多 Web 服务器对 HTTP 状态码有拦截处理功能,而且状态码数量有限,不够灵活。比如返回 200 表示接口处理成功且正常响应,现在需要有一个状态码表示接口处理成功且正常响应,但是请求数据状态不对,可以返回 2001 表示。
自定义响应体
定义一个数据响应体是返回统一响应格式的第一步,无论接口正常返回,还是发生异常,返回给调用方的结构格式都应该不变。给出一个示例:
@ApiModel
@Data
public class Response<T> {
@ApiModelProperty(value = "返回码", example = "200")
private Integer code;
@ApiModelProperty(value = "返回码描述", example = "ok")
private String desc;
@ApiModelProperty(value = "响应时间戳", example = "2020-08-12 14:37:11")
private Date timestamp = new Date();
@ApiModelProperty(value = "返回结果")
private T data;
}
这样,只要在 Controller 的方法返回Response
就可以了,接口响应就一致了,但是这样会形成很多格式固定的代码模板,比如下面这种写法:
@RequestMapping("hello1")
public Response<String> hello1() {
final Response<String> response = new Response<>();
response.setCode(200);
response.setDesc("返回成功");
response.setData("Hello, World!");
return response;
}
调用接口响应结果为:
{
"code": 200,
"desc": "返回成功",
"timestamp": "2020-08-12 14:37:11",
"data": "Hello, World!"
}
这种重复且没有技术含量的代码,怎么能配得上程序猿这种优(lan)雅(duo)的生物呢?最好能在返回响应结果的前提下,减去那些重复的代码,比如:
@RequestMapping("hello2")
public String hello2() {
return "Hello, World!";
}
这就需要借助 Spring 提供的ResponseBodyAdvice
来实现了。
全局处理响应数据
先上代码:
/**
* <br>created at 2020/8/12
*
* @author www.howardliu.cn
* @since 1.0.0
*/
@RestControllerAdvice
public class ResultResponseAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(final MethodParameter returnType, final Class<? extends HttpMessageConverter<?>> converterType) {
return !returnType.getGenericParameterType().equals(Response.class);// 1
}
@Override
public Object beforeBodyWrite(final Object body, final MethodParameter returnType, final MediaType selectedContentType,
final Class<? extends HttpMessageConverter<?>> selectedConverterType,
final ServerHttpRequest request, final ServerHttpResponse response) {
if (body == null || body instanceof Response) {
return body;
}
final Response<Object> result = new Response<>();
result.setCode(200);
result.setDesc("查询成功");
result.setData(body);
if (returnType.getGenericParameterType().equals(String.class)) {// 2
ObjectMapper objectMapper = new ObjectMapper();
try {
return objectMapper.writeValueAsString(result);
} catch (JsonProcessingException e) {
throw new RuntimeException("将 Response 对象序列化为 json 字符串时发生异常", e);
}
}
return result;
}
}
/**
* <br>created at 2020/8/12
*
* @author www.howardliu.cn
* @since 1.0.0
*/
@RestController
public class HelloWorldController {
@RequestMapping("hello2")
public String hello2() {
return "Hello, World!";
}
@RequestMapping("user1")
public User user1() {
User u = new User();
u.setUid(System.currentTimeMillis() + "");
u.setName("测试1");
return u;
}
}
上面代码是实现了 Spring ResponseBodyAdvice
类的模板方式,按照 Spring 的要求实现就行。只有两个需要特别注意的地方,也就是代码中标注 1 和 2 的地方。
首先说 1 这一行,也就是supports
方法,这个方法是校验是否需要调用beforeBodyWrite
方法的前置判断,返回true
则执行beforeBodyWrite
方法,这里根据 Controller 方法返回类型来判断是否需要执行beforeBodyWrite
,也可以一律返回true
,在后面判断是否需要进行类型转换。
然后重点说下 2 这一行,这行是坑,是大坑,如果对 Spring 结构不熟悉的,绝对会在这徘徊许久,不得妙法。
代码 2 这一行是判断Controller
的方法是否返回的是String
类型的结果,如果是,将返回的对象序列化之后返回。
这是因为Spring
对String
类型的响应类型单独处理了,使用StringHttpMessageConverter
类进行数据转换。在处理响应结果的时候,会在方法getContentLength
中计算响应体大小,其父类方法定义是protected Long getContentLength(T t, @Nullable MediaType contentType)
,而StringHttpMessageConverter
将方法重写为protected Long getContentLength(String str, @Nullable MediaType contentType)
,第一个参数是响应对象,固定写死是String
类型,如果我们强制返回Response
对象,就会报ClassCastException
。
当然,直接返回String
的场景不多,这个坑可能会在某天特殊接口中突然出现。
补充说明
上面只是展示了ResponseBodyAdvice
类最简单的应用,我们还可以实现更多的扩展使用。比如:
- 返回请求ID:这个需要与与
RequestBodyAdvice
联动,获取到请求ID后,在响应是放在响应体中; - 结果数据加密:通过
ResponseBodyAdvice
实现响应数据加密,不会侵入业务代码,而且可以通过注解方式灵活处理接口的加密等级; - 有选择的包装响应体:比如定义注解
IgnoreResponseWrap
,在不需要包装响应体的接口上定义,然后在supports
方法上判断方法的注解即可,比如:
@Override
public boolean supports(final MethodParameter returnType, final Class<? extends HttpMessageConverter<?>> converterType) {
final IgnoreResponseWrap[] declaredAnnotationsByType = returnType.getExecutable().getDeclaredAnnotationsByType(IgnoreResponseWrap.class);
return !(declaredAnnotationsByType.length > 0 || returnType.getGenericParameterType().equals(Response.class));
}
很多其他玩法就不一一列举了。
总结
上面说了正常响应的数据,只做到了一点优雅,想要完整,还需要考虑接口异常情况,总不能来个大大的try/catch/finally
包住业务逻辑吧,那也太丑了。后面会再来一篇,重点说说接口如何在出现异常时,也能返回统一的结果响应。
本文只是抛出一块砖,玉还得自己去找。
推荐阅读
- SpringBoot 实战:一招实现结果的优雅响应
- SpringBoot 实战:如何优雅的处理异常
- SpringBoot 实战:通过 BeanPostProcessor 动态注入 ID 生成器
- SpringBoot 实战:自定义 Filter 优雅获取请求参数和响应结果
- SpringBoot 实战:优雅的使用枚举参数
- SpringBoot 实战:优雅的使用枚举参数(原理篇)
- SpringBoot 实战:在 RequestBody 中优雅的使用枚举参数
- SpringBoot 实战:在 RequestBody 中优雅的使用枚举参数(原理篇)
- SpringBoot 实战:JUnit5+MockMvc+Mockito 做好单元测试
- SpringBoot 实战:加载和读取资源文件内容
- SpringBoot 实战:国际化组件 MessageSource
个人主页:https://www.howardliu.cn
个人博文:SpringBoot 实战:一招实现结果的优雅响应
CSDN 主页:http://blog.csdn.net/liuxinghao
CSDN 博文:SpringBoot 实战:一招实现结果的优雅响应