spring boot 全局错误处理

刘祖明

这两天在做 spring cloud 的 API gateway 的时候,遇到了一个全局错误处理的坑,我在在 spring security 中加入了一个 filter,该 filter 用来验证 token 是否合法,如果该 token 不合法,就抛出自定义错误 InvalidTokenException ,并且要返回的状态码为403,告诉前端该用户未认证。

我开始以为使用 @ControllerAdvice 来定义全局错误处理即可,最后发现过滤器中抛出了错误 @ControllerAdvice 却无法捕获到,并抛出我需要的错误信息。最后阅读 spring 的官方文档发现,spring 的全局错误处理不是只有 @ControllerAdvice

@ControllerAdvice 主要处理的就是 controller 层的错误信息,而没有进入 controller 层的错误 @ControllerAdvice 是无法处理的,那么我需要另外的一个全局错误处理。


@ControllerAdvice
public class ExceptionTranslator {

    @ExceptionHandler(ConcurrencyFailureException.class)
    @ResponseStatus(HttpStatus.CONFLICT)
    @ResponseBody
    public ErrorVM processConcurencyError(ConcurrencyFailureException ex) {
        return new ErrorVM(ErrorConstants.ERR_CONCURRENCY_FAILURE);
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ResponseBody
    public ErrorVM processValidationError(MethodArgumentNotValidException ex) {
        BindingResult result = ex.getBindingResult();
        List<FieldError> fieldErrors = result.getFieldErrors();

        return processFieldErrors(fieldErrors);
    }



    @ExceptionHandler(CustomParameterizedException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ResponseBody
    public ParameterizedErrorVM processParameterizedValidationError(CustomParameterizedException ex) {
        return ex.getErrorVM();
    }

    @ExceptionHandler(InvalidTokenException.class)
    @ResponseStatus(HttpStatus.FORBIDDEN)
    @ResponseBody
    public ErrorVM processInvalidTokenException(InvalidTokenException ex) {
        return new ErrorVM(ErrorConstants.INVALID_TOKEN, ex.getMessage());
    }

    @ExceptionHandler(AccessDeniedException.class)
    @ResponseStatus(HttpStatus.FORBIDDEN)
    @ResponseBody
    public ErrorVM processAccessDeniedException(AccessDeniedException e) {
        return new ErrorVM(ErrorConstants.ERR_ACCESS_DENIED, e.getMessage());
    }

    private ErrorVM processFieldErrors(List<FieldError> fieldErrors) {
        ErrorVM dto = Objects.nonNull(fieldErrors.get(0)) ? new ErrorVM(ErrorConstants.ERR_VALIDATION, fieldErrors.get(0).getDefaultMessage()) : new ErrorVM(ErrorConstants.ERR_VALIDATION);

        for (FieldError fieldError : fieldErrors) {
            dto.add(fieldError.getObjectName(), fieldError.getField(), fieldError.getCode());
        }

        return dto;
    }

    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    @ResponseBody
    @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
    public ErrorVM processMethodNotSupportedException(HttpRequestMethodNotSupportedException exception) {
        return new ErrorVM(ErrorConstants.ERR_METHOD_NOT_SUPPORTED, exception.getMessage());
    }

    @ExceptionHandler(CustomException.class)
    @ResponseBody
    @ResponseStatus(HttpStatus.IM_USED)
    public ErrorVM processCustomException(CustomException ex) {
        return new ErrorVM(ErrorConstants.ERR_CUSTOM, ex.getMessage());
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorVM> processRuntimeException(Exception ex) {
        BodyBuilder builder;
        ErrorVM errorVM;
        ResponseStatus responseStatus = AnnotationUtils.findAnnotation(ex.getClass(), ResponseStatus.class);
        if (responseStatus != null) {
            builder = ResponseEntity.status(responseStatus.value());
            errorVM = new ErrorVM("error." + responseStatus.value().value(), responseStatus.reason());
        } else {
            builder = ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR);
            errorVM = new ErrorVM(ErrorConstants.ERR_INTERNAL_SERVER_ERROR, "Internal server error");
        }
        return builder.body(errorVM);
    }
}

BasicErrorController 这个类就是用来捕获 /error 的所有错误,而过滤器中的错误会被重定向到 /error。我编写一个新的控制层类 TokenErrorController 并继承 BasicErrorController 类,这样错误 json 类型的错误都会被重定向到这个控制层里。


@RestController
public class TokenErrorController extends BasicErrorController {


    public TokenErrorController(){
        super(new DefaultErrorAttributes(), new ErrorProperties());
    }

    private static final String PATH = "/error";

    @RequestMapping(produces = {MediaType.APPLICATION_JSON_VALUE})
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        Map<String, Object> body = getErrorAttributes(request,
                isIncludeStackTrace(request, MediaType.ALL));
        HttpStatus status = getStatus(request);
        if (!Strings.isNullOrEmpty((String)body.get("exception")) && body.get("exception").equals(InvalidTokenException.class.getName())){
            body.put("status", HttpStatus.FORBIDDEN.value());
            status = HttpStatus.FORBIDDEN;
        }
        return new ResponseEntity<Map<String, Object>>(body, status);
    }

    @Override
    public String getErrorPath() {
        return PATH;
    }
}
阅读 19.1k

我的java进阶之旅
记录我的进阶之旅
82 声望
6 粉丝
0 条评论
你知道吗?

82 声望
6 粉丝
宣传栏