SpringBoot 学习笔记:设计统一的 API 接口格式

发布于 7 天前  40 次阅读


在使用 SpringBoot 开发 Web 项目的过程中,肯定会有前端与后端数据交互的情况,为了避免前端开发人员与后端开发人员因为接口规范而掐架,设计一套统一的接口格式对于维持开发团队的氛围来说是必不可少的(狗头保命 :P)。

SpringBoot 在处理 RestController 处理方法的返回值时,会自动使用 HttpMessageConverter 序列化对象,因此我们可以设计一个 POJO 对象,来方便我们统一接口格式,返回数据给前端。

设计并编写 API 数据响应对象

首先设计一下 API 数据响应对象的格式,可以根据实际业务需要进行设计,不用照抄。

选择 API 数据的返回格式

由于使用接口的大多数是 JavaScript,使用 JSON 是最佳选择,这里我选择使用 Gson 作为 HttpMessageConverter,同时将 API 格式设计成这样:

{
  // API 调用结果的错误码, 0 为成功, 负数为异常代码, 正数保留暂不使用.
  "code": 0,
  // 对错误码的详细解释, 一个错误码对应一个错误信息(可以是模板, 用模板填充详情).
  "message": "Success.",
  // API 响应结果, 可能的类型为: JsonArray, JsonObject, JsonNull, 不直接提供 Number/String 的原因是因为直接提供值可能会产生误解.
  "data": null
}

设计完 API 数据的统一返回格式后,就要设计一个 POJO 类,由于使用 Gson 等 JSON 对象序列化库,我们可以直接将 API 通用字段写成类成员变量。

设计 API 统一格式的对应 POJO 类


public class ApiResult<T> {

    /**
     * Http 响应状态码.
     */
    private final transient int httpStatus;

    /**
     * 业务错误代码.
     */
    private final int code;

    /**
     * 业务错误信息.
     */
    private final String message;

    /**
     * 接口调用返回数据(可能为 null).
     */
    private final T data;

    public ApiResult(int code, String message, T data) {
        this(code == 0 ? 200 : 400, code, message, data);
    }

    public ApiResult(int httpStatus, int code, String message, T data) {
        this.httpStatus = httpStatus;
        this.code = code;
        this.message = message;
        this.data = data;
    }

    public int getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }

    public T getData() {
        return data;
    }

    public int getHttpStatus() {
        return httpStatus;
    }
}

除了 API 通用字段外,我另外加入了一个 httpStatus 字段,因为我计划为 Http 响应指定响应状态,如果打算统一返回 HTTP 200 的话,可以不加入这个字段。

补充一个小知识,在 httpStatus 字段中,我加入了一个 transient 关键字,这个关键字声明了该属性不参与序列化,Gson 是认这个关键字的,因此在 Gson 序列化过程中,会绕过 httpStatus 属性。

设计完 API 统一返回格式和 POJO 类后,就开始让 SpringBoot 适配这个格式。

配置 SpringBoot

更换 HttpMessageConverter

SpringBoot 默认使用 Jackson 作为 JSON 的序列化器,如果有需要,也可以换成 Gson。

新建一个类,实现 WebMvcConfigurer 接口(不少老教程介绍实现 WebMvcConfigurerAdapter,但 Java 更新后,接口类也能有默认方法了,因此新版 SpringBoot 中直接实现 WebMvcConfigurer 就行),如果已经有实现类了,可以不用重复实现。

创建实现类后,覆盖 configureMessageConverters 方法,将 HttpMessageConverter 对象加入到 converters 中即可。

@Configuration
public class WebConfiguration implements WebMvcConfigurer {

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(new GsonHttpMessageConverter(new GsonBuilder()
                // 可以在这里配置一下 Gson
                .serializeNulls()
                .create()));
    }
}

(可选)添加 ControllerAdvice 来自动设置 HttpStatus

如果前面设计 API 统一返回格式时,选择了 Http 状态码随结果而定的话,那么就可以做这一步。

使用 ControllerAdvice 可以免去接口处理过程手动设置 HttpStatus 的麻烦,让我们可以更专注于业务代码。

ControllerAdvice 有两个相关接口:

  • ResponseBodyAdvice<T>:在将返回值序列化前预处理返回值
  • RequestBodyAdvice:在将请求体反序列化前进行预处理

关于这两个接口,后续找个时间写一篇文章介绍。

这里我们编写一个 ResponseBodyAdvice 实现类,来实现自动设置 Http 状态的效果:

@ControllerAdvice
public class ApiResultAdvice implements ResponseBodyAdvice<ApiResult<?>> {

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return returnType.getParameterType().equals(ApiResult.class);
    }

    @Override
    public ApiResult<?> beforeBodyWrite(ApiResult<?> body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        if (body == null) {
            return null;
        }
        HttpStatus httpStatus = HttpStatus.resolve(body.getHttpStatus());
        if (httpStatus != null) {
            response.setStatusCode(httpStatus);
        } else {
            throw new IllegalArgumentException("Invalid Http status code: " + body.getHttpStatus());
        }
        return body;
    }
}

注意,如果不添加 @ControllerAdvice 注解,那 SpringBoot 将不会装载实现,导致无法实现效果。

编写完成后,就不需要在业务处理代码中手动设置 Http 响应状态码了!

在接口中使用 API 统一格式

现在,我们试着编写一个接口,来试试我们设计的 API 效果如何:

    @ResponseBody
    @RequestMapping(method = RequestMethod.GET, path = "/api/user/getMe")
    public ApiResult<UserResponse> getMe() {
        Subject subject = SecurityUtils.getSubject();
        if (subject.isAuthenticated()) {
            User user = userDao.findById(Long.parseLong(subject.getPrincipal().toString())).orElse(null);
            if (user == null) {
                return new ApiResult<>(HttpStatus.BAD_REQUEST, -10001, "User does not exist.", null);
            }
            return new ApiResult<>(0, "Success.", new UserResponse(user));
        }
        return new ApiResult<>(HttpStatus.UNAUTHORIZED, -10001, "Login required.", null);
    }

请求一下看看:

{
  "code": 0,
  "message": "Success.",
  "data": {
    "id": 1,
    "name": "用户35795",
    "joinTime": 1663755109000,
    "role": "visitor",
    "banned": false
  }
}

完美~


参考资料:


脑洞大开的开发者,热爱探讨尝试各种技术! [email protected][email protected]