美文网首页
Java Web统一异常处理的最佳实践

Java Web统一异常处理的最佳实践

作者: 梁某人的剑 | 来源:发表于2020-06-11 20:47 被阅读0次

背景

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);
    }
}

相关文章

  • Java Web统一异常处理的最佳实践

    背景 Java 包括三种类型的异常: 检查性异常(checked exceptions)、非检查性异常(unche...

  • Spring Boot统一异常处理实践

    摘要: SpringBoot异常处理。 原文:Spring MVC/Boot 统一异常处理最佳实践 作者:赵俊 前...

  • springboot 统一异常处理 自定义异常返回

    看完以下三篇就懂了: Spring MVC/Boot 统一异常处理最佳实践讲了异常处理的正确思路:service/...

  • Exception

    Java 中 9 个处理 Exception 的最佳实践 Java 中的异常和处理详解 如何优雅的设计 Java ...

  • Java 最佳实践的经验

    Java 最佳实践的面试问题 包含 Java 中各个部分的最佳实践,如集合,字符串,IO,多线程,错误和异常处理,...

  • 项目开发

    Retrofit封装 RxJava 与 Retrofit 结合的最佳实践 flatmap统一处理异常 Rx处理服务...

  • Java异常处理最佳实践

    在 Java 中处理异常并不是一个简单的事情。不仅仅初学者很难理解,即使一些有经验的开发者也需要花费很多时间来思考...

  • Java异常处理最佳实践

    我们为什么要做异常处理 1、给请求端明确的操作指导。 2、正确记录系统异常时的完整场景,包括代码的调用过程、出错点...

  • Java Web统一异常处理

    前言 日常的开发过程中,我们经常会遇到异常,对于异常处理,大部分人都是try-catch或者直接throw出去不管...

  • @ControllerAdvice

    @ExceptionHandler 异常统一处理 处理web请求中的异常 请求:http://localhos...

网友评论

      本文标题:Java Web统一异常处理的最佳实践

      本文链接:https://www.haomeiwen.com/subject/imyitktx.html