spring boot之 统一请求返回

1,478 阅读3分钟

在开发中,我经常会因为在制定接口返回数据格式而忧愁,也经历过从混乱的每个接口返回数据格式都不同,到同一一个类的过程。

在这里记录一下,如果简单快捷的统一咱们接口请求的返回数据格式

首先却要确定是的返回的数据格式

{
    "success":true,
    "code":200,
    "data":{
        "name":"orange",
        "age":18
    },
    "message":null
}
  • success 代表此次请求是否成功
  • data 代表此次请求返回的具体数据,可以是任意类型的,例如:对象,列表等
  • code 状态码来表示此次请求的具体结果,例如200表示操作成功,非200表示操作失败,可以与前端来共同定义维护code,用来做不同的逻辑判断,例如:code 1001 账号密码错误等
  • message 当此次请求为失败时用于展示错误信息

在定义code的枚举

/**
 * 返回响应码
 * @author orange
 */
public enum ApiCodeEnum {

    /**
     * 成功
     */
    SUCCESS(2000,"ok"),
    PARAM_ERROR(3001,"参数错误"),
    SERVER_ERROR(5000,"系统错误");
    ....
    
    @Getter
    Integer code;
    @Getter
    String msg;

    ApiCodeEnum(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}

所有关于业务逻辑的状态码code及对应message都可以通过定义apicode的枚举来确定,这样虽然用起来比较繁琐,但是对于日积月累的code种类来说方便统一管理。

创建统一返回类

/**
 * 接口统一返回实体类
 *
 * @param <T>
 * @author orange
 */
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class APIResponse<T> {
    private Boolean success;
    private T data;
    private Integer code;
    private String message;

    public static APIResponse<String> error(HttpCodeEnum httpCodeEnum) {
        return new APIResponse<>(false, null, httpCodeEnum.getCode(), httpCodeEnum.getMsg());
    }

    public static APIResponse<String> error(Integer code, String msg) {
        return new APIResponse<>(false, null, code, msg);
    }

    public void setSuccessCode(HttpCodeEnum httpCodeEnum) {
        this.code = httpCodeEnum.getCode();
        this.message = httpCodeEnum.getMsg();
        this.success = true;
    }
    public void setFalseCode(HttpCodeEnum httpCodeEnum) {
        this.code = httpCodeEnum.getCode();
        this.message = httpCodeEnum.getMsg();
        this.success = false;
    }
}

实际使用

只需要在接口方法中直接返回该实体类即可

@GetMapping("/user/{id}")
public APIResponse testResponse(@PathVariable Long id){
    return APIResponse.builder(userService.findUserById(id)).success(true).code(ApiCode.SUCCESS.getCode()).message(ApiCode.SUCCESS.getValues()).build();
}

这样就统一了接口的返回数据格式。返回结果为:

{
    "success":true,
    "code":200,
    "data":{
        "name":"orange",
        "age":18,
        "gender":"male"
    },
    "message":null
}

这是这样的话还是需要在每个接口中去用返回实体类包装,就会产生大量重复的代码及繁琐的包装过程,有没有没法简化这一步骤呢?肯定是有的。

@RestControllerAdvice
@Slf4j
@SuppressWarnings("rawtypes")
public class APIResponseAdvice implements ResponseBodyAdvice<Object> {

  

    /**
     * 自动处理APIException,包装为APIResponse
     */
    @ExceptionHandler(APIException.class)
    public APIResponse handleApiException(HttpServletRequest request, APIException ex) {
        log.error("process url {} failed", request.getRequestURL().toString(), ex);
        return APIResponse.builder().success(false).code(ex.getErrorCode()).message(ex.getErrorMessage()).build();
    }

    /**
     * 自动处理APIException,包装为APIResponse
     */
    @ExceptionHandler(LockedAccountException.class)
    public APIResponse handleLockedAccountException(HttpServletRequest request, APIException ex) {
        log.error("process url {} failed", request.getRequestURL().toString(), ex);
        return APIResponse.builder().success(false).code(HttpCodeEnum.ACCOUNT_LOCKED.getCode()).message(ex.getErrorMessage()).build();
    }


    /**
     * 仅当方法或类没有标记@NoAPIResponse才自动包装
     */
    @Override
    public boolean supports(MethodParameter returnType, @NotNull Class converterType) {
        return returnType.getParameterType() != APIResponse.class &&
                AnnotationUtils.findAnnotation(Objects.requireNonNull(returnType.getMethod()), NoAPIResponse.class) == null
                && AnnotationUtils.findAnnotation(returnType.getDeclaringClass(), NoAPIResponse.class) == null;
    }

    /**
     * 自动包装外层APIResposne响应
     */
    @Override
    public Object beforeBodyWrite(Object body, @NotNull MethodParameter returnType, @NotNull MediaType selectedContentType, @NotNull Class<? extends HttpMessageConverter<?>> selectedConverterType, @NotNull ServerHttpRequest request, @NotNull ServerHttpResponse response) {
        return APIResponse.builder().success(true).data(body).code(HttpCodeEnum.SUCCESS.getCode()).message(HttpCodeEnum.SUCCESS.getMsg()).build();
    }
}

只需要实现ResponseBodyAdvice这个接口同时加上@RestControllerAdvice的注解 @RestControllerAdvice 是一个控制增强器 是@ControllerAdvice + @ResponseBody的结合,主要是用来增强controller的。 在ResponseBodyAdvice中有两个方法 一个 supports 用来判断自动包装响应实体的条件,一个是具体包装响应实体的逻辑。 如果我们不需要某个接口的返回值被包装一层的话。我这里使用的是注解的方式。回头看下上图。在supports方法中,如果接口方法被@NoAPIResponse注解标注,那么这个方法就直接返回。跳过下面的beforeBodyWrite方法。

这个实现也很简单 首先定义这个注解

/**
 * 自动包装接口返回值注解
 * 标记该注解即返回值不会进行包装
 * @author orange
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface NoAPIResponse {

}

然后在使用中用这个注解标注方法即可

@GetMapping("/user/{id}")
@NoAPIResponse
public UserVo testResponse(@PathVariable Long id){
    return userService.findUserById(id);
}

这个时候的返回结果就是:

    {
        "name":"orange",
        "age":18,
        "gender":"male"
    }