SpringBoot 如何统一后端返回格式?老鸟们都是这样玩的!

大家好,我是磊哥。

今天我们来聊一聊在基于SpringBoot前后端分离开发模式下,如何友好的返回统一的标准格式以及如何优雅的处理全局异常。

首先我们来看看为什么要返回统一的标准格式?

为什么要对SpringBoot返回统一的标准格式

在默认情况下,SpringBoot的返回格式常见的有三种:

第一种:返回 String

@GetMapping("/hello")
public String getStr(){return "hello,javadaily";
}

此时调用接口获取到的返回值是这样:

hello,javadaily

第二种:返回自定义对象

@GetMapping("/aniaml")
public Aniaml getAniaml(){Aniaml aniaml = new Aniaml(1,"pig");return aniaml;
}

此时调用接口获取到的返回值是这样:

{"id": 1,"name": "pig"
}

第三种:接口异常

@GetMapping("/error")
public int error(){int i = 9/0;return i;
}

此时调用接口获取到的返回值是这样:

{"timestamp": "2021-07-08T08:05:15.423+00:00","status": 500,"error": "Internal Server Error","path": "/wrong"
}

基于以上种种情况,如果你和前端开发人员联调接口她们就会很懵逼,由于我们没有给他一个统一的格式,前端人员不知道如何处理返回值。

还有甚者,有的同学比如小张喜欢对结果进行封装,他使用了Result对象,小王也喜欢对结果进行包装,但是他却使用的是Response对象,当出现这种情况时我相信前端人员一定会抓狂的。

所以我们项目中是需要定义一个统一的标准返回格式的。

定义返回标准格式

一个标准的返回格式至少包含3部分:

  1. status 状态值:由后端统一定义各种返回结果的状态码

  2. message 描述:本次接口调用的结果描述

  3. data 数据:本次返回的数据。

{"status":"100","message":"操作成功","data":"hello,javadaily"
}

当然也可以按需加入其他扩展值,比如我们就在返回对象中添加了接口调用时间

  1. timestamp: 接口调用时间

定义返回对象

@Data
public class ResultData<T> {/** 结果状态 ,具体状态码参见ResultData.java*/private int status;private String message;private T data;private long timestamp ;public ResultData (){this.timestamp = System.currentTimeMillis();}public static <T> ResultData<T> success(T data) {ResultData<T> resultData = new ResultData<>();resultData.setStatus(ReturnCode.RC100.getCode());resultData.setMessage(ReturnCode.RC100.getMessage());resultData.setData(data);return resultData;}public static <T> ResultData<T> fail(int code, String message) {ResultData<T> resultData = new ResultData<>();resultData.setStatus(code);resultData.setMessage(message);return resultData;}}

定义状态码

public enum ReturnCode {/**操作成功**/RC100(100,"操作成功"),/**操作失败**/RC999(999,"操作失败"),/**服务限流**/RC200(200,"服务开启限流保护,请稍后再试!"),/**服务降级**/RC201(201,"服务开启降级保护,请稍后再试!"),/**热点参数限流**/RC202(202,"热点参数限流,请稍后再试!"),/**系统规则不满足**/RC203(203,"系统规则不满足要求,请稍后再试!"),/**授权规则不通过**/RC204(204,"授权规则不通过,请稍后再试!"),/**access_denied**/RC403(403,"无访问权限,请联系管理员授予权限"),/**access_denied**/RC401(401,"匿名用户访问无权限资源时的异常"),/**服务异常**/RC500(500,"系统异常,请稍后重试"),INVALID_TOKEN(2001,"访问令牌不合法"),ACCESS_DENIED(2003,"没有权限访问该资源"),CLIENT_AUTHENTICATION_FAILED(1001,"客户端认证失败"),USERNAME_OR_PASSWORD_ERROR(1002,"用户名或密码错误"),UNSUPPORTED_GRANT_TYPE(1003, "不支持的认证模式");/**自定义状态码**/private final int code;/**自定义描述**/private final String message;ReturnCode(int code, String message){this.code = code;this.message = message;}public int getCode() {return code;}public String getMessage() {return message;}
}

统一返回格式

@GetMapping("/hello")
public ResultData<String> getStr(){return ResultData.success("hello,javadaily");
}

此时调用接口获取到的返回值是这样:

{"status": 100,"message": "hello,javadaily","data": null,"timestamp": 1625736481648,"httpStatus": 0
}

这样确实已经实现了我们想要的结果,我在很多项目中看到的都是这种写法,在Controller层通过ResultData.success()对返回结果进行包装后返回给前端。

看到这里我们不妨停下来想想,这样做有什么弊端呢?

最大的弊端就是我们后面每写一个接口都需要调用ResultData.success()这行代码对结果进行包装,重复劳动,浪费体力;

而且还很容易被其他老鸟给嘲笑。


所以呢我们需要对代码进行优化,目标就是不要每个接口都手工制定ResultData返回值。

高级实现方式

要优化这段代码很简单,我们只需要借助SpringBoot提供的ResponseBodyAdvice即可。

ResponseBodyAdvice的作用:拦截Controller方法的返回值,统一处理返回值/响应体,一般用来统一返回格式,加解密,签名等等。

先来看下ResponseBodyAdvice的源码:

public interface ResponseBodyAdvice<T> {/*** 是否支持advice功能* true 支持,false 不支持*/boolean supports(MethodParameter var1, Class<? extends HttpMessageConverter<?>> var2);/*** 对返回的数据进行处理*/@NullableT beforeBodyWrite(@Nullable T var1, MethodParameter var2, MediaType var3, Class<? extends HttpMessageConverter<?>> var4, ServerHttpRequest var5, ServerHttpResponse var6);
}

我们只需要编写一个具体实现类即可

/*** @author jam* @date 2021/7/8 10:10 上午*/
@RestControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice<Object> {@Autowiredprivate ObjectMapper objectMapper;@Overridepublic boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {return true;}@SneakyThrows@Overridepublic Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {if(o instanceof String){return objectMapper.writeValueAsString(ResultData.success(o));}        return ResultData.success(o);}
}

需要注意两个地方:

  • @RestControllerAdvice注解

    @RestControllerAdvice@RestController注解的增强,可以实现三个方面的功能:

  1. 全局异常处理

  2. 全局数据绑定

  3. 全局数据预处理

  • String类型判断

  • if(o instanceof String){return objectMapper.writeValueAsString(ResultData.success(o));
    } 
    

    这段代码一定要加,如果Controller直接返回String的话,SpringBoot是直接返回,故我们需要手动转换成json。

    经过上面的处理我们就再也不需要通过ResultData.success()来进行转换了,直接返回原始数据格式,SpringBoot自动帮我们实现包装类的封装。

    @GetMapping("/hello")
    public String getStr(){return "hello,javadaily";
    }
    

    此时我们调用接口返回的数据结果为:

    @GetMapping("/hello")
    public String getStr(){return "hello,javadaily";
    }
    

    是不是感觉很完美,别急,还有个问题在等着你呢。


    接口异常问题

    此时有个问题,由于我们没对Controller的异常进行处理,当我们调用的方法一旦出现异常,就会出现问题,比如下面这个接口

    @GetMapping("/wrong")
    public int error(){int i = 9/0;return i;
    }
    

    返回的结果为:


    这显然不是我们想要的结果,接口都报错了还返回操作成功的响应码,前端看了会打人的。

    别急,接下来我们进入第二个议题,如何优雅的处理全局异常。

    SpringBoot为什么需要全局异常处理器

    1. 不用手写try...catch,由全局异常处理器统一捕获

      使用全局异常处理器最大的便利就是程序员在写代码时不再需要手写try...catch了,前面我们讲过,默认情况下SpringBoot出现异常时返回的结果是这样:

    {"timestamp": "2021-07-08T08:05:15.423+00:00","status": 500,"error": "Internal Server Error","path": "/wrong"
    }
    

    这种数据格式返回给前端,前端是看不懂的,所以这时候我们一般通过try...catch来处理异常

    @GetMapping("/wrong")
    public int error(){int i;try{i = 9/0;}catch (Exception e){log.error("error:{}",e);i = 0;}return i;
    }
    

    我们追求的目标肯定是不需要再手动写try...catch了,而是希望由全局异常处理器处理。

    1. 对于自定义异常,只能通过全局异常处理器来处理

    @GetMapping("error1")
    public void empty(){throw  new RuntimeException("自定义异常");
    }
    
    1. 当我们引入Validator参数校验器的时候,参数校验不通过会抛出异常,此时是无法用try...catch捕获的,只能使用全局异常处理器。

      SpringBoot集成参数校验请参考这篇文章SpringBoot开发秘籍 - 集成参数校验及高阶技巧

    如何实现全局异常处理器

    @Slf4j
    @RestControllerAdvice
    public class RestExceptionHandler {/*** 默认全局异常处理。* @param e the e* @return ResultData*/@ExceptionHandler(Exception.class)@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)public ResultData<String> exception(Exception e) {log.error("全局异常信息 ex={}", e.getMessage(), e);return ResultData.fail(ReturnCode.RC500.getCode(),e.getMessage());}}
    

    有三个细节需要说明一下:

    1. @RestControllerAdvice,RestController的增强类,可用于实现全局异常处理器

    2. @ExceptionHandler,统一处理某一类异常,从而减少代码重复率和复杂度,比如要获取自定义异常可以@ExceptionHandler(BusinessException.class)

    3. @ResponseStatus指定客户端收到的http状态码

    体验效果

    这时候我们调用如下接口:

    @GetMapping("error1")
    public void empty(){throw  new RuntimeException("自定义异常");
    }
    

    返回的结果如下:

    {"status": 500,"message": "自定义异常","data": null,"timestamp": 1625795902556
    }
    

    基本满足我们的需求了。

    但是当我们同时启用统一标准格式封装功能ResponseAdviceRestExceptionHandler全局异常处理器时又出现了新的问题:

    {"status": 100,"message": "操作成功","data": {"status": 500,"message": "自定义异常","data": null,"timestamp": 1625796167986},"timestamp": 1625796168008
    }
    

    此时返回的结果是这样,统一格式增强功能会给返回的异常结果再次封装,所以接下来我们需要解决这个问题。

    全局异常接入返回的标准格式

    要让全局异常接入标准格式很简单,因为全局异常处理器已经帮我们封装好了标准格式,我们只需要直接返回给客户端即可。

    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {if(o instanceof String){return objectMapper.writeValueAsString(ResultData.success(o));}if(o instanceof ResultData){return o;}return ResultData.success(o);
    }
    

    关键代码:

    if(o instanceof ResultData){return o;
    }
    

    如果返回的结果是ResultData对象,直接返回即可。

    这时候我们再调用上面的错误方法,返回的结果就符合我们的要求了。

    {"status": 500,"message": "自定义异常","data": null,"timestamp": 1625796580778
    }
    

    好了,今天的文章就到这里了,希望通过这篇文章你能掌握如何在你项目中友好实现统一标准格式到返回并且可以优雅的处理全局异常。

    github地址:https://github.com/jianzh5/cloud-blog/


    文末福利

    最后磊哥为了回馈广大的读者朋友,决定送 3 本疯狂 Java 李刚新作《疯狂Spring Boot终极讲义》横跨 WebFlux、Redis、ElasticSearch、Kafka、K8s 等 7 大整合方向,并有高并发秒杀系统案例,赠视频及代码,免费包邮到家。(日常在看转发较多的小伙伴获奖概率更高哦,本周五公布中奖名单~)

    当然,不差钱的朋友也可以直接下单购买,此书正在京东、当当限时5折促销,下单地址:

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

zabbix企业应用之监控docker容器资源情况

关于docker的监控&#xff0c;无论开源的CAdvisor、Data Dog还是我自己写的监控&#xff08;http://dl528888.blog.51cto.com/2382721/1635951&#xff09;&#xff0c;不是通过docker的stats api就是使用socket来进行。单独看一个主机的监控项还行&#xff0c;比如只查看容器t…

使用了synchronized,竟然还有线程安全问题!

线程安全问题一直是系统亘古不变的痛点。这不&#xff0c;最近在项目中发了一个错误使用线程同步的案例。表面上看已经使用了同步机制&#xff0c;一切岁月静好&#xff0c;但实际上线程同步却毫无作用。关于线程安全的问题&#xff0c;基本上就是在挖坑与填坑之间博弈&#xf…

序列图| 软件工程

什么是时序图&#xff1f; (What is Sequence Diagram?) Sequence Diagram is a "Connection Diagram" that represents a single structure or storyline executing in a system. It is the second most used UML diagram behind the class diagram. Sequence Diag…

终极解密输入网址按回车到底发生了什么?

详解输入网址点击回车&#xff0c;后台到底发生了什么。透析 HTTP 协议与 TCP 连接之间的千丝万缕的关系。掌握为何是三次握手四次挥手&#xff1f;time_wait 存在的意义是什么&#xff1f;全面图解重点问题&#xff0c;再也不用担心面试问这个问题。大致流程URL 解析&#xff…

unity, 相机空间 与 相机gameObject的局部空间

在unity里 相机空间 与 相机gameObject的局部空间 不重合。 Camera.worldToCameraMatrix的文档中有这样一句话&#xff1a; Note that camera space matches OpenGL convention: cameras forward is the negative Z axis. This is different from Unitys convention, where for…

Winform实现漂亮动画-小火车

一、起因 最近在做一个Winform的项目&#xff0c;其中需要一些加载动画&#xff0c;所以就搜索了一下找些思路&#xff0c;以下链接是本文的参考。 参考&#xff1a;Jeremie Martinez &#xff08;译文链接&#xff09; 注&#xff1a;原文中并没有给出图片资源&#xff0c;图…

synchronized 加锁 this 和 class 的区别!

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;synchronized 是 Java 语言中处理并发问题的一种常用手段&#xff0c;它也被我们亲切的称之为“Java 内置锁”&#xff0c;由…

C# WinForm窗体四周阴影效果

一、起因 关于winform窗体无边框的问题很简单&#xff0c;只需要设置winform的窗体属性即可&#xff1a; FormBorderStyle FormBorderStyle.None; 但是这中无边框窗口实现的效果和背景完全没有层次的感觉&#xff0c;所以能加上阴影&#xff0c;突出窗口显示的感觉。 二、…

synchronized 优化手段之锁膨胀机制!

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;synchronized 在 JDK 1.5 之前性能是比较低的&#xff0c;在那时我们通常会选择使用 Lock 来替代 synchronized。然而这个情…

NTFS USN的Create和工具代码汇总

1、 因为之前把相关代码放在了GitHub上&#xff0c;后来突然有人帮忙改了些个BUG&#xff0c;非常感谢 760193107&#xff0c;所以就写了个完整点的例子&#xff0c;希望对别人有所帮助。 GitHub项目地址 2、错误码&#xff1a;ERROR_JOURNAL_NOT_ACTIVE 在测试时&#xff…

在Java中,负数的绝对值不一定是正数!

作者 l Hollis来源 l Hollis&#xff08;ID&#xff1a;hollischuang&#xff09;绝对值是指一个数在数轴上所对应点到原点的距离&#xff0c;所以&#xff0c;在数学领域&#xff0c;正数的绝对值是这个数本身&#xff0c;负数的绝对值应该是他的相反数。这几乎是每个人都知道…

自己写着玩(二)

转载于:https://www.cnblogs.com/wangmengmeng/p/4572611.html

实战:隐藏SpringBoot中的私密数据!

这几天公司在排查内部数据账号泄漏&#xff0c;原因是发现某些实习生小可爱居然连带着账号、密码将源码私传到GitHub上&#xff0c;导致核心数据外漏&#xff0c;孩子还是没挨过社会毒打&#xff0c;这种事的后果可大可小。说起这个我是比较有感触的&#xff0c;之前我TM被删库…

JS的条形码和二维码生成

一、前言 最近做项目用到了JS生成条形码和二维码&#xff0c;内容不多&#xff0c;整理一下方便使用。 2018年7月5日更新&#xff1a; 二维码生成时&#xff0c;如果长度太长会有异常&#xff1a; Uncaught Error: code length overflow. (1604>1056) 创建的时候&#…

synchronized 中的 4 个优化,你知道几个?

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;synchronized 在 JDK 1.5 时性能是比较低的&#xff0c;然而在后续的版本中经过各种优化迭代&#xff0c;它的性能也得到了前…

31Exchange Server 2010跨站点部署-搬迁Exchange服务器到分支机构

16.4 将EX07和EX08搬迁到上海分支机构首先我在上海分支机机构站点下创建一个CAS阵列&#xff0c;命令如下&#xff1a;下面获取下当前域中的CAS阵列信息16.4.1搬迁CAS,HT服务器1、从广州总部NLB群集删除EX07主机2、修改EX07的IP地址为分支机构IP地址 192.168.20.27(上海分支机构…

@Autowired的这些骚操作,你都知道吗?

前言最近review别人代码的时候&#xff0c;看到了一些Autowired不一样的用法&#xff0c;觉得有些意思&#xff0c;特定花时间研究了一下&#xff0c;收获了不少东西&#xff0c;现在分享给大家。也许Autowired比你想象中更强大。1. Autowired的默认装配我们都知道在spring中Au…

C# Winform 使用二维码

关于C# Winform 程序中使用二维码的使用记录&#xff1a; 1、使用 Nuget 安装 ZXing.Net 程序包&#xff1b; 2、调用代码&#xff1a; private void button1_Click(object sender, EventArgs e) {BarcodeWriter writer new BarcodeWriter();writer.Format BarcodeFormat…

[Swust OJ 85]--单向公路(BFS)

题目链接:http://acm.swust.edu.cn/problem/0085/ Time limit(ms): 5000      Memory limit(kb): 65535Description某个地区有许多城镇&#xff0c;但并不是每个城镇都跟其他城镇有公路连接&#xff0c;且有公路的并不都能双向行驶。现在我们把这些城镇间的公路分布及允许…

7 种分布式全局 ID 生成策略,你更爱哪种?

上了微服务之后&#xff0c;很多原本很简单的问题现在都变复杂了&#xff0c;例如全局 ID 这事&#xff01;最近工作中刚好用到这块内容&#xff0c;于是调研了市面上几种常见的全局 ID 生成策略&#xff0c;稍微做了一下对比&#xff0c;供小伙伴们参考。当数据库分库分表之后…