本文将介绍如何在 SpringBoot 中优雅地统一响应处理。
一、为什么要统一响应? 在前后端分离的开发模式中,响应的结构统一能有效地降低前后端的沟通成本,有利于开发。
二、响应体格式 响应体普遍包含以下三个部分:
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
的接口” 方法执行结束且响应未发送之前自定义其响应内容。
实现类应该在 RequestMappingHandlerAdapter
和 ExceptionHandlerExceptionResolver
中注册,或者直接使用 @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) { ··· 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) { ··· 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) { if (body instanceof Map && ((Map<?, ?>) body).get("status" ).equals(404 )) { return body; } 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); } }
参考