异常的处理在我们的日常开发中是一个绕不过去的坎,在Spring Boot 项目中如何优雅的去处理异常,正是我们这一节课需要研究的方向。
在一个Spring Boot项目中,我们可以把异常分为两种,第一种是请求到达 Controller
层之前,第二种是到达 Controller层
之后项目代码中发生的错误。而第一种又可以分为两种错误类型:1. 路径错误 2. 类似于请求方式错误,参数类型不对等类似错误。
为了保持返回值的统一,我们这里定义了统一返回的类 ReturnVO
,以及一个记录错误返回码和错误信息的枚举类 ReturnCode
,而具体的错误信息和错误代码保存到了 response.properties
中,使用流进行读取。
public class ReturnVO { private static Properties properties = ReadPropertiesUtil.getProperties(System.getProperty("user.dir") + CommonUrl.RESPONSE_PROP_URL); /** * 返回代码 */ private String code; /** * 返回信息 */ private String message; /** * 返回数据 */ private Object data; public Object getData() { return data; } public void setData(Object data) { this.data = data; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public String getCode() { return code; } public void setCode(String code) { this.code = code; } /** * 默认构造,返回操作正确的返回代码和信息 */ public ReturnVO() { this.setCode(properties.getProperty(ReturnCode.SUCCESS.val())); this.setMessage(properties.getProperty(ReturnCode.SUCCESS.msg())); } /** * 返回代码,这里需要在枚举中去定义 * @param code */ public ReturnVO(ReturnCode code) { this.setCode(properties.getProperty(code.val())); this.setMessage(properties.getProperty(code.msg())); } /** * 返回数据,默认返回正确的code和message * @param data */ public ReturnVO(Object data) { this.setCode(properties.getProperty(ReturnCode.SUCCESS.val())); this.setMessage(properties.getProperty(ReturnCode.SUCCESS.msg())); this.setData(data); } /** * 返回错误的代码,以及自定义的错误信息 * @param code * @param message */ public ReturnVO(ReturnCode code, String message) { this.setCode(properties.getProperty(code.val())); this.setMessage(message); } /** * 返回自定义的code,message,以及data * @param code * @param message * @param data */ public ReturnVO(ReturnCode code, String message, Object data) { this.setCode(code.val()); this.setMessage(message); this.setData(data); } @Override public String toString() { return "ReturnVO{" + "code='" + code + '/'' + ", message='" + message + '/'' + ", data=" + data + '}'; } }
其他的错误处理只需要在枚举类中添加对应的异常即可,枚举的名称要定义为异常的名称,这样可以直接不用对其他的代码进行修改,添加一个新的异常时,仅仅添加枚举类中的字段和properties文件中的属性。
public enum ReturnCode { /** 操作成功 */ SUCCESS("SUCCESS_CODE", "SUCCESS_MSG"), /** 操作失败 */ FAIL("FAIL_CODE", "FAIL_MSG"), /** 空指针异常 */ NullPointerException("NPE_CODE", "NPE_MSG"), /** 自定义异常之返回值为空 */ NullResponseException("NRE_CODE", "NRE_MSG"), /** 运行时异常 */ RuntimeException("RTE_CODE","RTE_MSG"), /** 请求方式错误异常 */ HttpRequestMethodNotSupportedException("REQUEST_METHOD_UNSUPPORTED_CODE","REQUEST_METHOD_UNSUPPORTED_MSG"), /** INTERNAL_ERROR */ BindException("BIND_EXCEPTION_CODE","BIND_EXCEPTION_MSG"), /** 页面路径不对 */ UrlError("UE_CODE","UE_MSG"); private ReturnCode(String value, String msg){ this.val = value; this.msg = msg; } public String val() { return val; } public String msg() { return msg; } private String val; private String msg; }
这里我自定义了一些异常用于后面的测试,在我们实际的项目中需要定义很多的异常去完善。
SUCCESS_CODE=2000 SUCCESS_MSG=操作成功 FAIL_CODE=5000 FAIL_MSG=操作失败 NPE_CODE=5001 NPE_MSG=空指针异常 NRE_CODE=5002 NRE_MSG=返回值为空 RTE_CODE=5001 RTE_MSG=运行时异常 UE_CODE=404 UE_MSG=页面路径有误 REQUEST_METHOD_UNSUPPORTED_CODE=4000 REQUEST_METHOD_UNSUPPORTED_MSG=请求方式异常 BIND_EXCEPTION_CODE=4001 BIND_EXCEPTION_MSG=请求参数绑定失败
这里的路径错误处理方式是采用了实现 ErrorController
接口,然后实现了 getErrorPath()
方法:
/** * 请求路径有误 * @author yangwei * @since 2019-01-02 18:13 */ @RestController public class RequestExceptionHandler implements ErrorController { @Override public String getErrorPath() { return "/error"; } @RequestMapping("/error") public ReturnVO errorPage(){ return new ReturnVO(ReturnCode.UrlError); } }
这里可以进行测试一下:
类似于到达 Controller
之前的请求参数错误,请求方式错误,数据格式不对等等错误都归类为一种,这里仅仅展示请求方式错误的处理方式。
/** * 全局异常处理类 * @author yangwei * * 用于全局返回json,如需返回ModelAndView请使用ControllerAdvice * 继承了ResponseEntityExceptionHandler,对于一些类似于请求方式异常的异常进行捕获 */ @RestControllerAdvice public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { private static Properties properties = ReadPropertiesUtil.getProperties(System.getProperty("user.dir") + CommonUrl.RESPONSE_PROP_URL); /** * 重写handleExceptionInternal,自定义处理过程 **/ @Override protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body, HttpHeaders headers, HttpStatus status, WebRequest request) { //这里将异常直接传给handlerException()方法进行处理,返回值为OK保证友好的返回,而不是出现500错误码。 return new ResponseEntity<>(handlerException(ex), HttpStatus.OK); } /** * 异常捕获 * @param e 捕获的异常 * @return 封装的返回对象 **/ @ExceptionHandler(Exception.class) public ReturnVO handlerException(Throwable e) { ReturnVO returnVO = new ReturnVO(); String errorName = e.getClass().getName(); errorName = errorName.substring(errorName.lastIndexOf(".") + 1); //如果没有定义异常,而是直接抛出一个运行时异常,需要进入以下分支 if (e.getClass() == RuntimeException.class) { returnVO.setMessage(properties.getProperty(valueOf("RuntimeException").msg()) +": "+ e.getMessage()); returnVO.setCode(properties.getProperty(valueOf("RuntimeException").val())); } else { returnVO.setMessage(properties.getProperty(valueOf(errorName).msg())); returnVO.setCode(properties.getProperty(valueOf(errorName).val())); } return returnVO; } }
这里我们可以进行测试:
@RestController @RequestMapping(value = "/user") public class UserController { @Autowired private IUserService userService; @PostMapping(value = "/findAll") public Object findAll() { throw new RuntimeException("ddd"); } @RequestMapping(value = "/findAll1") public ReturnVO findAll1(UserDO userDO) { System.out.println(userDO); return new ReturnVO(userService.findAll1()); } @RequestMapping(value = "/test") public ReturnVO test() { throw new RuntimeException("测试非自定义运行时异常"); } }
直接在浏览器访问findAll,默认为get方法,这里按照我们期望会抛出请求方式异常的错误:
访问findAll1?id=123ss,这里由于我们接受的 UserDO
中 id
属性是 Integer
类型,所以这里报一个参数绑定异常:
访问test,测试非自定义运行时异常:
我们上节课使用AOP对于全局异常处理进行了一次简单的操作,这节课进行了完善,并将其放入到我们的公用模块,使用时只需导入jar包,然后在启动类配置扫描包路径即可
/** * 统一封装返回值和异常处理 * * @author vi * @since 2018/12/20 6:09 AM */ @Slf4j @Aspect @Order(5) @Component public class ResponseAop { @Autowired private GlobalExceptionHandler exceptionHandler; /** * 切点 */ @Pointcut("execution(public * indi.viyoung.viboot.*.controller..*(..))") public void httpResponse() { } /** * 环切 */ @Around("httpResponse()") public ReturnVO handlerController(ProceedingJoinPoint proceedingJoinPoint) { ReturnVO returnVO = new ReturnVO(); try { Object proceed = proceedingJoinPoint.proceed(); if (proceed instanceof ReturnVO) { returnVO = (ReturnVO) proceed; } else { returnVO.setData(proceed); } } catch (Throwable throwable) { // 这里直接调用刚刚我们在handler中编写的方法 returnVO = exceptionHandler.handlerException(throwable); } return returnVO; } }
做完这些准备工作,以后我们在进行异常处理的时候只需要进行以下几步操作: