springboot如何统一后端返回格式?老鸟们都是这样玩的!(代码片段)

king哥Java架构 king哥Java架构     2022-12-11     342

关键词:

大家好,我是只爱教妹学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部分:

  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);  
  
   /**  
  * 对返回的数据进行处理  
  */  
    @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注解的增强,可以实现三个方面的功能:

  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捕获的,只能使用全局异常处理器。

如何实现全局异常处理器

`@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 上 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:

  1. jdk1.7 到 jdk1.8 Map 发生了什么变化(底层)?

  2. ConcurrentHashMap

  3. 并行跟并发有什么区别?

  4. jdk1.7 到 jdk1.8 java 虚拟机发生了什么变化?

  5. 如果叫你自己设计一个中间件,你会如何设计?

  6. 什么是中间件?

  7. ThreadLock 用过没有,说说它的作用?

  8. Hashcode()和 equals()和==区别?

  9. mysql 数据库中,什么情况下设置了索引但无法使用?

  10. mysql 优化会不会,mycat 分库,垂直分库,水平分库?

  11. 分布式事务解决方案?

  12. sql 语句优化会不会,说出你知道的?

  13. mysql 的存储引擎了解过没有?

  14. 红黑树原理?

题 2:

  1. 说说三种分布式锁?

  2. redis 的实现原理?

  3. redis 数据结构,使⽤场景?

  4. redis 集群有哪⼏种?

  5. codis 原理?

  6. 是否熟悉⾦融业务?记账业务?蚂蚁⾦服对这部分有要求。

好啦~展示完毕,大概估摸一下自己是青铜还是王者呢?

前段时间,在和群友聊天时,把今年他们见到的一些不同类别的面试题整理了一番,于是有了以下面试题集,也一起分享给大家~

如果你觉得这些内容对你有帮助,可以加入csdn进阶交流群,领取资料

基础篇


JVM 篇


MySQL 篇



Redis 篇




由于篇幅限制,详解资料太全面,细节内容太多,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!

如果你觉得这些内容对你有帮助,可以在这里,领取资料

springboot统一后端返回格式?老鸟们都是这样玩的!(代码片段)

大家好,我是bigsai。今天我们来聊一聊在基于SpringBoot前后端分离开发模式下,如何友好的返回统一的标准格式以及如何优雅的处理全局异常。首先我们来看看为什么要返回统一的标准格式?为什么要对SpringBoot返回统... 查看详情

springboot如何进行参数校验,老鸟们都这么玩的!(代码片段)

大家好,我是飘渺。前几天写了一篇SpringBoot如何统一后端返回格式?老鸟们都是这样玩的!阅读效果还不错,而且被很多号主都转载过,今天我们继续第二篇,来聊聊在SprinBoot中如何集成参数校验Validator&#... 查看详情

springboot如何异步编程,老鸟们都这么玩的(代码片段)

大家好,我是飘渺。今天继续给大家带来SpringBoot老鸟系列的第六篇,来聊聊在SpringBoot项目中如何实现异步编程。老鸟系列文章导读:1.SpringBoot如何统一后端返回格式?老鸟们都是这样玩的!2.SpringBoot如何进行... 查看详情

springboot如何异步编程,老鸟们都这么玩的(代码片段)

大家好,我是飘渺。今天继续给大家带来SpringBoot老鸟系列的第六篇,来聊聊在SpringBoot项目中如何实现异步编程。老鸟系列文章导读:1.SpringBoot如何统一后端返回格式?老鸟们都是这样玩的!2.SpringBoot如何进行... 查看详情

springboot如何异步编程,老鸟们都这么玩的(代码片段)

大家好,我是飘渺。今天继续给大家带来SpringBoot老鸟系列的第六篇,来聊聊在SpringBoot项目中如何实现异步编程。老鸟系列文章导读:1.SpringBoot如何统一后端返回格式?老鸟们都是这样玩的!2.SpringBoot如何进行... 查看详情

springboot如何生成接口文档,老鸟们都这么玩的!(代码片段)

为什么要用Swagger?“作为一名程序员,我们最讨厌两件事:1.别人不写注释。2.自己写注释。而作为一名接口开发者,我们同样讨厌两件事:1.别人不写接口文档,文档不及时更新。2.需要自己写接口文档&#x... 查看详情

springboot如何进行业务校验,老鸟们都这么玩的~

大家好,我是飘渺。今天继续给大家带来 SpringBoot老鸟系列的第七篇,来聊聊在SpringBoot项目中如何实现业务异常校验Assert。希望通过今天的文章,咱们能够了解到:如何使用Assert参数校验?为什么用了Validator... 查看详情

springboot如何进行限流?老鸟们都这么玩的!(代码片段)

大家好,我是飘渺。SpringBoot老鸟系列的文章已经写了四篇,每篇的阅读反响都还不错,那今天继续给大家带来老鸟系列的第五篇,来聊聊在SpringBoot项目中如何对接口进行限流,有哪些常见的限流算法,如... 查看详情

springboot如何生成接口文档,老鸟们都这么玩的!(代码片段)

大家好,我是飘渺。SpringBoot老鸟系列的文章已经写了两篇,每篇的阅读反响都还不错,果然大家还是对SpringBoot比较感兴趣。那今天我们就带来老鸟系列的第三篇:集成Swagger接口文档以及Swagger的高级功能。文章涉... 查看详情

springboot如何进行对象复制,老鸟们都这么玩的!(代码片段)

大家好,我是飘渺。今天带来SpringBoot老鸟系列的第四篇,来聊聊在日常开发中如何优雅的实现对象复制。首先我们看看为什么需要对象复制?为什么需要对象复制如上,是我们平时开发中最常见的三层MVC架构模型... 查看详情

springboot如何进行对象复制,老鸟们都这么玩的!(代码片段)

‍‍‍‍今天来聊聊在日常开发中如何优雅的实现对象复制。首先我们看看为什么需要对象复制?为什么需要对象复制如上,是我们平时开发中最常见的三层MVC架构模型,编辑操作时Controller层接收到前端传来的DTO对... 查看详情

springboot返回统一数据格式及其原理浅析

大家都知道,前后分离之后,后端响应最好以统一的格式的响应.譬如:名称描述  status状态码,标识请求成功与否,如[1:成功;-1:失败]  errorCode错误码,给出明确错误码,更好的应对业务异常;请求成功该值可为空&nbs... 查看详情

springboot集成接口文档,老鸟们也被打脸了!

之前我在SpringBoot老鸟系列中专门花了大量的篇幅详细介绍如何集成Swagger,以及如何对Swagger进行扩展让其支持接口参数分组功能。详情可见:SpringBoot如何生成接口文档,老鸟们都这么玩的!可是当我接触到另一个... 查看详情

springboot如何保证接口安全?老鸟们都是这么玩的(代码片段)

一.为什么要保证接口安全?接口安全至关重要,因为在今天的数字时代中,许多系统和应用程序都需要通过网络进行数据交换。如果这些接口存在安全漏洞,黑客或攻击者可能会利用这些漏洞来获取敏感信息或破坏系统。因此... 查看详情

springboot集成接口文档,老鸟们也被打脸了!(代码片段)

之前我在SpringBoot老鸟系列中专门花了大量的篇幅详细介绍如何集成Swagger,以及如何对Swagger进行扩展让其支持接口参数分组功能。详情可见:SpringBoot如何生成接口文档,老鸟们都这么玩的!可是当我接触到另一个... 查看详情

springboot如何保证接口安全?老鸟们都是这么玩的!(代码片段)

大家好,我是飘渺。对于互联网来说,只要你系统的接口暴露在外网,就避免不了接口安全问题。如果你的接口在外网裸奔,只要让黑客知道接口的地址和参数就可以调用,那简直就是灾难。举个例子:你... 查看详情

springboot如何保证接口安全?老鸟们都是这么玩的!(代码片段)

大家好,我是飘渺。对于互联网来说,只要你系统的接口暴露在外网,就避免不了接口安全问题。如果你的接口在外网裸奔,只要让黑客知道接口的地址和参数就可以调用,那简直就是灾难。举个例子:你... 查看详情

springboot统一响应实体封装+统一异常类管理(代码片段)

前言:  在日常前后端分离的接口开发过程中,需要我们按相应的格式给前端返回响应的数据,常见的方式就是我们后端自己封装一个包装类,每次返回给前端数据的时候都需要我们自己手动构建一。短时间内来看或许并没有... 查看详情