Java开发不得不知道的全局异常处理
关于异常处理逻辑,无论是在Controller层、Service层还是其他的什么位置我们需要做到的就是捕获异常并将其转化为自定义异常。捕获抛出异常在service文件夹下创建ExceptionService.javapackage com.javafamily.familydemo.service;import com.javafamily.familydemo.exception.PetsException;import com.javafamily.familydemo.exception.PetsExceptionType;import org.springframework.stereotype.Service;@Servicepublic class ExceptionService { public void sError() { try { // 错误的驱动类 Class.forName("com.mysql.jdbc.xxxx.Driver"); } catch (ClassNotFoundException e) { throw new PetsException( PetsExceptionType.SYSTEM_ERROR, "在XXX业务,sError()方法内,出现ClassNotFoundException,请告知管理员"); } } public void uError(int input) { // 输入小于0,则抛出异常 if (input < 0) { //模拟业务校验失败逻辑 throw new PetsException( PetsExceptionType.USER_INPUT_ERROR, "数据输入错误,请确认后重新输入!"); } }}1234567891011121314151617181920212223242526272829303132复制代码类型:[java]在任意一个controller文件中调用服务层if (id==1){ exceptionService.sError();}else { exceptionService.uError(-1);}12345复制代码类型:[java]全局异常处理器@ControllerAdvice监听所有Controller与@ExceptionHandler注解相结合,当系统里面抛出的异常是自定义异常就交给下面的方法来处理。使用ExceptionResponse.error(e)包装为通用的接口数据结构返回给前端。package com.javafamily.familydemo.exception;import org.springframework.web.bind.annotation.ControllerAdvice;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.ResponseBody;@ControllerAdvicepublic class HandlerException { // 处理主动转换的自定义异常 @ExceptionHandler(PetsException.class) @ResponseBody public ExceptionResponse petsException(PetsException e) { if (e.getCode() == PetsExceptionType.SYSTEM_ERROR.getCode()) { // 400异常不需要持久化,将异常信息告知用户 } return ExceptionResponse.error(e); } // 处理在程序中未能捕获的异常 @ExceptionHandler(Exception.class) @ResponseBody public ExceptionResponse exception(Exception e) { return ExceptionResponse.error(new PetsException( PetsExceptionType.OTHER_ERROR)); }}123456789101112131415161718192021222324252627复制代码类型:[java]执行程序并在postman中进行访问。根据上面的截图可以看出我们给的状态码与真正的状态码是不相同的,说明我们只是利用了HTTP状态码去表达他的含义。所以我们要将两个状态码统一。在exception文件夹下创建GlobalResponseBodyAdvice实现ResponseBodyAdvice接口,在数据返回给前端之前做最后一步拦截处理。ResponseBodyAdvice的处理过程在全局异常处理的后面。package com.javafamily.familydemo.exception;import org.springframework.core.MethodParameter;import org.springframework.http.HttpStatus;import org.springframework.http.MediaType;import org.springframework.http.server.ServerHttpRequest;import org.springframework.http.server.ServerHttpResponse;import org.springframework.stereotype.Component;import org.springframework.web.bind.annotation.ControllerAdvice;import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;@Component@ControllerAdvice// 在数据返回给前端之前做最后一步拦截处理public class GlobalResponseBodyAdvice implements ResponseBodyAdvice { @Override public boolean supports(MethodParameter returnType, Class converterType) { //return returnType.hasMethodAnnotation(ResponseBody.class); return true; } @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { // 如果响应结果是JSON数据类型 if (selectedContentType.equalsTypeAndSubtype( MediaType.APPLICATION_JSON)) { // 为HTTP响应结果设置状态码,状态码就是Response的code,二者达到统一 response.setStatusCode( HttpStatus.valueOf(((ExceptionResponse) body).getCode()) ); return body; } return body; }}1234567891011121314151617181920212223242526272829303132333435363738394041复制代码类型:[java]执行程序,在postman中进行访问。优化我们有了GlobalResponseBodyAdvice作为在数据返回给前端之前做最后一步拦截处理就不再需要Controller层上封装ExceptionResponse封装类。可以在GlobalResponseBodyAdvice中进行统一的封装。完整代码:package com.javafamily.familydemo.exception;import org.springframework.core.MethodParameter;import org.springframework.http.HttpStatus;import org.springframework.http.MediaType;import org.springframework.http.server.ServerHttpRequest;import org.springframework.http.server.ServerHttpResponse;import org.springframework.stereotype.Component;import org.springframework.web.bind.annotation.ControllerAdvice;import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;@Component@ControllerAdvicepublic class GlobalResponseBodyAdvice implements ResponseBodyAdvice { @Override public boolean supports(MethodParameter methodParameter, Class aClass) { return true; } @Override public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) { // 如果响应结果是JSON数据类型 if (mediaType.equalsTypeAndSubtype( MediaType.APPLICATION_JSON)) { if (body instanceof ExceptionResponse) { ExceptionResponse exceptionResponse = (ExceptionResponse) body; if (exceptionResponse.getCode() != 999) { serverHttpResponse.setStatusCode(HttpStatus.valueOf( exceptionResponse.getCode() )); } return body; } else { serverHttpResponse.setStatusCode(HttpStatus.OK); return ExceptionResponse.success(body); } } return body; }}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748复制代码类型:[java]Controller或全局异常处理响应的结果body是ExceptionResponse直接return给前端。Controller或全局异常处理响应的结果body不是ExceptionResponse,就将body封装为ExceptionResponse之后再return给前端。服务端数据校验的异常处理在上面我们已经讲了一个服务端数据校验的异常处理的代码,通过if判断,如果参数不合法就会抛出自定义的异常。但是这种写法有些麻烦,我们再介绍几种简单的方法。异常校验的规范及常用注解我们在model文件夹下的PetsVO.java的name字段添加@NotEmpty注解。public class PetsVO { private long id; // message是要返回给前端用户看的信息 @NotEmpty(message = "name不能为空") private String name; private String varieties; private Date createTime;}12345678复制代码类型:[java]在controller层的savePets()中添加@Valid注解。除了@NotEmpty注解以外,还有很多常见的注解可以加在PetsVO的属性字段上,然后在参数校验的方法上加@Valid注解进行使用。注解用法@Null被注释的元素必须为 null@NotNull被注释的元素必须不为 null@AssertTrue被注释的元素必须为 true@AssertFalse被注释的元素必须为 false@Min(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值@Max(value)被注释的元素必须是一个数字,其值必须小于等于指定的最大值@DecimalMin(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值@DecimalMax(value)被注释的元素必须是一个数字,其值必须小于等于指定的最大值@Size(max, min)被注释的元素的大小必须在指定的范围内@Digits (integer, fraction)被注释的元素必须是一个数字,其值必须在可接受的范围内@Past被注释的元素必须是一个过去的日期@Future被注释的元素必须是一个将来的日期@Pattern(value)被注释的元素必须符合指定的正则表达式HibernateValidator附加的注解注解用法@Email被注释的元素必须是电子邮箱地址@Length被注释的字符串的大小必须在指定的范围内@NotEmpty被注释的字符串的必须非空@Range被注释的元素必须在合适的范围内执行代码,在postman中进行测试。将name空出来触发异常。用户输入异常的全局处理数据校验失败时会抛出异常BindException或MethodArgumentNotValidException。所以我们对这两种异常做全局处理,防止重复编码带来困扰,使其返回给前端的信息更精确。package com.javafamily.familydemo.exception;import org.springframework.validation.BindException;import org.springframework.validation.FieldError;import org.springframework.web.bind.MethodArgumentNotValidException;import org.springframework.web.bind.annotation.ControllerAdvice;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.ResponseBody;@ControllerAdvicepublic class HandlerException { @ExceptionHandler(MethodArgumentNotValidException.class) @ResponseBody public ExceptionResponse handleBindException(MethodArgumentNotValidException ex) { FieldError fieldError = ex.getBindingResult().getFieldError(); return ExceptionResponse.error(new PetsException(PetsExceptionType.USER_INPUT_ERROR, fieldError.getDefaultMessage())); } @ExceptionHandler(BindException.class) @ResponseBody public ExceptionResponse handleBindException(BindException ex) { FieldError fieldError = ex.getBindingResult().getFieldError(); return ExceptionResponse.error(new PetsException(PetsExceptionType.USER_INPUT_ERROR, fieldError.getDefaultMessage())); } // 处理主动转换的自定义异常 @ExceptionHandler(PetsException.class) @ResponseBody public ExceptionResponse petsException(PetsException e) { if (e.getCode() == PetsExceptionType.SYSTEM_ERROR.getCode()) { // 400异常不需要持久化,将异常信息告知用户 } return ExceptionResponse.error(e); } //处理程序员在程序中未能捕获(遗漏的)异常 @ExceptionHandler(Exception.class) @ResponseBody public ExceptionResponse exception(Exception e) { return ExceptionResponse.error(new PetsException( PetsExceptionType.OTHER_ERROR)); }}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748复制代码类型:[java]给出关于字段校验比较详细的提示信息。断言我们可以通过一下一行断言代码,来代替以上五行代码:Assert.isTrue(input<0,"数据输入错误,请确认后重新输入!");使用org.springframework.util.Assert断言,如果不满足条件就抛出IllegalArgumentException。可以使用下面的全局异常处理函数: