统一返回格式

身为一名后端开发者,少不了前后端的信息交互,这个时候如果你传给前端的信息乱七八糟,往往少不了挨一顿揍。所以统一一下返回格式还是很有必要的。有点经验的开发者一般都会养成这个习惯,所以今天这篇文章的目的并不在于讲解如何统一返回信息。现在有这么一个问题,你已经封装好了统一的返回信息,按照标准给前端返回了code,msg和data,但是你每次写接口都要重新写一遍生成这个对象,再包装,让代码变得十分冗余并且不美观。接下来就记录我个人在自己的项目上对这个现象进行优化的过程。


@RestControllerAdvice注解

首先他是一个组合注解,由@ControllerAdvice、@ResponseBody组成,而@ControllerAdvice继承了@Component。个人理解他的作用是,在Controller层return完数据,在最终返回给前端信息前对数据进行拦截并自定义处理。看一下ResponseBodyAdvice的接口源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface ResponseBodyAdvice<T> {
/**
* 是否开启advice功能,可以自行决定哪些类需要开启,只需要从returnType中获取方法名和
* 类名,判断是否和你指定的相等即可。
*/
boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);

/**
* 自定义对返回数据的处理
*/
@Nullable
T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response);
}

接口源码很简单,接下来就是自己写一个实现类去实现这个接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RestControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice<Object> {

@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}

@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
// 后面会提到这个判断的作用
if (body instanceof ResponseResult){
return body;
}
// 把返回的数据封装,success返回一个带有状态码、消息、数据的对象。
return ResponseResult.success(body);
}
}

因为我个人项目是微服务的架构,所以这个实现类配置在公共库library中,还要记得再具体启动类上配置一下扫描路径,单体应用可以略过这个步骤

1
2
3
4
/**
* 在application启动类上加一个组件扫描路径
*/
@ComponentScan(basePackages = {"com.xxx.xxx","com.xxx.library"})

这样就已经配置好了,写一个接口测试一下

1
2
3
4
5
6
7
8
@RestControllerAdvice
@RequestMapping("/user")
public class UserController {
@RequestMapping(value = "/exposure/test",method = RequestMethod.POST)
public Integer dele() throws ExceptionVo {
return 2;
}
}

测试结果

1
2
3
4
5
{
"code": 200,
"msg": "SUCCESS",
"data": 2
}

这样我们就再也不用不断创建ResponseResult啦,省去了很多代码。

然而别高兴的太早,我们调用的方法一旦出现异常,返回的信息就会变成这样。

1
2
3
4
5
6
7
8
9
10
{
"code": 200,
"msg": "SUCCESS",
"data": {
"timestamp": 1668752852033,
"status": 500,
"error": "Internal Server Error",
"path": "/user/exposure/test"
}
}

这下又会被前端揍了,所以我们需要做点措施,以防挨揍。


全局异常捕获与处理

你也很烦每次涉及到数据库什么操作都要加个try-catch吧?我的评价是不如直接抛出异常让全局异常处理器去干这个活。他还有个好处,try-catch捕获不了的异常,它也能捕获,例如参数注解校验上报的错。按照我的想法,我想直接抛出错误枚举类(就是你自己定义的状态码+信息)中的返回信息,那么就要自定义异常,再全局捕获这个异常,再直接返回异常信息。配置步骤如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 自定义异常
*/
public class ExceptionVo extends RuntimeException{

/**
* 枚举类
*/
private RespBeanEnum e;

public ExceptionVo() {
super();
}

public ExceptionVo(RespBeanEnum res) {
super(res.getMessage());
this.e = res;
}

public RespBeanEnum getE() {
return e;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 全局异常处理器
*/
@Slf4j
@RestControllerAdvice
public class RestExceptionHandler {
/**
* 全局捕获自定义异常
*/
@ExceptionHandler(ExceptionVo.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResponseResult exception(ExceptionVo e) {
log.error("运行时异常:{}", e.getE().getMessage(), e);
// 直接返回封装好的请求体
return ResponseResult.error(e.getE());
}
}

测试时间到!

1
2
3
4
5
6
7
8
@RestControllerAdvice
@RequestMapping("/user")
public class UserController {
@RequestMapping(value = "/exposure/test",method = RequestMethod.POST)
public Integer dele() throws ExceptionVo {
throw new ExceptionVo(RespBeanEnum.DELETE_ERROR);
}
}

这个时候,之前这部分的代码就发挥作用了

1
2
3
if (body instanceof ResponseResult){
return body;
}

没有这部分代码的话,结果会是这样

1
2
3
4
5
6
7
8
{
"code": 200,
"msg": "SUCCESS",
"data": {
"code": 3314,
"msg": "删除出错"
}
}

加上这部分,才会是正确的结果,具体为什么很简单。在异常处理器里你已经返回了一个ResponseResult对象,不加这句话,会在你定义的ResponseAdvice里再封装一次,因此判断一下body是不是已经是ResponseResult,是就直接返回。

正确结果

1
2
3
4
{
"code": 3314,
"msg": "删除出错"
}

到这里,项目上必要的优化配置已经结束了,接下来就是在合适的地方写就好了。如果有任何错误的地方欢迎联系我指出。