SpringBoot 封装接口返回数据的统一结构

74 阅读7分钟

1. 基础代码

1.1. 封装代码

代码使用了Lombok注解。

首先提供一个枚举,用于封装返回的提示码和提示信息。

package com.example.demo.common.result;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * @author wangbo
 * @date 2021/05/12
 */
@Getter
@AllArgsConstructor
public enum ResultCode {

    //成功提示码
    SUCCESS(20000, "成功"),

    //自定义失败信息
    FAILURE(50000, "失败"),

    //通用错误码 50001~50099
    PROGRAM_INSIDE_EXCEPTION(50001, "程序内部异常"),
    REQUEST_PARAM_ERROR(50002, "请求参数错误");

    //用户模块错误码 50100~50199
    //商品模块错误码 50200~50299
    //订单模块错误码 50300~50399

    private final Integer code;
    private final String message;
}

接下来提供一个返回类型,这里使用了泛型,用于对返回数据进行统一包装。

package com.example.demo.common.result;

import lombok.Data;

/**
 * @author wangbo
 * @date 2021/05/12
 */
@Data
public class Result<T> {

    private Integer code;

    private String message;

    private T data;

    /**
     * 成功
     */
    public static Result<Void> success() {
        Result<Void> result = new Result<>();
        result.setCode(ResultCode.SUCCESS.getCode());
        result.setMessage(ResultCode.SUCCESS.getMessage());
        return result;
    }

    /**
     * 成功,有返回数据
     */
    public static <V> Result<V> success(V data) {
        Result<V> result = new Result<>();
        result.code = ResultCode.SUCCESS.getCode();
        result.message = ResultCode.SUCCESS.getMessage();
        result.data = data;
        return result;
    }

    /**
     * 失败
     */
    public static Result<Void> failure() {
        Result<Void> result = new Result<>();
        result.setCode(ResultCode.FAILURE.getCode());
        result.setMessage(ResultCode.FAILURE.getMessage());
        return result;
    }

    /**
     * 失败,自定义失败信息
     */
    public static Result<Void> failure(String message) {
        Result<Void> result = new Result<>();
        result.setCode(ResultCode.FAILURE.getCode());
        result.setMessage(message);
        return result;
    }

    /**
     * 失败,使用已定义枚举
     */
    public static Result<Void> failure(ResultCode resultCode) {
        Result<Void> result = new Result<>();
        result.setCode(resultCode.getCode());
        result.setMessage(resultCode.getMessage());
        return result;
    }
}

1.2. 使用示例

package com.example.demo.controller;

import com.example.demo.entity.User;
import com.example.demo.result.Result;
import com.example.demo.result.ResultCode;
import com.example.demo.service.UserService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * @auther wangbo
 * @date 2021-01-10 17:48
 */
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/list")
    public Result<List<User>> list(){
        List<User> list = userService.list();
        return Result.success(list);
    }

    @GetMapping("/success")
    public Result<Void> success(){
        return Result.success();
    }

    @GetMapping("/failure")
    public Result<Void> failure(){
        return Result.failure("测试自定义失败信息");
    }

    @GetMapping("/failure1")
    public Result<Void> failure1(){
        return Result.failure(ResultCode.REQUEST_PARAM_ERROR);
    }
}

1.3. 结果示例

http://localhost:8080/user/list

{
    "code":20000,
    "message":"成功",
    "data":[
        {
            "id":1,
            "name":"张三",
            "age":32,
            "email":"jxd@baomidou",
            "managerId":3,
            "createTime":"2020-11-04T19:04:22",
            "remark":null
        },
        {
            "id":2,
            "name":"李四",
            "age":32,
            "email":"hb@baomidou",
            "managerId":3,
            "createTime":"2020-11-04T19:18:14",
            "remark":null
        },
        {
            "id":3,
            "name":"王五",
            "age":32,
            "email":"@baomidou",
            "managerId":2,
            "createTime":"2020-11-04T19:20:12",
            "remark":null
        }
    ]
}

http://localhost:8080/user/success

{
    "code":20000,
    "message":"成功",
    "data":null
}

http://localhost:8080/user/failure

{
    "code":50000,
    "message":"测试自定义失败信息",
    "data":null
}

http://localhost:8080/user/failure1

{
    "code":50002,
    "message":"请求参数错误",
    "data":null
}

2. 代码优化

对于上面的使用示例,可以继续做如下修改:

  1. 对于没有返回数据的接口可以继续使用上面的写法,比如增加,删除,更新接口,统一包装类不会再进行重复包装处理。
  2. 对于有返回数据的接口,比如查询接口,可以把返回值进行统一包装处理,这样每个查询接口返回的就是业务对象,比如List<User>,而不包装对象Result<T>了。统一包装处理会把这些接口的返回值包装为Result<T>格式。
  3. 对于一些第三方回调之类的接口,可能要求我们的接口响应格式符合第三方的规定,所以这些接口不需要进行统一包装,我们可以通过在这些接口上添加一个自定义注解来绕过统一包装类的处理。

注意:如果你的项目使用Swagger的话,其实这样修改会让生成的接口文档中的响应格式不完整。比如对于本文中的/user/list查询接口,文档中显示的返回结果将会是List<User>格式,而不是Result<List<User>>格式。

这种修改将代码复杂化了,非必要情况下不需要这么做!

2.1. 返回值包装

这里使用到了@RestControllerAdvice注解,该注解既可以全局拦截异常也可拦截正常的返回值,并且可以对返回值进行修改。添加一个接口返回值包装类:

package com.example.demo.advice;

import com.example.demo.annotation.NotResultWrap;
import com.example.demo.result.Result;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

/**
 * 接口返回值包装
 * @auther wangbo
 * @date 2021-01-13 15:55
 */
@Slf4j
@RestControllerAdvice(basePackages = "com.example.demo.controller")
public class ResponseControllerAdvice implements ResponseBodyAdvice<Object> {

    /**
     * 判断哪些接口需要进行返回值包装
     * 返回 true 才会执行 beforeBodyWrite 方法;返回 false 则不执行。
     */
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        log.info("判断是否需要进行返回值包装");
        //如果接口方法返回 Result 不需要再次包装
        //如果接口方法使用了 @NotResultWrap 注解,表示不需要包装了
        //只对成功的请求进行返回包装,异常情况统一放在全局异常中进行处理
        return !(returnType.getParameterType().equals(Result.class)
                || returnType.hasMethodAnnotation(NotResultWrap.class));
    }

    /**
     * 进行接口返回值包装
     */
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  ServerHttpRequest request, ServerHttpResponse response) {
        log.info("进行返回值包装");
        Result<Object> result = new Result<>();
        //String 类型不能直接包装,需要进行特殊处理
        if (returnType.getParameterType().equals(String.class)){
            ObjectMapper objectMapper = new ObjectMapper();
            try {
                //使用 jackson 将返回数据转换为 json
                return objectMapper.writeValueAsString(result.success(body));
            } catch (JsonProcessingException e) {
                //这里会走统一异常处理
                throw new RuntimeException("String类型返回值包装异常");
            }
        }
        return result.success(body);
    }
}

写这个类的时候有以下几点注意事项:

(1) @RestControllerAdvice(basePackages = "com.example.demo.controller")该注解必须写basePackages属性,用来指定该返回值包装类所处理的接口包。如果不指定这个属性的话,该包装类会拦截所有的响应。比如404之类的响应,不属于系统异常,不会被全局异常处理类处理,但是会被返回值包装类拦截,并且会被包装,但是这些异常响应其实是不需要也不应该进行包装的。

未指定basePackages属性,404的响应,明显是不合理的:

{
  "code": 20000,
  "message": "成功",
  "data": {
    "timestamp": "2021-01-17T10:42:04.406+00:00",
    "status": 404,
    "error": "Not Found",
    "message": "",
    "path": "/user_info/add3"
  }
}

指定了basePackages属性,404的响应:

{
  "timestamp": "2021-01-17T10:26:00.865+00:00",
  "status": 404,
  "error": "Not Found",
  "message": "",
  "path": "/user_info/add3"
}

(2)对于返回值为String的接口,需要单独处理,如果没有进行单独处理,程序会抛出一个类转换异常。

SpringMVC内部定义了九个不同的MessageConverter用来处理不同的返回值。在AbstractMessageConverterMethodProcessor类下面的writeWithMessageConverters方法可以看出来,每个MessageConverer是根据返回类型和媒体类型来选择处理的MessageConverter的。

Controller层中返回的类型是String,但是在ResponseBodyAdvice实现类中,我们把响应的类型修改成了Result。这就导致了Spring在处理MessageConverter的时候,依旧根据之前的String类型选择对应String类型的StringMessageConverter。而在StringMessageConverter类型,它只接受String类型的返回值,所以我们在ResponseBodyAdvice中将返回值从String类型改成Result类型之后,调用StringMessageConverter方法发生类型强转。Result无法转换成String,发生类型转换异常。所以需要单独处理。

当然,也可以选择对String返回类型的接口不进行包装处理,直接返回原生String,这种直接在supports方法中进行拦截就可以了。

2.2. 自定义枚举

用于标志接口方法不需要进行统一返回值包装的自定义注解:

package com.example.demo.annotation;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * 不进行返回值包装注解
 * @auther wangbo
 * @date 2021-01-13 20:55
 */
@Target({METHOD})
@Retention(RUNTIME)
public @interface NotResultWrap {
}

2.3. 接口修改示例

接口方法可以修改如下,前四个接口的返回结果和上面的示例是一样的:

建议:有返回数据的走包装,无数据返回的直接返回Result,提高性能。

package com.example.demo.controller;

import com.example.demo.annotation.NotResultWrap;
import com.example.demo.entity.User;
import com.example.demo.result.Result;
import com.example.demo.result.ResultCode;
import com.example.demo.service.UserService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * @auther wangbo
 * @date 2021-01-10 17:48
 */
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;
	
    //只有这个方法进行了返回值改变,去掉了外层的Result包装,在接口返回值包装类中进行统一包装
    @GetMapping("/list")
    public List<User> list(){
        return userService.list();
    }

    @GetMapping("/success")
    public Result<Void> success(){
        return Result.success();
    }

    @GetMapping("/failure")
    public Result<Void> failure(){
        return Result.failure("测试自定义失败信息");
    }

    @GetMapping("/failure1")
    public Result<Void> failure1(){
        return Result.failure(ResultCode.REQUEST_PARAM_ERROR);
    }

    //测试字符串的包装
    @GetMapping("/string")
    public String string(){
        return "测试字符串包装";
    }

    //测试不进行返回值包装注解的效果
    @NotResultWrap
    @GetMapping("/not_wrap")
    public String notWrap(){
        return "测试不包装注解";
    }
}

后面新加的两个接口的返回结果示例:

http://localhost:8080/user/string

这个接口返回的其实是String格式,只不过这是个JSON格式的字符串。

{"code":20000,"message":"成功","data":"测试字符串包装"}

http://localhost:8080/user/not_wrap

测试不包装注解