Springboot项目异常处理及返回结果统一

目录
  • 背景
  • 返回结果定义
  • 异常的定义
  • 异常的处理
  • 返回结果的处理
  • 完整代码
  • 使用示例

背景

在创建项目的初期,我们需要规范后端返回的数据结构,以便更好地与前端开发人员合作。

比如后端返回的数据为:

{
  "msg": "请跳转登陆页面",
}

此时前端无法确定后端服务的处理结果是成功的还是失败的。在前端展示页面,成功与失败的展示是要作区分的,甚至不同的成功或失败结果要做出不同的展现效果,这也就是我们为什么要对返回结果做出统一规范的原因。

返回结果定义

public class ResultWrap<T, M> {
  //  方便前端判断当前请求处理结果是否正常
  private int code;
  //  业务处理结果
  private T data;
  //  产生错误的情况下,提示用户信息
  private String message;
  //  产生错误情况下的异常堆栈,提示开发人员
  private String error;
  //  发生错误的时候,返回的附加信息
  private M metaInfo;
}
  • 1.为了把模糊的消息定性,我们给所有的返回结果都带上一个code字段,前端可以根据这个字段来判断我们的处理结果到底是成功的还是失败的;比如code=200的时候,我们的处理结果一定是成功的,其他的code值全都是失败的,并且我们会有多种code值,以便前端页面可以根据不同的code值做出不同的交互动作。
  • 2.一般在处理成功的情况下,后端会返回一些业务数据,比如返回订单列表等,那么这些数据我们就放在字段data里面,只有业务逻辑处理成功的情况下,data字段里面才可能有数据。
  • 3.如若我们的处理失败了,那么我们应该会有对应的消息提醒用户为什么处理失败了。比如文件上传失败了,用户名或密码错误等等,都是需要告知用户的。
  • 4.除了需要告知用户失败原因,我们也需要保留一个字段给开发人员,当错误是服务器内部错误的时候,我们需要让开发人员能第一时间定位是啥原因引起的,我们会把错误的堆栈信息给到error字段。
  • 5.当发生异常的时候,我们还会需要返回一些额外的补充数据给前端,比如用户登陆失败一次和失败多次需要不同的交互效果,此时我们会在metaInfo里面返回登陆失败次数;或者在用户操作没有权限的时候,根据用户是否已登陆来确定此时是跳转登陆页面还是直接弹窗提示当前操作没有权限。

定义好返回结果后,我们和前端的交互数据结果就统一好了。

异常的定义

之所以定义一个统一的异常类,是为了把所有的异常全部汇总成一个异常,最终我们只需要在异常处理的时候单独处理这一个异常即可。

@Data
public class AwesomeException extends Throwable {
  // 错误码
  private int code;
  // 提示消息
  private String msg;
​
  public AwesomeException(int code, String msg, Exception e) {
    super(e);
    this.code = code;
    this.msg = msg;
  }
​
  public AwesomeException(int code, String msg) {
    this.code = code;
    this.msg = msg;
  }
}
  • 1.我们同样需要一个与返回结果一致的code字段,这样的话,我们在异常处理的时候,才能把当前异常转换成最终的ResultWrap结果。
  • 2.msg就是在产生异常的时候,需要给到用户的提示消息。

这样的话,我们的后端开发人员遇到异常的情况,只需要通过创建AwesomeException异常对象抛出即可,不需要再为创建什么异常而烦恼了。

异常的处理

我们下面需要针对所有抛出的异常进行统一的处理:

import com.example.awesomespring.exception.AwesomeException;
import com.example.awesomespring.vo.ResultWrap;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.AuthorizationException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
​
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
 * @className ExceptionHandler
 * @description:
 */
@Slf4j
@RestControllerAdvice
public class AwesomeExceptionHandler {
  /**
   * 捕获没有用户权限的异常
   *
   * @return
   */
  @ExceptionHandler(AuthorizationException.class)
  public ResultWrap handleException(AuthorizationException e) {
    return ResultWrap.failure(401, "您暂时没有访问权限!", e);
  }
​
  /**
   * 处理AwesomeException
   *
   * @param e
   * @param request
   * @param response
   * @return
   */
  @ExceptionHandler(AwesomeException.class)
  public ResultWrap handleAwesomeException(AwesomeException e, HttpServletRequest request, HttpServletResponse response) {
    return ResultWrap.failure(e);
  }
​
  /**
   * 专门针对运行时异常
   *
   * @param e
   * @return
   */
  @ExceptionHandler(RuntimeException.class)
  public ResultWrap handleRuntimeException(RuntimeException e) {
    return ResultWrap.failure(e);
  }
}

在项目中,我们集成了shiro权限管理框架,因为它抛出的异常没有被我们的AwesomeException包装,所以这个AuthorizationException异常需要我们单独处理。

AwesomeException是我们大多数业务逻辑抛出来的异常,我们根据AwesomeException里面的code、msg和它包装的cause,封装成一个最终的响应数据ResultWrap。

另一个RuntimeException是必须要额外处理的,任何开发人员都无法保证自己的代码是完全没有bug的,任何的空指针异常都会影响用户体验,这种编码性的错误我们需要通过统一的错误处理让它变得更柔和一点。

返回结果的处理

我们需要针对所有的返回结果进行检查,如果不是ResultWrap类型的返回数据,我们需要包装一下,以便保证我们和前端开发人员达成的共识。

import com.example.awesomespring.vo.ResultWrap;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.util.Objects;
​
/**
 * @className AwesomeResponseAdvice
 * @description:
 */
@RestControllerAdvice
public class AwesomeResponseAdvice implements ResponseBodyAdvice {
  @Override
  public boolean supports(MethodParameter returnType, Class converterType) {
    // 如果返回String,那么就不包装了。
    if (StringHttpMessageConverter.class.isAssignableFrom(converterType)) {
      return false;
    }
    // 有一些情况是不需要包装的,比如调用第三方API返回的数据,所以我们做了一个自定义注解来避免所以的结果都被包装成ResultWrap。
    boolean ignore = false;
    IgnoreResponseAdvice ignoreResponseAdvice =
        returnType.getMethodAnnotation(IgnoreResponseAdvice.class);
    // 如果我们在方法上添加了IgnoreResponseAdvice注解,那么就不要拦截包装了
    if (Objects.nonNull(ignoreResponseAdvice)) {
      ignore = ignoreResponseAdvice.value();
      return !ignore;
    }
    // 如果我们在类上面添加了IgnoreResponseAdvice注解,也在方法上面添加了IgnoreResponseAdvice注解,那么以方法上的注解为准。
    Class<?> clazz = returnType.getDeclaringClass();
    ignoreResponseAdvice = clazz.getDeclaredAnnotation(IgnoreResponseAdvice.class);
    RestController restController = clazz.getDeclaredAnnotation(RestController.class);
    if (Objects.nonNull(ignoreResponseAdvice)) {
      ignore = ignoreResponseAdvice.value();
    } else if (Objects.isNull(restController)) {
      ignore = true;
    }
    return !ignore;
  }
​
  @Override
  public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
    // 如果返回结果为null,那么我们直接返回ResultWrap.success()
    if (Objects.isNull(body)) {
      return ResultWrap.success();
    }
    // // 如果返回结果已经是ResultWrap,直接返回
    if (body instanceof ResultWrap) {
      return body;
    }
    // 否则我们把返回结果包装成ResultWrap
    return ResultWrap.success(body);
  }
}
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
​
/**
 * @className IgnoreResponseAdvice
 * @description:
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface IgnoreResponseAdvice {
  // 是否忽略ResponseAdvice;决定了是否要包装返回数据
  boolean value() default true;
}

至此,我们把整个异常处理与返回结果的统一处理全部关联起来了,我们后端的开发人员无论是返回异常还是返回ResultWrap或者其他数据结果,都能很好地保证与前端开发人员的正常协作,不必为数据结构的变化过多地沟通。

完整代码

ResultWrap.java

import com.example.awesomespring.exception.AwesomeException;
import com.example.awesomespring.util.JsonUtil;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus;
​
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Objects;
​
/**
 * @className ResultWrap
 * @description:
 */
@Data
@AllArgsConstructor
public class ResultWrap<T, M> {
  //  方便前端判断当前请求处理结果是否正常
  private int code;
  //  业务处理结果
  private T data;
  //  产生错误的情况下,提示用户信息
  private String message;
  //  产生错误情况下的异常堆栈,提示开发人员
  private String error;
  //  发生错误的时候,返回的附加信息
  private M metaInfo;
​
  /**
   * 成功带处理结果
   *
   * @param data
   * @param <T>
   * @return
   */
  public static <T> ResultWrap success(T data) {
    return new ResultWrap(HttpStatus.OK.value(), data, StringUtils.EMPTY, StringUtils.EMPTY, null);
  }
​
  /**
   * 成功不带处理结果
   *
   * @return
   */
  public static ResultWrap success() {
    return success(HttpStatus.OK.name());
  }
​
  /**
   * 失败
   *
   * @param code
   * @param message
   * @param error
   * @return
   */
  public static <M> ResultWrap failure(int code, String message, String error, M metaInfo) {
    return new ResultWrap(code, null, message, error, metaInfo);
  }
​
  /**
   * 失败
   *
   * @param code
   * @param message
   * @param error
   * @param metaInfo
   * @param <M>
   * @return
   */
  public static <M> ResultWrap failure(int code, String message, Throwable error, M metaInfo) {
    String errorMessage = StringUtils.EMPTY;
    if (Objects.nonNull(error)) {
      errorMessage = toStackTrace(error);
    }
    return failure(code, message, errorMessage, metaInfo);
  }
​
  /**
   * 失败
   *
   * @param code
   * @param message
   * @param error
   * @return
   */
  public static ResultWrap failure(int code, String message, Throwable error) {
    return failure(code, message, error, null);
  }
​
  /**
   * 失败
   *
   * @param code
   * @param message
   * @param metaInfo
   * @param <M>
   * @return
   */
  public static <M> ResultWrap failure(int code, String message, M metaInfo) {
    return failure(code, message, StringUtils.EMPTY, metaInfo);
  }
​
  /**
   * 失败
   *
   * @param e
   * @return
   */
  public static ResultWrap failure(AwesomeException e) {
    return failure(e.getCode(), e.getMsg(), e.getCause());
  }
​
​
  /**
   * 失败
   *
   * @param e
   * @return
   */
  public static ResultWrap failure(RuntimeException e) {
    return failure(500, "服务异常,请稍后访问!", e.getCause());
  }
​
  private static final String APPLICATION_JSON_VALUE = "application/json;charset=UTF-8";
​
  /**
   * 把结果写入响应中
   *
   * @param response
   */
  public void writeToResponse(HttpServletResponse response) {
    int code = this.getCode();
    if (Objects.isNull(HttpStatus.resolve(code))) {
      response.setStatus(HttpStatus.OK.value());
    } else {
      response.setStatus(code);
    }
    response.setContentType(APPLICATION_JSON_VALUE);
    try (PrintWriter writer = response.getWriter()) {
      writer.write(JsonUtil.obj2String(this));
      writer.flush();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
​
  /**
   * 获取异常堆栈信息
   *
   * @param e
   * @return
   */
  private static String toStackTrace(Throwable e) {
    if (Objects.isNull(e)) {
      return StringUtils.EMPTY;
    }
    StringWriter sw = new StringWriter();
    PrintWriter pw = new PrintWriter(sw);
    try {
      e.printStackTrace(pw);
      return sw.toString();
    } catch (Exception e1) {
      return StringUtils.EMPTY;
    }
  }
}

AwesomeException.java

​import lombok.Data;
​
/**
 * @className AwesomeException
 * @description:
 */
@Data
public class AwesomeException extends Throwable {
​
  private int code;
  private String msg;
  public AwesomeException(int code, String msg, Exception e) {
    super(e);
    this.code = code;
    this.msg = msg;
  }
  public AwesomeException(int code, String msg) {
    this.code = code;
    this.msg = msg;
  }
}

AwesomeExceptionHandler.java

import com.example.awesomespring.exception.AwesomeException;
import com.example.awesomespring.vo.ResultWrap;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.AuthorizationException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
​
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
​
/**
 * @className ExceptionHandler
 * @description:
 */
@Slf4j
@RestControllerAdvice
public class AwesomeExceptionHandler {
  /**
   * 捕获没有用户权限的异常
   *
   * @return
   */
  @ExceptionHandler(AuthorizationException.class)
  public ResultWrap handleException(AuthorizationException e) {
    return ResultWrap.failure(401, "您暂时没有访问权限!", e);
  }
​
  /**
   * 处理AwesomeException
   *
   * @param e
   * @param request
   * @param response
   * @return
   */
  @ExceptionHandler(AwesomeException.class)
  public ResultWrap handleAwesomeException(AwesomeException e, HttpServletRequest request, HttpServletResponse response) {
    return ResultWrap.failure(e);
  }
​
  /**
   * 专门针对运行时异常
   *
   * @param e
   * @return
   */
  @ExceptionHandler(RuntimeException.class)
  public ResultWrap handleRuntimeException(RuntimeException e) {
    return ResultWrap.failure(e);
  }
}

IgnoreResponseAdvice.java

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
​
/**
 * @className IgnoreResponseAdvice
 * @description:
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface IgnoreResponseAdvice {
​
  boolean value() default true;
}

AwesomeResponseAdvice.java

import com.example.awesomespring.vo.ResultWrap;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
​
import java.util.Objects;
​
/**
 * @className AwesomeResponseAdvice
 * @description:
 */
@RestControllerAdvice
public class AwesomeResponseAdvice implements ResponseBodyAdvice {
  @Override
  public boolean supports(MethodParameter returnType, Class converterType) {
    if (StringHttpMessageConverter.class.isAssignableFrom(converterType)) {
      return false;
    }
    boolean ignore = false;
    IgnoreResponseAdvice ignoreResponseAdvice =
        returnType.getMethodAnnotation(IgnoreResponseAdvice.class);
    if (Objects.nonNull(ignoreResponseAdvice)) {
      ignore = ignoreResponseAdvice.value();
      return !ignore;
    }
    Class<?> clazz = returnType.getDeclaringClass();
    ignoreResponseAdvice = clazz.getDeclaredAnnotation(IgnoreResponseAdvice.class);
    RestController restController = clazz.getDeclaredAnnotation(RestController.class);
    if (Objects.nonNull(ignoreResponseAdvice)) {
      ignore = ignoreResponseAdvice.value();
    } else if (Objects.isNull(restController)) {
      ignore = true;
    }
    return !ignore;
  }
​
  @Override
  public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
    if (Objects.isNull(body)) {
      return ResultWrap.success();
    }
    if (body instanceof ResultWrap) {
      return body;
    }
    return ResultWrap.success(body);
  }
}

使用示例

// 这里只要返回AwesomeException,就会被ExceptionHandler处理掉,包装成ResultWrap
@PostMapping("/image/upload")
String upload(@RequestPart("userImage") MultipartFile userImage) throws AwesomeException {
    fileService.putObject("video", userImage);
    return "success";
}
​
// 加上IgnoreResponseAdvice注解,该返回结果就不会被包装
@IgnoreResponseAdvice
@GetMapping("/read")
Boolean read() {
    return true;
}

到此这篇关于Springboot项目异常处理及返回结果统一的文章就介绍到这了,更多相关Springboot异常处理内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • SpringBoot项目优雅的全局异常处理方式(全网最新)

    前言 在日常项目开发中,异常是常见的,但是如何更高效的处理好异常信息,让我们能快速定位到BUG,是很重要的,不仅能够提高我们的开发效率,还能让你代码看上去更舒服,SpringBoot的项目已经对有一定的异常处理了,但是对于我们开发者而言可能就不太合适了,因此我们需要对这些异常进行统一的捕获并处理. 一.全局异常处理方式一 SpringBoot中,@ControllerAdvice 即可开启全局异常处理,使用该注解表示开启了全局异常的捕获,我们只需在自定义一个方法使用@ExceptionHandl

  • 深入聊一聊springboot项目全局异常处理那些事儿

    目录 前言 问题一:全局异常抽离出来后,业务错误码如何定义? 问题二:全局异常因引入了和业务相同的依赖jar,但jar存在版本差异 问题三:引入maven optional标签后,因业务没引入全局异常需要的jar,导致项目启动报错 总结 demo链接 前言 之前我们业务团队在处理全局异常时,在每个业务微服务中都加入了@RestControllerAdvice+@ExceptionHandler来进行全局异常捕获.某次领导在走查代码的时候,就提出了一个问题,为什么要每个微服务项目都要自己在写一套全

  • SpringBoot项目 文件上传临时目标被删除异常的处理方案

    1.业务背景 我们使用了SpringCloud 进行项目开发,其中一个主要服务(涉及到图片上传)的SpringBoot微服务在测试环境之中.因为此项目已经上线,很长一段时未针对此项目间做相关布更改和打包发. 由于最近此项目业务甲方需要新增部分功能.但是测试在上传课程时候,需要上传课程封面,发现上传课程封面的图片上传接口报错500啦. 本人在后端日志目录之中也无法查找到报错信息.仅仅只有前后端分离的前端调用接口的时候返回一个如下错误提示 Could not parse multipart serv

  • 如何在SpringBoot项目里进行统一异常处理

    目录 1.处理前 2.进行系统异常全局处理 3.进行自定义异常处理 效果 前言: 需要了解的知识: @ControllerAdvice的作用 1.处理前 异常代码: /** * 根据id获取医院设置 * * @param id 查看的id编号 * @return */ @ApiOperation(value = "根据id获取医院设置") @GetMapping("/findHospById/{id}") public Result findHospById(@Pa

  • Springboot项目异常处理及返回结果统一

    目录 背景 返回结果定义 异常的定义 异常的处理 返回结果的处理 完整代码 使用示例 背景 在创建项目的初期,我们需要规范后端返回的数据结构,以便更好地与前端开发人员合作. 比如后端返回的数据为: {  "msg": "请跳转登陆页面", } 此时前端无法确定后端服务的处理结果是成功的还是失败的.在前端展示页面,成功与失败的展示是要作区分的,甚至不同的成功或失败结果要做出不同的展现效果,这也就是我们为什么要对返回结果做出统一规范的原因. 返回结果定义 public

  • SpringBoot项目中处理返回json的null值(springboot项目为例)

    在后端数据接口项目开发中,经常遇到返回的数据中有null值,导致前端需要进行判断处理,否则容易出现undefined的情况,如何便捷的将null值转换为空字符串? 以SpringBoot项目为例,SSM同理. 1.新建配置类(JsonConfig.java) import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonProcessingException; import com.f

  • 详解如何在SpringBoot项目中使用统一返回结果

    目录 1.创建Spring Boot项目 2.返回结果的封装 3.后端接口实现 3.1 创建实体类 3.2 创建dao层 3.3 创建Controller层 4.前端部分 5.验证 在一个完整的项目中,如果每一个控制器的方法都返回不同的结果,那么对项目的维护和扩展都会很麻烦:并且现在主流的开发模式时前后端分离的模式,如果后端返回各式各样的结果,那么在前后端联调时会非常的麻烦,还会增加前后端的格外任务. 所以,在一个项目中统一返回结果就是一个十分必要和友好的做法.接下来就用一个简单的demo来看看

  • 详解如何在SpringBoot项目中使用全局异常处理

    目录 1. 创建自定义异常 2.创建全局异常处理器 3.创建测试控制器 在完整的项目开发中,异常的出现几乎是无法避免的:如果凡是有可能出现异常的地方,我们都手动的使用try-catch将其捕获的话,虽然也能达到处理异常的效果,但是这样做会使得代码显得十分臃肿并且后期不好维护,也不利于多人系统开发. 在Spring Boot中提供了统一处理异常的方法,SpringBoot中有一个ControllerAdvice的注解,使用该注解表示开启了全局异常的捕获,我们只需在自定义一个方法使用Exceptio

  • springboot 全局异常处理和统一响应对象的处理方式

    目录 springboot异常处理 SpringBoot 默认的异常处理机制 SpringBoot 全局异常处理 1. 局部异常处理 2. 全局异常处理 自定义异常 统一响应对象 定义统一的响应对象 枚举信息 响应对象 springboot异常处理 SpringBoot 默认的异常处理机制 默认情况,SpringBoot 提供两种不同响应方式 一种是浏览器客户端请求一个不存在的页面或服务端异常时,SpringBoot默认会响应一个html 另一种是使用postman等调试工具请求不存在的url或

  • 关于springboot的接口返回值统一标准格式

    目录 一.目标 二.为什么要对springboot的接口返回值统一标准格式? 第一种格式:response为String 第二种格式:response为Objct 第三种格式:response为void 第四种格式:response为异常 三.定义response的标准格式 四.初级程序员对response代码封装 步骤1:把标准格式转换为代码 步骤2:把状态码存在枚举类里面 步骤3:加一个体验类 五.高级程序员对response代码封装 步骤1:采用ResponseBodyAdvice技术来实

  • springboot全局异常处理代码实例

    这篇文章主要介绍了springboot全局异常处理代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 前言: 开发中异常的处理必不可少,常用的就是 throw 和 try catch,这样一个项目到最后会出现很多冗余的代码,使用全局异常处理避免过多冗余代码. 全局异常处理: 1.pom 依赖(延续上一章代码): <dependencies> <!-- Spring Boot Web 依赖 --> <!-- Web 依赖

随机推荐