前言
异常的处理在我们的日常开发中是一个绕不过去的坎,在Spring Boot 项目中如何优雅的去处理异常,正是我们这一节课需要研究的方向。
异常的分类
在一个Spring Boot项目中,我们可以把异常分为两种,第一种是请求到达Controller层之前,第二种是到达Controller层之后项目代码中发生的错误。而第一种又可以分为两种错误类型:1. 路径错误 2. 类似于请求方式错误,参数类型不对等类似错误。

定义ReturnVO和ReturnCode
为了保持返回值的统一,我们这里定义了统一返回的类ReturnVO,以及一个记录错误返回码和错误信息的枚举类ReturnCode,而具体的错误信息和错误代码保存到了response.properties中,使用流进行读取。
ReturnVO
1public class ReturnVO { 2 3 private static Properties properties = ReadPropertiesUtil.getProperties(System.getProperty("user.dir") + CommonUrl.RESPONSE_PROP_URL); 4 5 /** 6 * 返回代码 7 */ 8 private String code; 9 10 /** 11 * 返回信息 12 */ 13 private String message; 14 15 /** 16 * 返回数据 17 */ 18 private Object data; 19 20 21 public Object getData() { 22 return data; 23 } 24 25 public void setData(Object data) { 26 this.data = data; 27 } 28 29 public String getMessage() { 30 return message; 31 } 32 33 public void setMessage(String message) { 34 this.message = message; 35 } 36 37 public String getCode() { 38 return code; 39 } 40 41 public void setCode(String code) { 42 this.code = code; 43 } 44 45 /** 46 * 默认构造,返回操作正确的返回代码和信息 47 */ 48 public ReturnVO() { 49 this.setCode(properties.getProperty(ReturnCode.SUCCESS.val())); 50 this.setMessage(properties.getProperty(ReturnCode.SUCCESS.msg())); 51 } 52 53 /** 54 * 返回代码,这里需要在枚举中去定义 55 * @param code 56 */ 57 public ReturnVO(ReturnCode code) { 58 this.setCode(properties.getProperty(code.val())); 59 this.setMessage(properties.getProperty(code.msg())); 60 } 61 62 /** 63 * 返回数据,默认返回正确的code和message 64 * @param data 65 */ 66 public ReturnVO(Object data) { 67 this.setCode(properties.getProperty(ReturnCode.SUCCESS.val())); 68 this.setMessage(properties.getProperty(ReturnCode.SUCCESS.msg())); 69 this.setData(data); 70 } 71 72 /** 73 * 返回错误的代码,以及自定义的错误信息 74 * @param code 75 * @param message 76 */ 77 public ReturnVO(ReturnCode code, String message) { 78 this.setCode(properties.getProperty(code.val())); 79 this.setMessage(message); 80 } 81 82 /** 83 * 返回自定义的code,message,以及data 84 * @param code 85 * @param message 86 * @param data 87 */ 88 public ReturnVO(ReturnCode code, String message, Object data) { 89 this.setCode(code.val()); 90 this.setMessage(message); 91 this.setData(data); 92 } 93 94 @Override 95 public String toString() { 96 return "ReturnVO{" + 97 "code='" + code + '\'' + 98 ", message='" + message + '\'' + 99 ", data=" + data + 100 '}'; 101 } 102}
ReturnCode
其他的错误处理只需要在枚举类中添加对应的异常即可,枚举的名称要定义为异常的名称,这样可以直接不用对其他的代码进行修改,添加一个新的异常时,仅仅添加枚举类中的字段和properties文件中的属性。
1public enum ReturnCode { 2 3 /** 操作成功 */ 4 SUCCESS("SUCCESS_CODE", "SUCCESS_MSG"), 5 6 /** 操作失败 */ 7 FAIL("FAIL_CODE", "FAIL_MSG"), 8 9 /** 空指针异常 */ 10 NullPointerException("NPE_CODE", "NPE_MSG"), 11 12 /** 自定义异常之返回值为空 */ 13 NullResponseException("NRE_CODE", "NRE_MSG"), 14 15 /** 运行时异常 */ 16 RuntimeException("RTE_CODE","RTE_MSG"), 17 18 /** 请求方式错误异常 */ 19 HttpRequestMethodNotSupportedException("REQUEST_METHOD_UNSUPPORTED_CODE","REQUEST_METHOD_UNSUPPORTED_MSG"), 20 21 /** INTERNAL_ERROR */ 22 BindException("BIND_EXCEPTION_CODE","BIND_EXCEPTION_MSG"), 23 24 /** 页面路径不对 */ 25 UrlError("UE_CODE","UE_MSG"); 26 27 private ReturnCode(String value, String msg){ 28 this.val = value; 29 this.msg = msg; 30 } 31 32 public String val() { 33 return val; 34 } 35 36 public String msg() { 37 return msg; 38 } 39 40 private String val; 41 private String msg; 42}
response.properties
这里我自定义了一些异常用于后面的测试,在我们实际的项目中需要定义很多的异常去完善。
1SUCCESS_CODE=2000 2SUCCESS_MSG=操作成功 3 4FAIL_CODE=5000 5FAIL_MSG=操作失败 6 7NPE_CODE=5001 8NPE_MSG=空指针异常 9 10NRE_CODE=5002 11NRE_MSG=返回值为空 12 13RTE_CODE=5001 14RTE_MSG=运行时异常 15 16UE_CODE=404 17UE_MSG=页面路径有误 18 19REQUEST_METHOD_UNSUPPORTED_CODE=4000 20REQUEST_METHOD_UNSUPPORTED_MSG=请求方式异常 21 22BIND_EXCEPTION_CODE=4001 23BIND_EXCEPTION_MSG=请求参数绑定失败
路径错误处理
这里的路径错误处理方式是采用了实现ErrorController接口,然后实现了getErrorPath()方法:
1/** 2 * 请求路径有误 3 * @author yangwei 4 * @since 2019-01-02 18:13 5 */ 6@RestController 7public class RequestExceptionHandler implements ErrorController { 8 9 @Override 10 public String getErrorPath() { 11 return "/error"; 12 } 13 14 @RequestMapping("/error") 15 public ReturnVO errorPage(){ 16 return new ReturnVO(ReturnCode.UrlError); 17 } 18}
这里可以进行测试一下:

使用ControllerAdvice对其他类型的异常进行处理
类似于到达Controller之前的请求参数错误,请求方式错误,数据格式不对等等错误都归类为一种,这里仅仅展示请求方式错误的处理方式。
1/** 2 * 全局异常处理类 3 * @author yangwei 4 * 5 * 用于全局返回json,如需返回ModelAndView请使用ControllerAdvice 6 * 继承了ResponseEntityExceptionHandler,对于一些类似于请求方式异常的异常进行捕获 7 */ 8@RestControllerAdvice 9public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { 10 11 private static Properties properties = ReadPropertiesUtil.getProperties(System.getProperty("user.dir") + CommonUrl.RESPONSE_PROP_URL); 12 13 /** 14 * 重写handleExceptionInternal,自定义处理过程 15 **/ 16 @Override 17 protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body, HttpHeaders headers, HttpStatus status, WebRequest request) { 18 //这里将异常直接传给handlerException()方法进行处理,返回值为OK保证友好的返回,而不是出现500错误码。 19 return new ResponseEntity<>(handlerException(ex), HttpStatus.OK); 20 } 21 22 /** 23 * 异常捕获 24 * @param e 捕获的异常 25 * @return 封装的返回对象 26 **/ 27 @ExceptionHandler(Exception.class) 28 public ReturnVO handlerException(Throwable e) { 29 ReturnVO returnVO = new ReturnVO(); 30 String errorName = e.getClass().getName(); 31 errorName = errorName.substring(errorName.lastIndexOf(".") + 1); 32 //如果没有定义异常,而是直接抛出一个运行时异常,需要进入以下分支 33 if (e.getClass() == RuntimeException.class) { 34 returnVO.setMessage(properties.getProperty(valueOf("RuntimeException").msg()) +": "+ e.getMessage()); 35 returnVO.setCode(properties.getProperty(valueOf("RuntimeException").val())); 36 } else { 37 returnVO.setMessage(properties.getProperty(valueOf(errorName).msg())); 38 returnVO.setCode(properties.getProperty(valueOf(errorName).val())); 39 } 40 return returnVO; 41 } 42}
这里我们可以进行测试:
1@RestController 2@RequestMapping(value = "/user") 3public class UserController { 4 5 @Autowired 6 private IUserService userService; 7 8 @PostMapping(value = "/findAll") 9 public Object findAll() { 10 throw new RuntimeException("ddd"); 11 } 12 13 @RequestMapping(value = "/findAll1") 14 public ReturnVO findAll1(UserDO userDO) { 15 System.out.println(userDO); 16 return new ReturnVO(userService.findAll1()); 17 } 18 19 @RequestMapping(value = "/test") 20 public ReturnVO test() { 21 throw new RuntimeException("测试非自定义运行时异常"); 22 } 23}
直接在浏览器访问findAll,默认为get方法,这里按照我们期望会抛出请求方式异常的错误:

访问findAll1?id=123ss,这里由于我们接受的UserDO中id属性是Integer类型,所以这里报一个参数绑定异常:

访问test,测试非自定义运行时异常:

结合AOP使用,放入公用模块减少代码的重复
我们上节课使用AOP对于全局异常处理进行了一次简单的操作,这节课进行了完善,并将其放入到我们的公用模块,使用时只需导入jar包,然后在启动类配置扫描包路径即可
1/** 2 * 统一封装返回值和异常处理 3 * 4 * @author vi 5 * @since 2018/12/20 6:09 AM 6 */ 7@Slf4j 8@Aspect 9@Order(5) 10@Component 11public class ResponseAop { 12 13 @Autowired 14 private GlobalExceptionHandler exceptionHandler; 15 16 /** 17 * 切点 18 */ 19 @Pointcut("execution(public * indi.viyoung.viboot.*.controller..*(..))") 20 public void httpResponse() { 21 } 22 23 /** 24 * 环切 25 */ 26 @Around("httpResponse()") 27 public ReturnVO handlerController(ProceedingJoinPoint proceedingJoinPoint) { 28 ReturnVO returnVO = new ReturnVO(); 29 try { 30 Object proceed = proceedingJoinPoint.proceed(); 31 if (proceed instanceof ReturnVO) { 32 returnVO = (ReturnVO) proceed; 33 } else { 34 returnVO.setData(proceed); 35 } 36 } catch (Throwable throwable) { 37 // 这里直接调用刚刚我们在handler中编写的方法 38 returnVO = exceptionHandler.handlerException(throwable); 39 } 40 return returnVO; 41 } 42}
做完这些准备工作,以后我们在进行异常处理的时候只需要进行以下几步操作:
- 引入公用模块jar包
- 在启动类上配置扫描包路径
- 如果新增异常的话,在枚举类中新增后,再去properties中进行返回代码和返回信息的编辑即可(注意:枚举类的变量名一定要和异常名保持一致)
公众号

原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知!