问题背景
前后端分离的项目中,后端API返回的往往是一个格式统一的JSON串,典型的如同下面的结构
{
"code": 200,
"message": "ok",
"data": null
}
这个结构是在项目中自定义的,其中的code是由后端自定义的,并不一定与http的status code相对应。通过POSTMAN等工具我们可以看到HTTP自带的status code,默认的情况下,Spring对于正常返回的请求设置是200,发生异常的情况下则设置为500。
这在大多数情况下已经够用了,但是为了规范,有时候我们也希望能设置成其它编码,例如说新建对象则是201,异步处理则是202。以及对应于用户请求有误时应该返回4XX而不是500。
浏览了网上的一些文章,目前看到的说有以下三种做法
通过@ResponseStatus注解
这个注解既可以作用到自定义异常类型上,也可以作用到对异常进行处理的ExceptionHandler上,典型的用法如下
@ExceptionHandler(value = CustomException.class)
@ResponseBody
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public BaseResponse handleException(CustomException e, HttpServletRequest request) {
//...
}
这种做法对于异常处理流程是非常方便的,比如说用户输入不正确抛出BadRequest异常,资源不存在抛出NotFoundException,无权限则抛出ForbiddenException,然后在对应的ExceptionHandler上标注对应的返回码。
然后对于普通的正常返回比如201,202这种,则需要标注到普通的controller方法上(注:这个注解一般都是用到ExceptionHandler里面,经过测试以后发现也可以作用到普通controller方法上指定正常返回的ResponseStatus),对代码有一定的侵入性,但是整体而言还是比较方便且通用的方法。
修改status的Response属性
示例如下
public void testPost(HttpServletResponse response) throws IOException {
response.setStatus(201);
response.getWriter().append("created.");
}
由于这个方法限定controller方法的返回值必须为void,有诸多不便,极少使用
返回ResponseEntity类型
ResponseEntity是Spring比较建议的做法,在上面的ResponseStatus注解的方案中,ResponseStatus注解的注释中,Spring也展示了更加推荐用ResponseEntity的方式。
如果使用ResponseEntity,则需要将controller方法中返回的类型改成ResponseEntity,或者将自定义的BaseResponse定义为ResponseEntity的子类
示例如下:
public ResponseEntity<User> testCreateUser() {
User = new User();
return new ResponseEntity(user, HttpStatus.CREATED);
}
或者将BaseResponse定义成子类
@Data
public class Message{
int code;
String message;
Object result;
}
public class BaseResponse extends ResponseEntity<Message> {
public BaseResponse(HttpStatus status) { super(status); }
// ...其它方法
}
这种方法如果在立项之初就规定使用ResponseEntity还好,但是对于已经稳定的定义好了BaseResponse的项目,对每个方法进行替换太过麻烦,还有一个很大的不足是,项目直接结合swagger使用时,原本只需要在BaseResponse的各个字段中添加@ApiModelProperty注解,swagger可以自动生成文档来描述返回的类型。但是通过ResponseEntity返回时无法生成对应的返回值文档。
上面三个方法是在网上找到的常见做法,但是都有一点缺点导致不是特别完美。于是我尝试通过对controller的返回值进行拦截,发现一样可以达到目的。
通过拦截器修改
Spring开放了ResponseBodyAdvice接口,来允许对ResponseBody进行拦截和访问。其中supports方法用来指定是否需要进行拦截,beforeBodyWrite方法用来执行真正的处理逻辑。
那么要做的事情就很简单了,在beforeBodyWrite中,读取返回值并设置对应的Http code。 示例如下:
@ControllerAdvice
public class HttpStatusCodeAdvice implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return returnType.getParameterType().isAssignableFrom(BaseResponse.class);
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType,
MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request,
ServerHttpResponse response) {
if (body != null) {
int code = ((BaseResponse) body).getCode();
response.setStatusCode(code);
}
return body;
}
}
只需要添加这个拦截器,对于原本的代码一点都不需要改动,运行验证成功
还有什么其它的较好的做法,欢迎在评论区留言讨论~