背景
Java 包括三种类型的异常: 检查性异常(checked exceptions)、非检查性异常(unchecked Exceptions) 和错误(errors)。
所有不是 Runtime Exception 的异常,统称为 Checked Exception,又被称为检查性异常。这类异常的产生不是程序本身的问题,通常由外界因素造成的。为了预防这些异常产生时,造成程序的中断或得到不正确的结果,Java 要求编写可能产生这类异常的程序代码时,一定要去做异常的处理。
Java 语言将派生于 RuntimeException 类或 Error 类的所有异常称为非检查性异常。
这里我们主要考虑是代码逻辑中的异常处理,所一以主要关注Runtime Exception,也就是非检查性异常。
在web项目中我们通常将Runtime Exception异常定义为以下三个类别:
- 请求参数校验异常
- 业务异常
- 一般应用异常
这里我们遵循以下原则
- 不随意返回多数据类型,统一返回值的规范。
- 不在业务代码中捕获任何异常,全部由 @ControllerAdvice 来处理。
封装统一的异常处理结果
统一的错误处理,自然处理之后错误信息的数据格式应该是统一的。这里的信息通常是给前端使用或者程序员Debug的,所以要求其中包含的内容易读且信息充足。这里给出一个格式的案例:
{
"error": {
"code": "REQUEST_VALIDATION_FAILED",
"status": 400,
"message": "Request data format validation failed",
"path": "/users",
"timestamp": "2020-06-11T09:30:47.678Z",
"data": {
"cause": "'name' is blank. "
}
}
}
由此,我们封装ErrorDetai类来作为错误信息的容器:
public final class ErrorDetail {
private final ErrorCode code;
private final int status;
private final String message;
private final String path;
private final Instant timestamp;
private final Map<String, Object> data = newHashMap();
public ErrorCode getCode() {
return code;
}
public int getStatus() {
return status;
}
public String getMessage() {
return message;
}
public String getPath() {
return path;
}
public Instant getTimestamp() {
return timestamp;
}
public Map<String, Object> getData() {
return unmodifiableMap(data);
}
public HttpStatus httpStatus() {
return code.getStatus();
}
}
然后在信息外面包装错误展示对象ErrorRepresentation,以实现我们最初设计的数据结构:
public class ErrorRepresentation {
private final ErrorDetail error;
private ErrorRepresentation(ErrorDetail error) {
this.error = error;
}
public static ErrorRepresentation from(ErrorDetail error) {
return new ErrorRepresentation(error);
}
public ErrorDetail getError() {
return error;
}
public HttpStatus httpStatus() {
return error.httpStatus();
}
}
在全局处理的时候,返回的Response内容统一包装为ErrorRepresentation类的实例:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(AppException.class)
@ResponseBody
public ResponseEntity<?> handleAppException(AppException ex, HttpServletRequest request) {
log.error("App error:", ex);
ErrorRepresentation representation = ErrorRepresentation.from(ErrorDetail.from(ex, request.getRequestURI()));
return new ResponseEntity<>(representation, new HttpHeaders(), representation.httpStatus());
}
@ExceptionHandler({MethodArgumentNotValidException.class})
@ResponseBody
public ResponseEntity<ErrorRepresentation> handleInvalidRequest(MethodArgumentNotValidException ex, HttpServletRequest request) {
String path = request.getRequestURI();
Map<String, Object> error = ex.getBindingResult().getFieldErrors().stream()
.collect(Collectors.toMap(FieldError::getField, fieldError -> {
String message = fieldError.getDefaultMessage();
return isEmpty(message) ? "No Message" : message;
}));
log.error("Validation error for [{}]:{}", ex.getParameter().getParameterType().getName(), error);
ErrorRepresentation representation = ErrorRepresentation.from(ErrorDetail.from(new RequestValidationException(error), path));
return new ResponseEntity<>(representation, new HttpHeaders(), representation.httpStatus());
}
@ExceptionHandler(ConstraintViolationException.class)
@ResponseBody
public ResponseEntity<?> handleConstraintViolationException(ConstraintViolationException ex, HttpServletRequest request) {
String path = request.getRequestURI();
Map<String, Object> error = new HashMap<>();
error.put("cause", ex.getMessage());
log.error("Error occurred while access[{}]:", ex.getMessage());
ErrorRepresentation representation = ErrorRepresentation.from(ErrorDetail.from(new RequestValidationException(error), path));
return new ResponseEntity<>(representation, new HttpHeaders(), representation.httpStatus());
}
@ExceptionHandler(Throwable.class)
@ResponseBody
public ResponseEntity<?> handleGeneralException(Throwable ex, HttpServletRequest request) {
String path = request.getRequestURI();
log.error("Error occurred while access[{}]:", path, ex);
ErrorRepresentation representation = ErrorRepresentation.from(ErrorDetail.from(new SystemException(ex), path));
return new ResponseEntity<>(representation, new HttpHeaders(), representation.httpStatus());
}
}
业务异常handler
业务异常通常是具有特定业务含义的,非常specific的。但是在全局统一异常处理中,我又期望统一地进行处理。这种场景下,面向对象语言可继承的特性就显得非常契合。我们定义所有的业务异常都继承与一个AppException父类,那么在全局处理的时候,handle住AppException异常就可以起到一夫当关的作用。
GlobalExceptionHandler
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(AppException.class)
@ResponseBody
public ResponseEntity<?> handleAppException(AppException ex, HttpServletRequest request) {
log.error("App error:", ex);
ErrorRepresentation representation = ErrorRepresentation.from(ErrorDetail.from(ex, request.getRequestURI()));
return new ResponseEntity<>(representation, new HttpHeaders(), representation.httpStatus());
}
}
AppException
public abstract class AppException extends RuntimeException {
private final ErrorCode code;
private final Map<String, Object> data = newHashMap();
protected AppException(ErrorCode code, Map<String, Object> data) {
super(format(code, data));
this.code = code;
if (!isEmpty(data)) {
this.data.putAll(data);
}
}
protected AppException(ErrorCode code, Map<String, Object> data, Throwable cause) {
super(format(code, data), cause);
this.code = code;
if (!isEmpty(data)) {
this.data.putAll(data);
}
}
private static String format(ErrorCode errorCode, Map<String, Object> data) {
return String.format("[%s]%s:%s.", errorCode.toString(), errorCode.getMessage(), isEmpty(data) ? "" : data.toString());
}
public ErrorCode getCode() {
return code;
}
public Map<String, Object> getData() {
return unmodifiableMap(data);
}
public HttpStatus httpStatus() {
return code.getStatus();
}
public String userMessage() {
return code.getMessage();
}
}
ErrorDetail适配AppException的构造
public final class ErrorDetail {
private final ErrorCode code;
private final int status;
private final String message;
private final String path;
private final Instant timestamp;
private final Map<String, Object> data = newHashMap();
private ErrorDetail(AppException ex, String path) {
this.code = ex.getCode();
this.status = ex.httpStatus().value();
this.message = ex.userMessage();
this.path = path;
this.timestamp = now();
if (!isEmpty(ex.getData())) {
this.data.putAll(ex.getData());
}
}
public static ErrorDetail from(AppException ex, String path) {
return new ErrorDetail(ex, path);
}
// getters
...
}
举一个业务异常的🌰
UserNotFoundException
public class UserNotFoundException extends AppException {
public UserNotFoundException(String identifier) {
super(USER_NOT_FOUND, ImmutableMap.of("identifier", identifier));
}
}
ErrorCode
public enum ErrorCode {
SYSTEM_ERROR(INTERNAL_SERVER_ERROR, "System error");
private HttpStatus status;
private String message;
ErrorCode(HttpStatus status, String message) {
this.status = status;
this.message = message;
}
// getters
...
}
请求参数校验异常handler
springBoot应用,通常会有MethodArgumentNotValidException、ConstraintViolationException
两种校验异常,分别来自spring framework跟javax
GlobalExceptionHandler
@ExceptionHandler({MethodArgumentNotValidException.class})
@ResponseBody
public ResponseEntity<ErrorRepresentation> handleInvalidRequest(MethodArgumentNotValidException ex, HttpServletRequest request) {
String path = request.getRequestURI();
Map<String, Object> error = ex.getBindingResult().getFieldErrors().stream()
.collect(Collectors.toMap(FieldError::getField, fieldError -> {
String message = fieldError.getDefaultMessage();
return isEmpty(message) ? "No Message" : message;
}));
log.error("Validation error for [{}]:{}", ex.getParameter().getParameterType().getName(), error);
ErrorRepresentation representation = ErrorRepresentation.from(ErrorDetail.from(new RequestValidationException(error), path));
return new ResponseEntity<>(representation, new HttpHeaders(), representation.httpStatus());
}
@ExceptionHandler(ConstraintViolationException.class)
@ResponseBody
public ResponseEntity<?> handleConstraintViolationException(ConstraintViolationException ex, HttpServletRequest request) {
String path = request.getRequestURI();
Map<String, Object> error = new HashMap<>();
error.put("cause", ex.getMessage());
log.error("Error occurred while access[{}]:", ex.getMessage());
ErrorRepresentation representation = ErrorRepresentation.from(ErrorDetail.from(new RequestValidationException(error), path));
return new ResponseEntity<>(representation, new HttpHeaders(), representation.httpStatus());
}
RequestValidationException
public class RequestValidationException extends AppException {
public RequestValidationException(Map<String, Object> data) {
super(REQUEST_VALIDATION_FAILED, data);
}
}
ErrorCode
public enum ErrorCode {
REQUEST_VALIDATION_FAILED(BAD_REQUEST, "Request data format validation failed");
private HttpStatus status;
private String message;
ErrorCode(HttpStatus status, String message) {
this.status = status;
this.message = message;
}
// getters
...
}
一般应用异常的handler
对于其余的应用异常,通常是未知的问题,我们通常会统一通过500错误暴露给用户,我们仍然以一个统一的格式进行全集handle:
@ControllerAdvice
public class GlobalExceptionHandler {
...
@ExceptionHandler(Throwable.class)
@ResponseBody
public ResponseEntity<?> handleGeneralException(Throwable ex, HttpServletRequest request) {
String path = request.getRequestURI();
log.error("Error occurred while access[{}]:", path, ex);
ErrorRepresentation representation = ErrorRepresentation.from(ErrorDetail.from(new SystemException(ex), path));
return new ResponseEntity<>(representation, new HttpHeaders(), representation.httpStatus());
}
}
SystemException
public class SystemException extends AppException {
public SystemException(Throwable cause) {
super(SYSTEM_ERROR, of("detail", cause.getMessage()), cause);
}
}