SpringBoot自定义返回Http code

3,109 阅读3分钟

问题背景

前后端分离的项目中,后端API返回的往往是一个格式统一的JSON串,典型的如同下面的结构

{
    "code": 200,
    "message": "ok",
    "data": null
}

这个结构是在项目中自定义的,其中的code是由后端自定义的,并不一定与http的status code相对应。通过POSTMAN等工具我们可以看到HTTP自带的status code,默认的情况下,Spring对于正常返回的请求设置是200,发生异常的情况下则设置为500。

image.png

这在大多数情况下已经够用了,但是为了规范,有时候我们也希望能设置成其它编码,例如说新建对象则是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;
    }
}

只需要添加这个拦截器,对于原本的代码一点都不需要改动,运行验证成功

image.png

还有什么其它的较好的做法,欢迎在评论区留言讨论~