SpringBoot 如何统一后端返回格式?老鸟们都是这样玩的!
Posted king哥Java架构
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringBoot 如何统一后端返回格式?老鸟们都是这样玩的!相关的知识,希望对你有一定的参考价值。
大家好,我是只爱教妹学Java的秃头哥。
今天我们来聊一聊在基于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部分:
-
status 状态值:由后端统一定义各种返回结果的状态码
-
message 描述:本次接口调用的结果描述
-
data 数据:本次返回的数据。
`{
"status":"100",
"message":"操作成功",
"data":"hello,javadaily"
}
`
当然也可以按需加入其他扩展值,比如我们就在返回对象中添加了接口调用时间
- 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);
/**
* 对返回的数据进行处理
*/
@Nullable
T 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> {
@Autowired
private ObjectMapper objectMapper;
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
return true;
}
@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));
}
return ResultData.success(o);
}
}
`
需要注意两个地方:
-
@RestControllerAdvice
注解@RestControllerAdvice
是@RestController
注解的增强,可以实现三个方面的功能:
-
全局异常处理
-
全局数据绑定
-
全局数据预处理
- 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为什么需要全局异常处理器
-
不用手写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
了,而是希望由全局异常处理器处理。
- 对于自定义异常,只能通过全局异常处理器来处理
`@GetMapping("error1")
public void empty(){
throw new RuntimeException("自定义异常");
}
`
- 当我们引入Validator参数校验器的时候,参数校验不通过会抛出异常,此时是无法用
try...catch
捕获的,只能使用全局异常处理器。
如何实现全局异常处理器
`@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());
}
}
`
有三个细节需要说明一下:
-
@RestControllerAdvice
,RestController的增强类,可用于实现全局异常处理器 -
@ExceptionHandler
,统一处理某一类异常,从而减少代码重复率和复杂度,比如要获取自定义异常可以@ExceptionHandler(BusinessException.class)
-
@ResponseStatus
指定客户端收到的http状态码
体验效果
这时候我们调用如下接口:
`@GetMapping("error1")
public void empty(){
throw new RuntimeException("自定义异常");
}
`
返回的结果如下:
`{
"status": 500,
"message": "自定义异常",
"data": null,
"timestamp": 1625795902556
}
`
基本满足我们的需求了。
但是当我们同时启用统一标准格式封装功能ResponseAdvice
和RestExceptionHandler
全局异常处理器时又出现了新的问题:
`{
"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 上 star 数超 30K+ 的文档整理出来的,我上传以后,毫无意外的短短半个小时点赞量就达到了 13k,说实话还是有点不可思议的。
一千道互联网 Java 工程师面试题
内容涵盖:Java、MyBatis、ZooKeeper、Dubbo、Elasticsearch、Memcached、Redis、mysql、Spring、SpringBoot、SpringCloud、RabbitMQ、Kafka、Linux等技术栈(485页)
初级—中级—高级三个级别的大厂面试真题
阿里云——Java 实习生/初级
List 和 Set 的区别 HashSet 是如何保证不重复的
HashMap 是线程安全的吗,为什么不是线程安全的(最好画图说明多线程环境下不安全)?
HashMap 的扩容过程
HashMap 1.7 与 1.8 的 区别,说明 1.8 做了哪些优化,如何优化的?
对象的四种引用
Java 获取反射的三种方法
Java 反射机制
Arrays.sort 和 Collections.sort 实现原理 和区别
Cloneable 接口实现原理
异常分类以及处理机制
wait 和 sleep 的区别
数组在内存中如何分配
答案展示:
美团——Java 中级
BeanFactory 和 ApplicationContext 有什么区别
Spring Bean 的生命周期
Spring IOC 如何实现
说说 Spring AOP
Spring AOP 实现原理
动态代理(cglib 与 JDK)
Spring 事务实现方式
Spring 事务底层原理
如何自定义注解实现功能
Spring MVC 运行流程
Spring MVC 启动流程
Spring 的单例实现原理
Spring 框架中用到了哪些设计模式
为什么选择 Netty
说说业务中,Netty 的使用场景
原生的 NIO 在 JDK 1.7 版本存在 epoll bug
什么是 TCP 粘包/拆包
TCP 粘包/拆包的解决办法
Netty 线程模型
说说 Netty 的零拷贝
Netty 内部执行流程
答案展示:
蚂蚁金服——Java 高级
题 1:
jdk1.7 到 jdk1.8 Map 发生了什么变化(底层)?
ConcurrentHashMap
并行跟并发有什么区别?
jdk1.7 到 jdk1.8 java 虚拟机发生了什么变化?
如果叫你自己设计一个中间件,你会如何设计?
什么是中间件?
ThreadLock 用过没有,说说它的作用?
Hashcode()和 equals()和==区别?
mysql 数据库中,什么情况下设置了索引但无法使用?
mysql 优化会不会,mycat 分库,垂直分库,水平分库?
分布式事务解决方案?
sql 语句优化会不会,说出你知道的?
mysql 的存储引擎了解过没有?
红黑树原理?
题 2:
说说三种分布式锁?
redis 的实现原理?
redis 数据结构,使⽤场景?
redis 集群有哪⼏种?
codis 原理?
是否熟悉⾦融业务?记账业务?蚂蚁⾦服对这部分有要求。
好啦~展示完毕,大概估摸一下自己是青铜还是王者呢?
前段时间,在和群友聊天时,把今年他们见到的一些不同类别的面试题整理了一番,于是有了以下面试题集,也一起分享给大家~
如果你觉得这些内容对你有帮助,可以加入csdn进阶交流群,领取资料
基础篇
JVM 篇
MySQL 篇
Redis 篇
由于篇幅限制,详解资料太全面,细节内容太多,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!
如果你觉得这些内容对你有帮助,可以在这里,领取资料
以上是关于SpringBoot 如何统一后端返回格式?老鸟们都是这样玩的!的主要内容,如果未能解决你的问题,请参考以下文章