当客户端/前端向服务端发送一个请求后,这个请求并不是每次都能完全正确的处理,比如出现一些资源不存在、参数错误或者内部错误等信息的时候,就需要将异常反馈给客户端或者前端。那么这就需要程序有完整的异常处理机制。
在 Java 中所有异常的基类都是 Throwable 延伸出来,但通常不使用 Throwable,在该基类延伸出来 Error 和 Exception 两种异常,严格意义上讲 Error 不属于异常,而是错误,Exception 才是异常,两者有什么区别呢?所谓错误就是应用发生错误的是就启动不起来,一般都是操作系统或者是 JVM 级别上发生的错误,一旦发生错误,我们是无法通过代码来进行处理的;但是 Exception 一般是我们可以通过代码来进行处理的,例如我们平时查询数据库没有找到某一条记录,就是一个典型的空异常,再比如我们进行计算的时候,分母为 0, 也会报一个异常。
Error 在平常工作我们是不需要处理,重点就是 Exception,Exception 又分为两种,一种是 CheckedException 异常,另一种是 RuntimeException 异常;这两个异常的区别是 CheckedException 要求必须在代码中进行处理,如果不处理,程序连编译都无法通过;RuntimeException不要求强制进行处理,因为这个异常属于运行的异常,不一定在编译阶段就能发现。如下面示例代码:
Exception:
这里 throw new Exception(), 要么进行 try...catch, 要么使用 throws Exception 抛出去,否则就会提示报错,程序无法编译通过。
下图示例,RuntimeException,即使不做任何处理也不会报错,是可以正常编译的
RuntimeException 是一个继承了 Exception 异常类的具体类,CheckException 是没有这个类的,例如我们自定义一个 HttpException extends RuntimeException,那么这个HttpException 就是一个运行时异常,如果一个 APIException extends Exception,那么就是一个 CheckException 异常。
介绍完了异常的基础知识后,我们如何进行统一异常处理?
1、定义统一返给前端的格式
统一返回给前端的格式是一个 JSON 类型,包含有自定义的 code 码、异常信息以及请求信息
@Getter @Setter public class UnifyResponse { private Integer code; private String message; private String request; public UnifyResponse(Integer code, String message, String request) { this.code = code; this.message = message; this.request = request; } }
2、自定义一个 HttpException 类,用于处理已知异常
@Getter public class HttpException extends RuntimeException { protected Integer code; protected Integer httpStatusCode = 500; }
这里可以根据 HttpStatus 进行定义出来具体的异常类,例如 404 为资源找不到,401 为无权限等
public class NotFoundException extends HttpException { public NotFoundException(int code) { this.code = code; this.httpStatusCode = 404; } }
public class ForbiddenException extends HttpException { public ForbiddenException(int code) { this.code = code; this.httpStatusCode = 401; } }
需要注意的是在 HttpException 中只声明了连个变量,code 和 httpStatus,并没有 message(错误信息),是因为这里我们将错误信息封装到了一个配置文件中,然后通过类读取配置文件,根据 code 码来获取到对应的 message
3、resources下创建一个config/exception-code.properties
gabriel.codes[10000]=通用异常
gabriel.codes[10001]=通用参数错误
4、创建类获取配置文件信息
package com.gx.missyou.core.configuration; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.PropertySource; import org.springframework.stereotype.Component; import java.util.HashMap; import java.util.Map; /** * 该类主要用于根据 code 值获取异常的 message 消息 * SpringBoot 中通过 @PropertySource 注解将类与配置文件进行关联 * 用法:@PropertySource(classpath:config/exception-code.properties) ** 如何将该类中的私有字段 codes 与配置文件中所有的配置相关联? * 1. 配置文件中的 gabriel.codes["xxxxx"] 这里的 codes 会和私有变量 Map
*/ @PropertySource(value = "classpath:/config/exception-code.properties") @ConfigurationProperties(prefix = "gabriel") @Component public class ExceptionCodeConfiguration { private Mapcodes * 中的 codes 自动关联 * 2. 通过 @ConfigurationProperties(prefix="gabriel") 进行配置前缀,这里的 gabriel 必须要和配置 * 文件中的 gabriel.codes[10000] = "通用异常" 前缀 gabriel 保持一致 * * 注意:SpringBoot 自动将配置文件和类进行关联的时候,类是必须要在容器中才可以的,所以在类上必须要添加 * 一个 @Component 注解
codes = new HashMap<>(); public Map getCodes() { return codes; } public void setCodes(Map codes) { this.codes = codes; } public String getMessages(int code) { return codes.get(code); } }
5、完善统一捕获异常文件
@ControllerAdvice public class GlobalExceptionAdvice { @Autowired private ExceptionCodeConfiguration codeConfiguration; // 处理未知异常 @ExceptionHandler(value = Exception.class) @ResponseBody @ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR) public UnifyResponse handleException(HttpServletRequest request, Exception e) { System.out.println("this is handleException"); String uri = request.getRequestURI(); String method = request.getMethod(); System.out.println("异常原因:" + e); return new UnifyResponse(9999, "服务器内部异常", method + " " + uri); } // 处理已知异常(自定义异常) @ExceptionHandler(HttpException.class) public ResponseEntityhandleHttpException(HttpServletRequest request, HttpException e) { String requestUrl = request.getRequestURI(); String method = request.getMethod(); String messages = codeConfiguration.getMessages(e.getCode()); // 组装 ResponseEntity UnifyResponse message = new UnifyResponse(e.getCode(), messages, method + " " + requestUrl); // 设置 HttpHeaders HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); // 设置 HttpStatus HttpStatus httpStatus = HttpStatus.resolve(e.getHttpStatusCode()); assert httpStatus != null; return new ResponseEntity<>(message, headers, httpStatus); } }
添加由于参数校验后返回给前台的的异常信息
@ExceptionHandler(MethodArgumentNotValidException.class) @ResponseStatus(code = HttpStatus.BAD_REQUEST) @ResponseBody public UnifyResponse handleBeanValidation(HttpServletRequest request, MethodArgumentNotValidException e) { String requestUrl = request.getRequestURI(); String method = request.getMethod(); ListallErrors = e.getBindingResult().getAllErrors(); String message = this.formatAllErrorMessages(allErrors); return new UnifyResponse(10001, message, method + " " + requestUrl); } private String formatAllErrorMessages(List errors) { StringBuffer errorMsg = new StringBuffer(); errors.forEach(objectError -> errorMsg.append(objectError.getDefaultMessage()).append(";")); return errorMsg.toString(); }
添加处理URL和查询参数的异常
@ExceptionHandler(ConstraintViolationException.class) @ResponseStatus(code = HttpStatus.BAD_REQUEST) @ResponseBody public UnifyResponse handleConstraintException(HttpServletRequest request, ConstraintViolationException e) { String uri = request.getRequestURI(); String method = request.getMethod(); // 可以直接通过 e.getMessage() 获取到所有的异常信息,也可以通过自定义循环去获取 // String message = e.getMessage(); StringBuilder messages = new StringBuilder(); // 通过循环自定义去获取 message for (ConstraintViolation> error : e.getConstraintViolations()) { messages.append(error.getMessage()).append(";"); } return new UnifyResponse(10001, messages.toString(), method + " " + uri); }