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

本文介绍了如何在SpringBoot项目中实现全局统一的返回格式,包括定义标准状态码、封装返回结果,并优雅处理Controller方法异常,以提升前后端协作效率。

大家好,我是飘渺。

今天我们来聊一聊在基于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捕获的,只能使用全局异常处理器。

      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/

    最后,我是飘渺Jam,一名写代码的架构师,做架构的程序员,期待你的关注。咱们下期见!

<think>好的,我现在要解决用户的问题:如何在Spring Boot项目中正确添加依赖。首先,我需要回顾用户提供的引用内容,特别是引用[1]提到了检查依赖、配置和版本兼容性,这三点是关键。用户可能遇到了依赖配置不正确的问题,比如缺少starter依赖或者版本冲突。 接下来,我需要考虑Spring Boot项目添加依赖的一般方法。通常,使用Maven或Gradle作为构建工具,所以需要分别说明这两种工具的配置方式。Maven是通过pom.xml中的parent和starter依赖来管理,而Gradle则是通过插件和依赖声明。同时,版本管理也是重点,因为Spring Boot的版本决定了相关依赖的兼容性,所以需要提到如何使用Spring Boot的BOM(Bill of Materials)来统一版本号,避免冲突。 然后,用户的问题可能涉及到如何选择正确的starter。例如,引用[1]中提到的spring-boot-starter-security,这说明不同的功能需要不同的starter。需要给出一些常见starter的例子,比如web、data-jpa等,并说明它们的作用,帮助用户理解如何根据项目需求添加依赖。 另外,用户可能对依赖管理工具不熟悉,需要详细步骤。比如,在Maven中,先继承spring-boot-starter-parent,然后添加具体的starter依赖。在Gradle中,应用插件并使用依赖管理块。同时,要提醒用户检查依赖是否被错误地排除,或者是否有多个版本冲突的情况,如引用[1]中提到的版本兼容性问题。 还要考虑用户可能遇到的典型错误场景,比如缺少依赖导致自动配置失败,或者版本不匹配引发的类找不到异常。这时候需要指导用户如何排查,例如使用mvn dependency:tree或gradle dependencies命令查看依赖树,确认是否存在冲突。 此外,引用[2]和[3]提到了日志配置和Tomcat配置,虽然用户当前的问题是关于依赖的,但可能需要简要说明这些特定功能的依赖添加方式,比如引入spring-boot-starter-web会自动包含Tomcat,但需要更换服务器时如何排除Tomcat并添加Jetty依赖。 最后,需要总结最佳实践,比如使用Spring Initializr初始化项目,合理利用BOM管理版本,以及定期检查依赖更新。同时,生成相关问题,帮助用户深入理解,比如如何处理依赖冲突,如何排除不需要的依赖等。</think>在 Spring Boot 项目中正确添加依赖需要遵循以下步骤,确保依赖配置的规范性和版本兼容性: --- ### 一、构建工具选择 1. **Maven 配置** - 继承父项目管理版本: ```xml <parent> <groupId>org.springframework.boot</groupId>     <artifactId>spring-boot-starter-parent</artifactId>     <version>3.1.5</version> <!--根据实际需求选择版本--> </parent> ``` - 添加 Starter 依赖: ```xml <dependency>     <groupId>org.springframework.boot</groupId>     <artifactId>spring-boot-starter-web</artifactId> <!--示例依赖--> </dependency> ``` 2. **Gradle 配置** - 应用插件: ```groovy plugins {     id 'org.springframework.boot' version '3.1.5'     id 'io.spring.dependency-management' version '1.1.3' } ``` - 声明依赖: ```groovy dependencies {     implementation 'org.springframework.boot:spring-boot-starter-web' } ``` --- ### 二、依赖管理原则 1. **优先使用 Spring Boot Starter** - 每个模块对应一个 Starter,例如: - Web 开发:$spring-boot-starter-web$ - 数据访问:$spring-boot-starter-data-jpa$ - 安全:$spring-boot-starter-security$ [^1] 2. **统一版本管理** - Maven 通过 `<parent>` 继承版本,Gradle 通过 `dependencyManagement` 插件自动对齐版本。 3. **检查依赖冲突** - 使用命令分析依赖树: ```bash mvn dependency:tree # Maven gradle dependencies # Gradle ``` --- ### 三、典型场景示例 1. **添加数据库依赖** ```xml <dependency>     <groupId>org.springframework.boot</groupId>     <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency>     <groupId>mysql</groupId>     <artifactId>mysql-connector-java</artifactId>     <scope>runtime</scope> </dependency> ``` 2. **替换内嵌服务器** - 排除 Tomcat: ```xml <dependency>     <groupId>org.springframework.boot</groupId>     <artifactId>spring-boot-starter-web</artifactId>     <exclusions>         <exclusion>             <groupId>org.springframework.boot</groupId>             <artifactId>spring-boot-starter-tomcat</artifactId>         </exclusion>     </exclusions> </dependency> ``` - 添加 Jetty: ```xml <dependency>     <groupId>org.springframework.boot</groupId>     <artifactId>spring-boot-starter-jetty</artifactId> </dependency> ``` --- ### 四、验证依赖正确性 1. 启动应用检查日志,确认自动配置生效: ```log Tomcat initialized with port 8080 (http) # 默认内嵌服务器[^3] ``` 2. 使用 `@Autowired` 注入组件测试功能可用性。 --- ### 五、最佳实践 1. **使用 Spring Initializr 初始化项目**(https://start.spring.io/),自动生成标准依赖配置。 2. **定期检查版本更新**,避免依赖过时导致的安全问题。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值