统一异常处理

项目开发中保证零异常似乎是不可能的,不论是系统异常还是程序本身的编码问题造成的异常信息都要以一种约定的数据结构返回,友好的处理方式在前后端分离模式下(后端提供API接口给到前端)能大大增加大家的沟通、工作效率。基于Spring Boot进行异常统一处理,本文中主要用到@ControllerAdvice注解。

快速导航

  • 统一返回数据结构
    • [统一返回数据结构] 定义接口返回数据结构
    • [统一返回数据结构] 数据接口字段模型定义
    • [统一返回数据结构] 封装接口返回方法(成功、失败)
  • 统一异常处理
    • [统一异常处理] 状态消息枚举
    • [统一异常处理] 自定义异常类
    • [统一异常处理] @ControllerAdvice统一处理异常
  • 测试
    • [测试] 测试正常返回及空指针系统异常
    • [测试] 自定义异常测试

本文示例基于Spring Boot实战系列(3)AOP面向切面编程 /chapter3/chapter3-1可在Github获取源码

统一返回数据结构

定义接口返回数据结构

先定义接口返回数据结构,code为0表示操作成功,非0表示异常。其中data只有在处理成功才会返回,其他情况不会返回,或者那些不需要返回数据的接口(更新、删除…)

  1. {
  2. "code": 0,
  3. "message": "SUCCESS",
  4. "data": {
  5. }
  6. }

数据接口字段模型定义

创建/domain/Result.java类,对以上数据接口涉及的字段进行定义。

  1. ```java
  2. package com.angelo.domain;
  3. public class Result<T> {
  4. private Integer code; // 状态码
  5. private String message; // 状态描述信息
  6. private T data; // 定义为范型
  7. // 以下 getter、setter方法省略
  8. }

封装接口返回方法

创建/util/MessageUtil.java类,对返回的成功、失败进行统一封装。

  1. ```java
  2. package com.angelo.util;
  3. import com.angelo.domain.Result;
  4. public class MessageUtil {
  5. /**
  6. * 成功方法
  7. * @param object
  8. * @return
  9. */
  10. public static Result success(Object object) {
  11. Result result = new Result();
  12. result.setCode(0);
  13. result.setMessage("SUCCESS");
  14. if (object != null) {
  15. result.setData(object);
  16. }
  17. return result;
  18. }
  19. /**
  20. * 成功但是
  21. */
  22. public static Result success() {
  23. return success(null);
  24. }
  25. /**
  26. * 失败方法
  27. * @param code
  28. * @param message
  29. * @return
  30. */
  31. public static Result error(Integer code, String message) {
  32. Result result = new Result();
  33. result.setCode(code);
  34. result.setMessage(message);
  35. return result;
  36. }
  37. }

统一异常处理

状态消息枚举

项目用到的状态码、描述信息要有个文件统一去做枚举定义,一方面可以实现复用,另一方面如果状态码、描述有改动只需要在定义枚举的地方改动即可。

新建/enums/MessageEnum.java枚举

  1. package com.angelo.enums;
  2. public enum MessageEnum {
  3. SYSTEM_ERROR(1001, "系统异常"),
  4. NAME_EXCEEDED_CHARRACTER_LIMIT(2000, "姓名超过了限制,最大4个字符!");
  5. private Integer code;
  6. private String message;
  7. MessageEnum(Integer code, String message) {
  8. this.code = code;
  9. this.message = message;
  10. }
  11. public Integer getCode() {
  12. return code;
  13. }
  14. public String getMessage() {
  15. return message;
  16. }
  17. }

自定义异常类

Spring Boot框架只对抛出的RuntimeException异常进行事物回滚,那么Spring Boot封装的RuntimeException异常也是继承的Exception

新建/exception/UserException.java类,继承于RuntimeException

  1. ```java
  2. package com.angelo.exception;
  3. import com.angelo.enums.MessageEnum;
  4. public class UserException extends RuntimeException {
  5. private Integer code;
  6. public UserException(MessageEnum messageEnum) {
  7. super(messageEnum.getMessage());
  8. this.code = messageEnum.getCode();
  9. }
  10. public Integer getCode() {
  11. return code;
  12. }
  13. public void setCode(Integer code) {
  14. this.code = code;
  15. }
  16. }

@ControllerAdvice统一处理异常

关于@ControllerAdvice更多内容可参考官方文档https://docs.spring.io/spring-framework/docs/5.0.0.M1/javadoc-api/org/springframework/web/bind/annotation/ControllerAdvice.html

  • @ControllerAdvice,spring3.2新增加,用于定义 @ExceptionHandler, @InitBinder, 和 @ModelAttribute方法,并应用到所有的@RequestMapping方法。
  • @ExceptionHandler,拦截异常,方法里的value是指需要拦截的异常类型,通过该注解可实现自定义异常处理。

  • 注意: 之前讲过AOP面向切面编程,注解@AfterThrowing会捕捉到项目中的错误信息,如果使用了此注解,它捕获到错误信息之后,会直接返回,是不会触发@ControllerAdvice注解的。
  1. package com.angelo.handle;
  2. import com.angelo.domain.Result;
  3. import com.angelo.exception.UserException;
  4. import com.angelo.util.MessageUtil;
  5. import org.slf4j.Logger;
  6. import org.slf4j.LoggerFactory;
  7. import org.springframework.web.bind.annotation.ControllerAdvice;
  8. import org.springframework.web.bind.annotation.ExceptionHandler;
  9. import org.springframework.web.bind.annotation.ResponseBody;
  10. @ControllerAdvice
  11. public class ExceptionHandle {
  12. private final static Logger logger = LoggerFactory.getLogger(ExceptionHandle.class);
  13. @ExceptionHandler(value = Exception.class)
  14. @ResponseBody
  15. public Result handle(Exception e) {
  16. logger.info("进入error");
  17. // 是否属于自定义异常
  18. if (e instanceof UserException) {
  19. UserException userException = (UserException) e;
  20. return MessageUtil.error(userException.getCode(), userException.getMessage());
  21. } else {
  22. logger.error("系统异常 {}", e);
  23. return MessageUtil.error(1000, "系统异常!");
  24. }
  25. }
  26. }

测试

测试正常返回及空指针系统异常

修改Usercontroller.java类,在查询用户列表接口增加返回值处理,如下所示:

  1. /**
  2. * 查询用户列表
  3. * @return
  4. */
  5. @RequestMapping(value = "/user/list/{exception}")
  6. public Result<User> userList(@PathVariable("exception") Boolean exception) {
  7. if (exception) {
  8. return null; // 测试空指针异常
  9. }
  10. return MessageUtil.success(userRepository.findAll());
  11. }
  • 返回成功

统一异常处理 - 图1

  • 返回系统异常

统一异常处理 - 图2

自定义异常测试

修改Usercontroller.java类,在保存用户信息接口增加姓名长度校验抛出自定义错误,如下所示:

  1. /**
  2. * 保存一个用户
  3. * @param name
  4. * @param age
  5. * @return
  6. */
  7. @PostMapping(value = "/user")
  8. public User userAdd(@RequestBody User userParams) throws Exception {
  9. User user = new User();
  10. user.setName(userParams.getName());
  11. user.setAge(userParams.getAge());
  12. if (userParams.getName().length() > 4) { // 校验测试异常类
  13. throw new UserException(MessageEnum.NAME_EXCEEDED_CHARRACTER_LIMIT);
  14. }
  15. return userRepository.save(user);
  16. }

统一异常处理 - 图3

Github查看本文完整示例 chapter4-1