java web 最佳实践_Java Web统一异常处理的最佳实践

背景

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 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 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 handleInvalidRequest(MethodArgumentNotValidException ex, HttpServletRequest request) {

String path = request.getRequestURI();

Map 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 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 data = newHashMap();

protected AppException(ErrorCode code, Map data) {

super(format(code, data));

this.code = code;

if (!isEmpty(data)) {

this.data.putAll(data);

}

}

protected AppException(ErrorCode code, Map 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 data) {

return String.format("[%s]%s:%s.", errorCode.toString(), errorCode.getMessage(), isEmpty(data) ? "" : data.toString());

}

public ErrorCode getCode() {

return code;

}

public Map 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 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 handleInvalidRequest(MethodArgumentNotValidException ex, HttpServletRequest request) {

String path = request.getRequestURI();

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

}

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值