需求

接口的返回响应,封装成统一的数据格式,再返回给前端。

依赖

对于SpringBoot项目,接口层基于 SpringWeb,也就是 SpringMVC

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

说明

为了使接口的返回结果数据更加规范化,便于接口测试和前端处理,需要以统一的格式来返回数据;

为了不在每一个接口里面,都写一段返回数据封装的代码,将数据封装的逻辑提取出来,使用面相切面原理(AOP),统一对数据进行封装。

如上,涉及到两个问题:

  1. 定义:响应实体的数据结构;
  2. 响应数据统一封装;

下面,我们分别来介绍这两个问题如何处理。

响应实体的数据结构

数据结构

返回响应,统一封装实体,数据结构如下:
在这里插入图片描述

代码

package com.example.core.model;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;

/**
 * 返回响应,统一封装实体
 *
 * @param <T> 数据实体泛型
 */
@Getter
@ToString
@EqualsAndHashCode
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Schema(name = "返回响应", description = "返回响应,统一封装实体")
public class Result<T> {

    @Schema(description = "用户提示", example = "操作成功!")
    private String userMessage;

    /**
     * 错误码<br>
     * 调用成功时,为 null。<br>
     * 示例:A0211
     */
    @Schema(description = "错误码")
    private String errorCode;

    /**
     * 错误信息<br>
     * 调用成功时,为 null。<br>
     * 示例:"用户输入密码错误次数超限"
     */
    @Schema(description = "错误信息")
    private String errorMessage;

    /**
     * 数据实体(泛型)<br>
     * 当接口没有返回数据时,为 null。
     */
    @Schema(description = "数据实体(泛型)")
    private T data;


    public static <T> Result<T> success(T data) {
        return new Result<>("操作成功!", null, null, data);
    }


    public static <T> Result<T> fail(String userMessage, String errorCode, String errorMessage) {
        return new Result<>(userMessage, errorCode, errorMessage, null);
    }

}

特别说明:不需要表示成功或失败的字段

在本处的数据结构中,没有一个专门用来表示接口请求成功或失败的字段(比如:success 或 code)。

推荐的做法是:使用 HTTP状态码表示请求是否成功;最简单的模型是,当状态码为200时,表示成功;当状态码为 3xx,4xx,5xx 时,代表请求失败。

HTTP的状态码,已经清晰的描述了请求的响应状态(成功/失败)。

复杂响应模型中, HTTP状态码

复杂模型中, HTTP状态码还包含请求成功的类型和失败的原因。

复杂模型中,请求成功的状态码及含义:

HTTP状态码含义
200 OK请求成功
201 Created新增成功
202 Accepted成功,异步任务已经接收

请求失败的状态码及含义

HTTP状态码含义
400 Bad Request失败,客户端请求错误(比如,参数传递错误)
401 Unauthorized失败,未登录
403 Forbidden失败,未授权
405 Method Not Allowed失败,Http请求方法不支持
500 Internal Server Error失败,内部服务器错误

405 Method Not Allowed:当一个请求,能找到对应的【接口路径】,但是没有找到对应的【请求方法】时,会报异常,返回响应码为 405 。
500 Internal Server Error:服务器内部出现了错误,返回响应码为 500。

响应统一封装

响应统一封装:基于 ResponseBodyAdvice

基于面相切面编程(AOP)原理,每个接口方法调用成功后,在返回给客户端前,会进行指定的处理,这里是响应数据统一封装成指定的格式;其实也可以做其他的事情,比如 加密。

代码

package com.example.core.advice;


import com.example.core.model.Result;
import com.example.core.util.JsonUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
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;

/**
 * 响应统一封装
 * <p>
 * 将响应数据,封装成统一的数据格式。
 * <p>
 * 通过本处理器,将接口方法返回的数据,统一封装到 Result 的 data 字段中,如果接口方法返回为 void,则 data 字段的值为 null。
 */
@Slf4j
@RestControllerAdvice(basePackages = "com.example.web")
public class GlobalResponseHandler implements ResponseBodyAdvice<Object> {

    /**
     * 此组件是否支持给定的控制器方法返回类型和选定的 {@code HttpMessageConverter} 类型。
     *
     * @return 如果应该调用 {@link #beforeBodyWrite} ,则为 {@code true};否则为false。
     */
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        // 返回类型不为Result,才需要封装
        return returnType.getParameterType() != Result.class;
    }


    /**
     * 统一封装返回响应数据
     */
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
                                  ServerHttpResponse response) {

        // 数据封装为Result:将接口方法返回的数据,封装到 Result.data 字段中。
        Result<Object> result = Result.success(body);

        // 返回类型不是 String:直接返回
        if (returnType.getParameterType() != String.class) {
            return result;
        }

        // 返回类型是 String:不能直接返回,需要进行额外处理
        // 1. 将 Content-Type 设为 application/json ;返回类型是String时,默认 Content-Type = text/plain
        HttpHeaders headers = response.getHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        // 2. 将 Result 转为 Json字符串 再返回
        // (否则会报错 java.lang.ClassCastException: com.example.core.model.Result cannot be cast to java.lang.String)
        return JsonUtil.toJson(result);
    }

}

补充说明

需要注意两点:

  1. 返回类型不为 Result,才需要封装;
  2. 返回类型是 String,需要进行额外处理,不能直接返回,否则会报错。

如果返回类型是 Result 也封装,就会使得接口返回中多一层 Result 嵌套;

SpringMVC 的接口如果返回值为String类型时:(1)默认 Content-Type = text/plain,需要手动设置为 application/json;(2)统一封装后的返回值也必须为String,否则会报错 ClassCastException,所以需要将封装好的Result 转换成JSON字符串

测试

代码

package com.example.web.exception.controller;

import com.example.core.log.annotation.ApiLog;
import com.example.core.model.PageQuery;
import com.example.web.exception.query.UserQuery;
import com.example.web.model.vo.UserVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.validation.Valid;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;

@Slf4j
@RestController
@RequestMapping("exception")
@Tag(name = "异常统一处理")
public class ExceptionController {


    @ApiLog
    @GetMapping(path = "users")
    @Operation(summary = "查询用户列表", description = "测试:BindException。参数校验异常:Get请求,Query参数,以对象的形式接收。")
    public List<UserVO> listUsers(@Valid UserQuery userQuery, PageQuery pageQuery,
                                  HttpServletRequest request, HttpServletResponse response, HttpSession session) {
        log.info("查询用户列表。userQuery={},pageQuery={}", userQuery, pageQuery);

        String queryName = userQuery.getName();
        String queryPhone = userQuery.getMobilePhone();

        return listMockUsers().stream().filter(user -> {
            boolean isName = true;
            boolean isPhone = true;
            if (StringUtils.hasText(queryName)) {
                isName = user.getName().contains(queryName);
            }
            if (StringUtils.hasText(queryPhone)) {
                isPhone = user.getMobilePhone().contains(queryPhone);
            }
            return isName && isPhone;
        }).collect(Collectors.toList());
    }


    private List<UserVO> listMockUsers() {
        List<UserVO> list = new ArrayList<>();

        UserVO vo = new UserVO();
        vo.setId("1234567890123456789");
        vo.setName("张三");
        vo.setMobilePhone("18612345678");
        vo.setEmail("zhangsan@qq.com");
        vo.setBeginTime(new Date());
        vo.setEndTime(new Date());
        vo.setBeginDate(new Date());
        vo.setEndDate(new Date());
        list.add(vo);

        UserVO vo2 = new UserVO();
        vo2.setId("1234567890123456781");
        vo2.setName("李四");
        vo2.setMobilePhone("13412345678");
        vo2.setEmail("lisi@example.com");
        vo2.setBeginTime(new Date());
        vo2.setEndTime(new Date());
        vo2.setBeginDate(new Date());
        vo2.setEndDate(new Date());
        list.add(vo2);

        return list;
    }

}

效果

在这里插入图片描述

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐