Spring Boot 统一响应处理

本文将介绍如何在 SpringBoot 中优雅地统一响应处理。

一、为什么要统一响应?

在前后端分离的开发模式中,响应的结构统一能有效地降低前后端的沟通成本,有利于开发。

二、响应体格式

响应体普遍包含以下三个部分:

  • code:状态码
  • msg:描述
  • data:数据
1
2
3
4
5
{
"code": "",
"msg": "",
"data": ""
}

三、状态码之争

关于响应的状态码,有两派观点:

  • 辩方 1:只使用 HTTP 状态码,原因如下:
    • 自定义状态码相当于抛弃了 HTTP 状态码,抛弃了普遍共识
    • 额外增加状态码不符合 RESTful 规范
  • 辩方 2:使用 HTTP 状态码,同时在响应体中额外增加自定义状态码,原因如下:
    • HTTP 状态码不够用
    • HTTP 状态码反映网络状态;自定义状态码反映业务状态
    • HTTP 状态码非 200 的响应可能被(运营商、浏览器、DNS)挟持,统一为 200 能够减少很多麻烦

个人观点:在网路无问题的前提下,HTTP 状态码固定为 200,业务执行状态通过自定义的 code 和 msg 标识。

四、响应体的定义

响应体:

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
public class ResultVO {

/**
* 状态码
*/
private final int code;

/**
* 描述
*/
private final String msg;

/**
* 数据
*/
private final Object data;

public static ResultVO success() {
return new ResultVO(ResultCode.SUCCESS, null);
}

public static ResultVO success(Object data) {
return new ResultVO(ResultCode.SUCCESS, data);
}


public static ResultVO fail() {
return new ResultVO(ResultCode.FAILED, null);
}

public static ResultVO fail(String msg) {
return new ResultVO(ResultCode.FAILED.getCode(), msg, null);
}

public static ResultVO fail(ResultCode resultCode) {
return new ResultVO(resultCode, null);
}

private ResultVO(ResultCode resultCode, Object data) {
this.code = resultCode.getCode();
this.msg = resultCode.getMsg();
this.data = data;
}

private ResultVO(int code, String msg, Object data) {
this.code = code;
this.msg = msg;
this.data = data;
}
}

状态码及描述的枚举类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public enum ResultCode {

SUCCESS(0, "操作成功"),

FAILED(1000, "操作失败"),

VALIDATE_FAILED(2000, "参数错误"),

UNAUTHORIZED(9100, "未登录"),

FORBIDDEN(9200, "未授权");

/**
* 状态码
*/
private final int code;

/**
* 描述
*/
private final String msg;
}

五、响应体的简单使用

1
2
3
4
5
6
7
8
9
@RestController
public class HelloController {

@GetMapping("/hello")
public ResultVO hello() {
return ResultVO.success("hello");
}

}

六、优雅地统一响应体

上面的做法存在一个弊端:在每一个接口中手动实例化响应体的操作过于繁琐。

下面介绍一个更加优雅的方式。

1. 说明 - 接口增强类

@ControllerAdvice 用于注解 “接口增强类”,”接口增强类” 的作用是在全局范围内对接口类进行增强

@RestControllerAdvice@ControllerAdvice 的基础上增加了 @ResponseBody,使它用于注解 “Rest 接口增强类”。

2. 说明 - ResponseBodyAdvice

Allows customizing the response after the execution of an @ResponseBody or a ResponseEntity controller method but before the body is written with an HttpMessageConverter.
Implementations may be registered directly with RequestMappingHandlerAdapter and ExceptionHandlerExceptionResolver or more likely annotated with @ControllerAdvice in which case they will be auto-detected by both.

允许在 “带有 @ResponseBody@ResponseEntity 的接口” 方法执行结束且响应未发送之前自定义其响应内容。

实现类应该在 RequestMappingHandlerAdapterExceptionHandlerExceptionResolver 中注册,或者直接使用 @ControllerAdvic 注解

ResponseBodyAdvice 用于增加 ResponseBody,对 Rest 接口返回的结果做加工处理。

我们可以实现 ResponseBodyAdvice 接口,并使用 @RestControllerAdvice,从而实现全局的接口返回结果处理。

3. 不完善的响应增强类

1
2
3
4
5
6
7
8
9
10
11
12
@RestControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}

@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
return new ResultVO(body);
}
}

4. String 的特殊处理

如果使用上述的代码,会发现在接口返回类型为 String 时会发生报错。

具体原因可以看:

spring mvc配置ResponseBodyAdvice后返回String报错-CSDN博客

解决办法是:

当返回结果为 String 类型时,生成统一返回体,再将统一返回体转换为 JSON,将 JSON 返回。

这样就可以再不改变返回类型的情况下 “包装” 返回结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {

···

// 若原返回结果为String,需要生成响应体、转换为JSON,再返回
if (body instanceof String || String.class.equals(returnType.getGenericParameterType())) {
ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
try {
return mapper.writeValueAsString(ResultVO.success(body));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}

···

}

5. 响应体的特殊处理

响应增强类可能会遇到响应结果本身就是响应体的情况,原因有:

  • 有时候,我们需要在接口方法中手动包装响应体
  • 如果定义了全局异常处理,则经过异常处理的结果本身就是响应体

此时,如果再进行处理,将会发生重复包装。

1
2
3
4
5
6
7
8
9
{
"code": "",
"msg": "",
"data": {
"code": "",
"msg": "",
"data": ""
}
}

因此,我们需要进行额外判断,当结果已经是响应体时不对其做加工。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {

···

// 如果已经包装为响应体,直接返回
if (body instanceof ResultVO) {
return body;
}

···

}

6. 404 的特殊处理

如果访问的资源不存在,Spring Boot 默认会返回 404 信息。

在实际开发中,我们会发现 “全局异常处理类” 并不能拦截 404,但 “全局响应处理类” 能够拦截,从而导致 404 响应被错误包装,如下:

此时有两种做法:

  • 使用额外的方式拦截 404
  • 不拦截 404 ,使两个处理类都对它放行

这里我选择的是第二种做法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {

···

// 放行404响应
if (body instanceof Map && ((Map<?, ?>) body).get("status").equals(404)) {
return body;
}

···

}

7. 完善的响应增强类

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
@RestControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}

@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
// 放行404响应
if (body instanceof Map && ((Map<?, ?>) body).get("status").equals(404)) {
return body;
}
// 若原返回结果为String,需要生成响应体、转换为JSON,再返回
if (body instanceof String || String.class.equals(returnType.getGenericParameterType())) {
ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
try {
return mapper.writeValueAsString(ResultVO.success(body));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
// 如果已经包装为响应体,直接返回
if (body instanceof ResultVO) {
return body;
}
return ResultVO.success(body);
}
}

参考