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

背景

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

比如后端返回的数据为:

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

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

返回结果定义

public class ResultWrap {
  //  方便前端判断当前请求处理结果是否正常
  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 {
  //  方便前端判断当前请求处理结果是否正常
  private int code;
  //  业务处理结果
  private T data;
  //  产生错误的情况下,提示用户信息
  private String message;
  //  产生错误情况下的异常堆栈,提示开发人员
  private String error;
  //  发生错误的时候,返回的附加信息
  private M metaInfo;
​
  /**
   * 成功带处理结果
   *
   * @param data
   * @param 
   * @return
   */
  public static  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  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 
   * @return
   */
  public static  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 
   * @return
   */
  public static  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异常处理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

你可能感兴趣的:(Springboot项目异常处理及返回结果统一)