• 接口返回响应,统一封装(ResponseBodyAdvice + Result)(SpringBoot)


    需求

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

    依赖

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

            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-webartifactId>
            dependency>
    
    • 1
    • 2
    • 3
    • 4

    说明

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

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

    如上,涉及到两个问题:

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

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

    响应实体的数据结构

    数据结构

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

    代码

    package com.example.core.model;
    
    import io.swagger.v3.oas.annotations.media.Schema;
    import lombok.*;
    
    /**
     * 返回响应,统一封装实体
     *
     * @param  数据实体泛型
     */
    @Getter
    @ToString
    @EqualsAndHashCode
    @AllArgsConstructor(access = AccessLevel.PRIVATE)
    @Schema(name = "返回响应", description = "返回响应,统一封装实体")
    public class Result<T> {
    
        @Schema(description = "用户提示", example = "操作成功!")
        private String userMessage;
    
        /**
         * 错误码
    * 调用成功时,为 null。
    * 示例:A0211 */
    @Schema(description = "错误码") private String errorCode; /** * 错误信息
    * 调用成功时,为 null。
    * 示例:"用户输入密码错误次数超限" */
    @Schema(description = "错误信息") private String errorMessage; /** * 数据实体(泛型)
    * 当接口没有返回数据时,为 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); } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

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

    在本处的数据结构中,没有一个专门用来表示接口请求成功或失败的字段(比如: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;
    
    /**
     * 响应统一封装
     * 

    * 将响应数据,封装成统一的数据格式。 *

    * 通过本处理器,将接口方法返回的数据,统一封装到 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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65

    补充说明

    需要注意两点:

    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;
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84

    效果

    在这里插入图片描述

  • 相关阅读:
    VSCode搭建Django开发环境
    Kubernetes_17_新建用户账号UserAccount(实践类)
    Shelby American 汽车 NFT 系列来袭!
    小 A 的卡牌游戏(Gym - 103186B)
    第十四届蓝桥杯模拟赛(第三期)Excel表
    kubernetes 通过HostAliases属性配置域名解析
    epoll协程简述
    基于CNN-LSTM的时序预测MATLAB实战
    RAC/RAC One Node 修改私网/心跳网卡名
    Redis基本命令的学习和Jedis
  • 原文地址:https://blog.csdn.net/sgx1825192/article/details/134085290