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

需求

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

依赖

对于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状态码含义
200 OK请求成功
201 Created新增成功
202 Accepted成功,异步任务已经接收

成功的类型:

HTTP状态码含义
400 Bad Request失败,客户端请求错误(比如,参数传递错误)
401 Unauthorized失败,未登录
403 Forbidden失败,未授权

响应统一封装

响应统一封装:基于 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 嵌套;

SpringWeb的接口如果返回值为String类型,默认 Content-Type = text/plain,需要手动设置为 application/json
返回类型为String时,接口必须返回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;
    }

}

效果

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/107097.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

使用WebStorm创建和配置TypeScript项目

创建 这里我用的是WebStorm 2019.2.2版本 首先&#xff0c;创建一个空项目 File -> New -> Project->Empty Project生成配置文件 自动配置&#xff1a; 打开终端输入tsc --init&#xff0c;即可自动生成tsconfig.json文件 手动配置&#xff1a; 在项目根目录下新建一…

数据结构与算法之矩阵: Leetcode 48. 旋转矩阵 (Typescript版)

旋转图像 https://leetcode.cn/problems/rotate-image/ 描述 给定一个 n n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。你必须在 原地 旋转图像&#xff0c;这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。 示例 1 输入&…

【Docker】Linux网桥连接多个命名空间

veth实现了点对点的虚拟连接&#xff0c;可以通过veth连接两个namespace&#xff0c;如果我们需要将3个或者多个namespace接入同一个二层网络时&#xff0c;就不能只使用veth了。 在物理网络中&#xff0c;如果需要连接多个主机&#xff0c;我们会使用bridge&#xff08;网桥&…

增强常见问题解答搜索引擎:在 Elasticsearch 中利用 KNN 的力量

在快速准确的信息检索至关重要的时代&#xff0c;开发强大的搜索引擎至关重要。 随着大型语言模型和信息检索架构&#xff08;如 RAG&#xff09;的出现&#xff0c;在现代软件系统中利用文本表示&#xff08;向量/嵌入&#xff09;和向量数据库已变得越来越流行。 在本文中&am…

javaweb+mysql的电子书查阅和下载系统

图书分类查看、热门下载、最新上传、站内数据统计。 登陆注册、图书查询、图书详情、图书下载。 身份分为管理员和用户。 源码下载地址 支持&#xff1a;远程部署/安装/调试、讲解、二次开发/修改/定制

串口占用检测工具

串口占用检测工具 平时需要检测哪个程序占用了串口&#xff0c;下面介绍一款非常方便的工具&#xff0c;它的工具箱里包含一个串口占用检测工具&#xff0c;可以非常方便的检测出来哪个程序占用了串口&#xff0c;并给出程序名和PID。 官网下载地址&#xff1a;http://www.red…

安装 tensorflow==1.15.2 遇见的问题

一、直接安装 命令&#xff1a;pip install tensorflow1.15.2 二、换 阿里云 镜像源 命令&#xff1a;pip install -i http://mirrors.aliyun.com/pypi/simple tensorflow1.15.2 三、换 豆瓣 镜像源 命令&#xff1a;pip install http://pypi.douban.com/simple tensorflow1…

UWB室内定位系统全套源码 高精度人员定位系统源码

UWB室内定位系统全套源码 高精度人员定位系统源码 UWB室内定位系统是一种高精度的室内定位技术&#xff0c;它可以实现对室内人员和物品的实时精确定位&#xff0c;具有重要的应用意义和社会价值。 UWB定位精度在厘米级内&#xff0c;其精度远远高于WIFI和蓝牙定位。精度、安全…

华为eNSP配置专题-策略路由的配置

文章目录 华为eNSP配置专题-策略路由的配置0、概要介绍1、前置环境1.1、宿主机1.2、eNSP模拟器 2、基本环境搭建2.1、终端构成和连接2.2、终端的基本配置 3、配置接入交换机上的VLAN4、配置核心交换机为网关和DHCP服务器5、配置核心交换机和出口路由器互通6、配置PC和出口路由器…

ubuntu安装nps客户端

Ubuntu安装nps客户端 1.什么是nps内网穿透&#xff1f;2.设备情况3.下载客户端3.链接服务端3.1、无配置文件模式3.2、注册到系统服务(启动启动、监控进程) 1.什么是nps内网穿透&#xff1f; nps是一款轻量级、高性能、功能强大的内网穿透代理服务器。目前支持tcp、udp流量转发…

单片机为什么一直用C语言,不用其他编程语言?

单片机为什么一直用C语言&#xff0c;不用其他编程语言&#xff1f; 51 单片机规模小得拮据&#xff0c;C 的优势几乎看不到。放个类型信息进去都费劲&#xff0c;你还想用虚函数&#xff1f;还想模板展开&#xff1f;程序轻松破 10k。最近很多小伙伴找我&#xff0c;说想要一些…

uview 1 uni-app表单 number digit 的输入框有初始化赋值后,但是校验失败

背景&#xff1a; 在onReady初始化规则 onReady() { this.$refs.uForm.setRules(this.rules); }, 同时&#xff1a;ref,model,rules,props都要配置好。 报错 当input框限定type为number&#xff0c;digit类型有初始值不做修改动作,直接提交会报错&#xff0c;验…

leetCode 76. 最小覆盖子串 + 滑动窗口 + 哈希Hash

我的往期文章&#xff1a;此题的其他解法&#xff0c;感兴趣的话可以移步看一下&#xff1a; leetCode 76. 最小覆盖子串 滑动窗口 图解&#xff08;详细&#xff09;-CSDN博客https://blog.csdn.net/weixin_41987016/article/details/134042115?spm1001.2014.3001.5501 力…

Java SE 学习笔记(十四)—— IO流(2)

目录 1 字节流1.1 字节流写数据1.1.1 创建字节输出流对象1.1.2 字节流写数据 1.2 字节流读数据1.2.1 创建字节输入流对象1.2.2 字节流读数据 1.3 字节流复制文件1.4 流的刷新与关闭1.5 资源释放方式1.5.1 try-catch-finally1.5.2 try-with-resource 2 字符流2.1 字符流概述2.2 …

PyCharm中文使用详解

PyCharm是一个Python IDE&#xff0c;可以帮助程序员节省时间&#xff0c;提高生产力。那么具体怎么用呢&#xff1f;本文介绍了PyCharm的安装、插件、外部工具、专业功能等&#xff0c;希望对大家有所帮助。 之前没有系统介绍过PyCharm。如何配置环境&#xff0c;如何DeBug&a…

springBoot与Vue共同搭建webSocket环境

欢迎使用Markdown编辑器 你好&#xff01; 这片文章将教会你从后端springCloud到前端VueEleementAdmin如何搭建Websocket 前端 1. 创建websocket的配置文件在utils文件夹下websocket.js // my-js-file.js import { Notification } from element-ui // 暴露自定义websocket对…

MSQL系列(九) Mysql实战-Join算法底层原理

Mysql实战-Join算法底层原理 前面我们讲解了BTree的索引结构&#xff0c;及Mysql的存储引擎MyISAM和InnoDB,今天我们来详细讲解下Mysql的查询连接Join的算法原理 文章目录 Mysql实战-Join算法底层原理1.Simple Nested-Loop Join 简单嵌套循环2.Block Nested-Loop Join 块嵌套…

linux 内存检测工具 kfence 详解(一)

版本基于&#xff1a; Linux-5.10 约定&#xff1a; PAGE_SIZE&#xff1a;4K 内存架构&#xff1a;UMA 系列博文&#xff1a; linux 内存检测工具 kfence 详解(一) linux 内存检测工具 kfence 详解(二) 0. 前言 本文 kfence 之外的代码版本是基于 Linux5.10&#xff0c;…

ORACLE-递归查询、树操作

1. 数据准备 -- 测试数据准备 DROP TABLE untifa_test;CREATE TABLE untifa_test(child_id NUMBER(10) NOT NULL, --子idtitle VARCHAR2(50), --标题relation_type VARCHAR(10) --关系,parent_id NUMBER(10) --父id );insert into untifa_test (CHILD_ID, TITLE, RELATION_TYP…

React 核心与实战2023版

课程亮点: 完整的前后台项目(PC+移动;完成业务;)React 最新企业标准技术栈(React 18 + Redux + ReactRouter + AntD)React + TypeScript (为大型项目奠定了基础)课程内容安排: React 介绍 React 是什么? React 是由Meta公司研发,是一个用于 构建Web和原生交互界面…