• Springboot项目中的异常处理与返回结果的统一


    背景

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

    1. {
    2.  "msg": "请跳转登陆页面",
    3. }
    4. 复制代码

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

    返回结果定义

    1. public class ResultWrap<T, M> {
    2. // 方便前端判断当前请求处理结果是否正常
    3. private int code;
    4. // 业务处理结果
    5. private T data;
    6. // 产生错误的情况下,提示用户信息
    7. private String message;
    8. // 产生错误情况下的异常堆栈,提示开发人员
    9. private String error;
    10. // 发生错误的时候,返回的附加信息
    11. private M metaInfo;
    12. }
    13. 复制代码

    1.为了把模糊的消息定性,我们给所有的返回结果都带上一个code字段,前端可以根据这个字段来判断我们的处理结果到底是成功的还是失败的;比如code=200的时候,我们的处理结果一定是成功的,其他的code值全都是失败的,并且我们会有多种code值,以便前端页面可以根据不同的code值做出不同的交互动作。

    2.一般在处理成功的情况下,后端会返回一些业务数据,比如返回订单列表等,那么这些数据我们就放在字段data里面,只有业务逻辑处理成功的情况下,data字段里面才可能有数据。

    3.如若我们的处理失败了,那么我们应该会有对应的消息提醒用户为什么处理失败了。比如文件上传失败了,用户名或密码错误等等,都是需要告知用户的。

    4.除了需要告知用户失败原因,我们也需要保留一个字段给开发人员,当错误是服务器内部错误的时候,我们需要让开发人员能第一时间定位是啥原因引起的,我们会把错误的堆栈信息给到error字段。

    5.当发生异常的时候,我们还会需要返回一些额外的补充数据给前端,比如用户登陆失败一次和失败多次需要不同的交互效果,此时我们会在metaInfo里面返回登陆失败次数;或者在用户操作没有权限的时候,根据用户是否已登陆来确定此时是跳转登陆页面还是直接弹窗提示当前操作没有权限。

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

    异常的定义

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

    1. @Data
    2. public class AwesomeException extends Throwable {
    3.  // 错误码
    4. private int code;
    5.  // 提示消息
    6. private String msg;
    7. public AwesomeException(int code, String msg, Exception e) {
    8. super(e);
    9. this.code = code;
    10. this.msg = msg;
    11. }
    12. public AwesomeException(int code, String msg) {
    13. this.code = code;
    14. this.msg = msg;
    15. }
    16. }
    17. 复制代码

    1.我们同样需要一个与返回结果一致的code字段,这样的话,我们在异常处理的时候,才能把当前异常转换成最终的ResultWrap结果。

    2.msg就是在产生异常的时候,需要给到用户的提示消息。

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

    异常的处理

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

    1. import com.example.awesomespring.exception.AwesomeException;
    2. import com.example.awesomespring.vo.ResultWrap;
    3. import lombok.extern.slf4j.Slf4j;
    4. import org.apache.shiro.authz.AuthorizationException;
    5. import org.springframework.web.bind.annotation.ExceptionHandler;
    6. import org.springframework.web.bind.annotation.RestControllerAdvice;
    7. import javax.servlet.http.HttpServletRequest;
    8. import javax.servlet.http.HttpServletResponse;
    9. /**
    10. * @author zouwei
    11. * @className ExceptionHandler
    12. * @date: 2022/8/6 下午10:44
    13. * @description:
    14. */
    15. @Slf4j
    16. @RestControllerAdvice
    17. public class AwesomeExceptionHandler {
    18. /**
    19. * 捕获没有用户权限的异常
    20. *
    21. * @return
    22. */
    23. @ExceptionHandler(AuthorizationException.class)
    24. public ResultWrap handleException(AuthorizationException e) {
    25. return ResultWrap.failure(401, "您暂时没有访问权限!", e);
    26. }
    27. /**
    28. * 处理AwesomeException
    29. *
    30. * @param e
    31. * @param request
    32. * @param response
    33. * @return
    34. */
    35. @ExceptionHandler(AwesomeException.class)
    36. public ResultWrap handleAwesomeException(AwesomeException e, HttpServletRequest request, HttpServletResponse response) {
    37. return ResultWrap.failure(e);
    38. }
    39. /**
    40. * 专门针对运行时异常
    41. *
    42. * @param e
    43. * @return
    44. */
    45. @ExceptionHandler(RuntimeException.class)
    46. public ResultWrap handleRuntimeException(RuntimeException e) {
    47. return ResultWrap.failure(e);
    48. }
    49. }
    50. 复制代码
    1. 在项目中,我们集成了shiro权限管理框架,因为它抛出的异常没有被我们的AwesomeException包装,所以这个AuthorizationException异常需要我们单独处理。
    2. AwesomeException是我们大多数业务逻辑抛出来的异常,我们根据AwesomeException里面的code、msg和它包装的cause,封装成一个最终的响应数据ResultWrap。
    3. 另一个RuntimeException是必须要额外处理的,任何开发人员都无法保证自己的代码是完全没有bug的,任何的空指针异常都会影响用户体验,这种编码性的错误我们需要通过统一的错误处理让它变得更柔和一点。

    返回结果的处理

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

    1. import com.example.awesomespring.vo.ResultWrap;
    2. import org.springframework.core.MethodParameter;
    3. import org.springframework.http.MediaType;
    4. import org.springframework.http.converter.StringHttpMessageConverter;
    5. import org.springframework.http.server.ServerHttpRequest;
    6. import org.springframework.http.server.ServerHttpResponse;
    7. import org.springframework.web.bind.annotation.RestController;
    8. import org.springframework.web.bind.annotation.RestControllerAdvice;
    9. import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
    10. import java.util.Objects;
    11. /**
    12. * @author zouwei
    13. * @className AwesomeResponseAdvice
    14. * @date: 2022/8/7 上午12:16
    15. * @description:
    16. */
    17. @RestControllerAdvice
    18. public class AwesomeResponseAdvice implements ResponseBodyAdvice {
    19. @Override
    20. public boolean supports(MethodParameter returnType, Class converterType) {
    21.    // 如果返回String,那么就不包装了。
    22. if (StringHttpMessageConverter.class.isAssignableFrom(converterType)) {
    23. return false;
    24. }
    25.    // 有一些情况是不需要包装的,比如调用第三方API返回的数据,所以我们做了一个自定义注解来避免所以的结果都被包装成ResultWrap。
    26. boolean ignore = false;
    27. IgnoreResponseAdvice ignoreResponseAdvice =
    28. returnType.getMethodAnnotation(IgnoreResponseAdvice.class);
    29.    // 如果我们在方法上添加了IgnoreResponseAdvice注解,那么就不要拦截包装了
    30. if (Objects.nonNull(ignoreResponseAdvice)) {
    31. ignore = ignoreResponseAdvice.value();
    32. return !ignore;
    33. }
    34.    // 如果我们在类上面添加了IgnoreResponseAdvice注解,也在方法上面添加了IgnoreResponseAdvice注解,那么以方法上的注解为准。
    35. Class clazz = returnType.getDeclaringClass();
    36. ignoreResponseAdvice = clazz.getDeclaredAnnotation(IgnoreResponseAdvice.class);
    37. RestController restController = clazz.getDeclaredAnnotation(RestController.class);
    38. if (Objects.nonNull(ignoreResponseAdvice)) {
    39. ignore = ignoreResponseAdvice.value();
    40. } else if (Objects.isNull(restController)) {
    41. ignore = true;
    42. }
    43. return !ignore;
    44. }
    45. @Override
    46. public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
    47.    // 如果返回结果为null,那么我们直接返回ResultWrap.success()
    48. if (Objects.isNull(body)) {
    49. return ResultWrap.success();
    50. }
    51.    // // 如果返回结果已经是ResultWrap,直接返回
    52. if (body instanceof ResultWrap) {
    53. return body;
    54. }
    55.    // 否则我们把返回结果包装成ResultWrap
    56. return ResultWrap.success(body);
    57. }
    58. }
    59. import java.lang.annotation.ElementType;
    60. import java.lang.annotation.Retention;
    61. import java.lang.annotation.RetentionPolicy;
    62. import java.lang.annotation.Target;
    63. /**
    64. * @author zouwei
    65. * @className IgnoreResponseAdvice
    66. * @date: 2022/8/7 下午12:35
    67. * @description:
    68. */
    69. @Target({ElementType.METHOD, ElementType.TYPE})
    70. @Retention(RetentionPolicy.RUNTIME)
    71. public @interface IgnoreResponseAdvice {
    72.  // 是否忽略ResponseAdvice;决定了是否要包装返回数据
    73. boolean value() default true;
    74. }
    75. 复制代码

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

    完整代码

    ResultWrap.java

    1. import com.example.awesomespring.exception.AwesomeException;
    2. import com.example.awesomespring.util.JsonUtil;
    3. import lombok.AllArgsConstructor;
    4. import lombok.Data;
    5. import org.apache.commons.lang3.StringUtils;
    6. import org.springframework.http.HttpStatus;
    7. import javax.servlet.http.HttpServletResponse;
    8. import java.io.IOException;
    9. import java.io.PrintWriter;
    10. import java.io.StringWriter;
    11. import java.util.Objects;
    12. /**
    13. * @author zouwei
    14. * @className ResultWrap
    15. * @date: 2022/8/2 下午2:02
    16. * @description:
    17. */
    18. @Data
    19. @AllArgsConstructor
    20. public class ResultWrap {
    21. // 方便前端判断当前请求处理结果是否正常
    22. private int code;
    23. // 业务处理结果
    24. private T data;
    25. // 产生错误的情况下,提示用户信息
    26. private String message;
    27. // 产生错误情况下的异常堆栈,提示开发人员
    28. private String error;
    29. // 发生错误的时候,返回的附加信息
    30. private M metaInfo;
    31. /**
    32. * 成功带处理结果
    33. *
    34. * @param data
    35. * @param
    36. * @return
    37. */
    38. public static ResultWrap success(T data) {
    39. return new ResultWrap(HttpStatus.OK.value(), data, StringUtils.EMPTY, StringUtils.EMPTY, null);
    40. }
    41. /**
    42. * 成功不带处理结果
    43. *
    44. * @return
    45. */
    46. public static ResultWrap success() {
    47. return success(HttpStatus.OK.name());
    48. }
    49. /**
    50. * 失败
    51. *
    52. * @param code
    53. * @param message
    54. * @param error
    55. * @return
    56. */
    57. public static ResultWrap failure(int code, String message, String error, M metaInfo) {
    58. return new ResultWrap(code, null, message, error, metaInfo);
    59. }
    60. /**
    61. * 失败
    62. *
    63. * @param code
    64. * @param message
    65. * @param error
    66. * @param metaInfo
    67. * @param
    68. * @return
    69. */
    70. public static ResultWrap failure(int code, String message, Throwable error, M metaInfo) {
    71. String errorMessage = StringUtils.EMPTY;
    72. if (Objects.nonNull(error)) {
    73. errorMessage = toStackTrace(error);
    74. }
    75. return failure(code, message, errorMessage, metaInfo);
    76. }
    77. /**
    78. * 失败
    79. *
    80. * @param code
    81. * @param message
    82. * @param error
    83. * @return
    84. */
    85. public static ResultWrap failure(int code, String message, Throwable error) {
    86. return failure(code, message, error, null);
    87. }
    88. /**
    89. * 失败
    90. *
    91. * @param code
    92. * @param message
    93. * @param metaInfo
    94. * @param
    95. * @return
    96. */
    97. public static ResultWrap failure(int code, String message, M metaInfo) {
    98. return failure(code, message, StringUtils.EMPTY, metaInfo);
    99. }
    100. /**
    101. * 失败
    102. *
    103. * @param e
    104. * @return
    105. */
    106. public static ResultWrap failure(AwesomeException e) {
    107. return failure(e.getCode(), e.getMsg(), e.getCause());
    108. }
    109. /**
    110. * 失败
    111. *
    112. * @param e
    113. * @return
    114. */
    115. public static ResultWrap failure(RuntimeException e) {
    116. return failure(500, "服务异常,请稍后访问!", e.getCause());
    117. }
    118. private static final String APPLICATION_JSON_VALUE = "application/json;charset=UTF-8";
    119. /**
    120. * 把结果写入响应中
    121. *
    122. * @param response
    123. */
    124. public void writeToResponse(HttpServletResponse response) {
    125. int code = this.getCode();
    126. if (Objects.isNull(HttpStatus.resolve(code))) {
    127. response.setStatus(HttpStatus.OK.value());
    128. } else {
    129. response.setStatus(code);
    130. }
    131. response.setContentType(APPLICATION_JSON_VALUE);
    132. try (PrintWriter writer = response.getWriter()) {
    133. writer.write(JsonUtil.obj2String(this));
    134. writer.flush();
    135. } catch (IOException e) {
    136. e.printStackTrace();
    137. }
    138. }
    139. /**
    140. * 获取异常堆栈信息
    141. *
    142. * @param e
    143. * @return
    144. */
    145. private static String toStackTrace(Throwable e) {
    146. if (Objects.isNull(e)) {
    147. return StringUtils.EMPTY;
    148. }
    149. StringWriter sw = new StringWriter();
    150. PrintWriter pw = new PrintWriter(sw);
    151. try {
    152. e.printStackTrace(pw);
    153. return sw.toString();
    154. } catch (Exception e1) {
    155. return StringUtils.EMPTY;
    156. }
    157. }
    158. }
    159. 复制代码

    AwesomeException.java

    1. import lombok.Data;
    2. /**
    3. * @author zouwei
    4. * @className AwesomeException
    5. * @date: 2022/8/6 下午11:00
    6. * @description:
    7. */
    8. @Data
    9. public class AwesomeException extends Throwable {
    10. private int code;
    11. private String msg;
    12. public AwesomeException(int code, String msg, Exception e) {
    13. super(e);
    14. this.code = code;
    15. this.msg = msg;
    16. }
    17. public AwesomeException(int code, String msg) {
    18. this.code = code;
    19. this.msg = msg;
    20. }
    21. }
    22. 复制代码

    AwesomeExceptionHandler.java

    1. import com.example.awesomespring.exception.AwesomeException;
    2. import com.example.awesomespring.vo.ResultWrap;
    3. import lombok.extern.slf4j.Slf4j;
    4. import org.apache.shiro.authz.AuthorizationException;
    5. import org.springframework.web.bind.annotation.ExceptionHandler;
    6. import org.springframework.web.bind.annotation.RestControllerAdvice;
    7. import javax.servlet.http.HttpServletRequest;
    8. import javax.servlet.http.HttpServletResponse;
    9. /**
    10. * @author zouwei
    11. * @className ExceptionHandler
    12. * @date: 2022/8/6 下午10:44
    13. * @description:
    14. */
    15. @Slf4j
    16. @RestControllerAdvice
    17. public class AwesomeExceptionHandler {
    18. /**
    19. * 捕获没有用户权限的异常
    20. *
    21. * @return
    22. */
    23. @ExceptionHandler(AuthorizationException.class)
    24. public ResultWrap handleException(AuthorizationException e) {
    25. return ResultWrap.failure(401, "您暂时没有访问权限!", e);
    26. }
    27. /**
    28. * 处理AwesomeException
    29. *
    30. * @param e
    31. * @param request
    32. * @param response
    33. * @return
    34. */
    35. @ExceptionHandler(AwesomeException.class)
    36. public ResultWrap handleAwesomeException(AwesomeException e, HttpServletRequest request, HttpServletResponse response) {
    37. return ResultWrap.failure(e);
    38. }
    39. /**
    40. * 专门针对运行时异常
    41. *
    42. * @param e
    43. * @return
    44. */
    45. @ExceptionHandler(RuntimeException.class)
    46. public ResultWrap handleRuntimeException(RuntimeException e) {
    47. return ResultWrap.failure(e);
    48. }
    49. }
    50. 复制代码

    IgnoreResponseAdvice.java

    1. import java.lang.annotation.ElementType;
    2. import java.lang.annotation.Retention;
    3. import java.lang.annotation.RetentionPolicy;
    4. import java.lang.annotation.Target;
    5. /**
    6. * @author zouwei
    7. * @className IgnoreResponseAdvice
    8. * @date: 2022/8/7 下午12:35
    9. * @description:
    10. */
    11. @Target({ElementType.METHOD, ElementType.TYPE})
    12. @Retention(RetentionPolicy.RUNTIME)
    13. public @interface IgnoreResponseAdvice {
    14. boolean value() default true;
    15. }
    16. 复制代码

    AwesomeResponseAdvice.java

    1. import com.example.awesomespring.vo.ResultWrap;
    2. import org.springframework.core.MethodParameter;
    3. import org.springframework.http.MediaType;
    4. import org.springframework.http.converter.StringHttpMessageConverter;
    5. import org.springframework.http.server.ServerHttpRequest;
    6. import org.springframework.http.server.ServerHttpResponse;
    7. import org.springframework.web.bind.annotation.RestController;
    8. import org.springframework.web.bind.annotation.RestControllerAdvice;
    9. import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
    10. import java.util.Objects;
    11. /**
    12. * @author zouwei
    13. * @className AwesomeResponseAdvice
    14. * @date: 2022/8/7 上午12:16
    15. * @description:
    16. */
    17. @RestControllerAdvice
    18. public class AwesomeResponseAdvice implements ResponseBodyAdvice {
    19. @Override
    20. public boolean supports(MethodParameter returnType, Class converterType) {
    21. if (StringHttpMessageConverter.class.isAssignableFrom(converterType)) {
    22. return false;
    23. }
    24. boolean ignore = false;
    25. IgnoreResponseAdvice ignoreResponseAdvice =
    26. returnType.getMethodAnnotation(IgnoreResponseAdvice.class);
    27. if (Objects.nonNull(ignoreResponseAdvice)) {
    28. ignore = ignoreResponseAdvice.value();
    29. return !ignore;
    30. }
    31. Class clazz = returnType.getDeclaringClass();
    32. ignoreResponseAdvice = clazz.getDeclaredAnnotation(IgnoreResponseAdvice.class);
    33. RestController restController = clazz.getDeclaredAnnotation(RestController.class);
    34. if (Objects.nonNull(ignoreResponseAdvice)) {
    35. ignore = ignoreResponseAdvice.value();
    36. } else if (Objects.isNull(restController)) {
    37. ignore = true;
    38. }
    39. return !ignore;
    40. }
    41. @Override
    42. public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
    43. if (Objects.isNull(body)) {
    44. return ResultWrap.success();
    45. }
    46. if (body instanceof ResultWrap) {
    47. return body;
    48. }
    49. return ResultWrap.success(body);
    50. }
    51. }
    52. 复制代码

    使用示例

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

     

  • 相关阅读:
    研发必会-异步编程利器之CompletableFuture(上)
    2023最新SSM计算机毕业设计选题大全(附源码+LW)之java新冠疫苗接种管理系统nt3mc
    Can We Edit Multimodal Large Language Models?
    【计算机网络系列】数据链路层④:扩展的以太网及其高速以太网
    JSP自定义标签之自定义分页01
    AXI协议详解(10)-非对齐传输
    Spring框架概述以及入门案例
    GBase 8c 数据库审计概述(二)
    AM@导数求导法则
    LeetCode 2344. 使数组可以被整除的最少删除次数 最大公约数
  • 原文地址:https://blog.csdn.net/m0_71777195/article/details/126266072