Spring 统一全局异常处理总结与实战

1,071 阅读7分钟

Spring Mvc 原生异常处理

HandlerExceptionResolver 全局异常解析

1、 HandlerExceptionResolver 源码

HandlerExceptionResolver#resolveException()方法不仅能够拿到发生异常的函数和异常对象,还能够拿到HttpServletRequest、HttpServletResponse对象,从而控制本次请求返回给前端的行为

public interface HandlerExceptionResolver {

	/**
	 * Try to resolve the given exception that got thrown during handler execution,
	 * returning a {@link ModelAndView} that represents a specific error page if appropriate.
	 * <p>The returned {@code ModelAndView} may be {@linkplain ModelAndView#isEmpty() empty}
	 * to indicate that the exception has been resolved successfully but that no view
	 * should be rendered, for instance by setting a status code.
	 * @param request current HTTP request
	 * @param response current HTTP response
	 * @param handler the executed handler, or {@code null} if none chosen at the
	 * time of the exception (for example, if multipart resolution failed)
	 * @param ex the exception that got thrown during handler execution
	 * @return a corresponding {@code ModelAndView} to forward to,
	 * or {@code null} for default processing in the resolution chain
	 */
	@Nullable
	ModelAndView resolveException(
			HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);

}

2、 HandlerExceptionResolver 异常拦截示例

实现HandlerExceptionResolver接口或者继承其子类AbstractHandlerExceptionResolver

  1. resolveException(); 中实现异常处理逻辑。
  2. 将实现类注册为 Spring Bean,Spring 就能扫描自动加载为全局异常处理器
@Slf4j
@Component
public class CustomHandlerExceptionResolver extends AbstractHandlerExceptionResolver {

    @Override
    public ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response,
                                           Object handler, Exception ex) {
        BaseResult result = null;
        // 不同异常处理
        if (ex instanceof ConstraintViolationException) {
            result = resolverConstraintViolationException(ex);
        } else {
            resolverOtherException(ex);
        }
        // 响应JSON配置
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding("UTF-8");
        response.setHeader("Cache-Control", "no-cache, must-revalidate");
        try {
            response.getWriter().write(JSON.toJSONString(result));
        } catch (IOException e) {
            log.error("response writer error", e);
        }
        return new ModelAndView();
    }

    /**
     * 处理参数校验异常
     *
     * @param ex 参数校验异常
     */
    private BaseResult resolverConstraintViolationException(Exception ex) {
        String msg = ((ConstraintViolationException) ex).getConstraintViolations().iterator().next().getMessage();
        return BaseResult.fail(ErrorCodeEnum.PARAMS_FAIL.getErrorCode(), msg);
    }

    /**
     * 处理其他异常
     *
     * @param ex 未配置异常类型
     */
    private BaseResult resolverOtherException(Exception ex) {
        log.warn("response resolver unknown error", ex);
        return BaseResult.fail(ErrorCodeEnum.UNKNOWN_ERROR.getErrorCode(), ErrorCodeEnum.UNKNOWN_ERROR.getErrorMsg());
    }
}

3、优缺点总结

优点:

  • 全局的异常处理器
  • 支持多种格式的响应,虽然方法返回 ModelAndView 但是参数中有 HttpServletResponse, 可以利用它来进行定制响应结果

缺点:

  • 需要与 HttpServletResponse 交互才能实现各种形式的响应体。
  • 优先级比较低
  • 这种方式全局异常处理返回JSP、velocity 等模板视图比较方便,前后端分离不适合

@ExceptionHandler 局部异常处理

1、@ExceptionHandler 注解源码

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionHandler {

	/**
	 * Exceptions handled by the annotated method. If empty, will default to any
	 * exceptions listed in the method argument list.
	 */
	Class<? extends Throwable>[] value() default {};

}

2、@ExceptionHandler 加载流程

3、 结合 @Controller 异常处理示例

在 Controller 中声明异常处理方法,并用@ExceptionHandler 注解标记

@Slf4j
@Validated
@RestController
@RequestMapping("/api/error")
public class ErrorController {

    @GetMapping("/param")
    public BaseResult<String> paramError(@NotBlank(message = "name 不能为空") @RequestParam String name) {
        return BaseResult.success(name);
    }

    /**
     * Controller 内异常处理器
     * 只能处理本Controller 及子类下的局部异常
     */
    @ExceptionHandler({ConstraintViolationException.class})
    public BaseResult handlerError(ConstraintViolationException ex) {
        String msg = ex.getConstraintViolations().iterator().next().getMessage();
        return BaseResult.fail(ErrorCodeEnum.PARAMS_FAIL.getErrorCode(), msg);
    }
}

优缺点总结

优点:

  • 优先级最高。
  • @ExceptionHandler 标记的方法返回值类型支持多种。可以是视图,也可以是 json 等。

缺点:

  • 一个 Controller 中的 @ExceptionHandler 注解上的异常类型不能出现相同的,否则运行时抛异常。
  • 需要显式的声明处理的异常类型。
  • 作用域仅仅是该 Controller 并不是真正意义上的全局异常。如果要想作用于全局需要将其放入所有控制器的父类中。

4、结合 @ControllerAdvice 异常处理示例

@Slf4j
@RestControllerAdvice(basePackageClasses = ErrorControllerAdvice.class)
public class ErrorControllerAdvice {

    @ExceptionHandler({ConstraintViolationException.class})
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public BaseResult handlerError(ConstraintViolationException ex) {
        String msg = ex.getConstraintViolations().iterator().next().getMessage();
        return BaseResult.fail(ErrorCodeEnum.PARAMS_FAIL.getErrorCode(), msg);
    }
}

@ControllerAdvice 限定范围控制

  • 按注解:@ControllerAdvice(annotations = RestController.class)
  • 按包名:@ControllerAdvice("org.example.controllers")
  • 按类型:@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
  • 按类:@RestControllerAdvice(basePackageClasses = ErrorControllerAdvice.class)

优缺点总结

优点:

  • 默认全局的异常处理,可以自定义生效范围,更加灵活。
  • 不同模块间异常处理及返回方式不同,可以通过生效范围控制,实现不同模块的异常处理
  • 可以完全控制响应的主体以及Http状态码

缺点:

  • 一个 Controller 中的 @ExceptionHandler 注解上的异常类型不能出现相同的,否则运行时抛异常。
  • 需要显式的声明处理的异常类型。

Spring Boot 中新增异常处理

Spring Mvc 中提供的异常处理器只能处理 Controller 后抛出的异常,有些请求还没到 Controller 就出现异常了,这些异常就不能被统一异常捕获,例如 Servlet 容器异常。

{
    "timestamp": 1669310139391,
    "status": 404,
    "error": "Not Found",
    "path": "/api/not"
}

身为 Java 开发者对这个页面以及如下返回值肯定不陌生,它就是 Spring Boot 提供的默认异常返回。浏览器访问时会返回默认界面,API 直接调用时会返回默认格式 JSON ,其实现方式就隐藏在 ErrorController 及其默认实现类中。

ErrorController 异常控制器

1、 ErrorController 接口层级

从类的继承关系上能看出 ErrorController 接口及其抽象实现 AbstractErrorController 只是异常处理逻辑抽象,BasicErrorController 中实现了默认的错误页面处理。

2、 BasicErrorController 源码

BasicErrorController 中使用@RequestMapping(produces = {"text/html"}) 实现根据请求类型差异化返回,默认的异常处理地址为server.error.path 中配置值

@Controller
@RequestMapping({"${server.error.path:${error.path:/error}}"})
public class BasicErrorController extends AbstractErrorController {
    private final ErrorProperties errorProperties;

    public BasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties) {
        this(errorAttributes, errorProperties, Collections.emptyList());
    }

    public BasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties, List<ErrorViewResolver> errorViewResolvers) {
        super(errorAttributes, errorViewResolvers);
        Assert.notNull(errorProperties, "ErrorProperties must not be null");
        this.errorProperties = errorProperties;
    }

    // 浏览器请求响应页面
    @RequestMapping(
        produces = {"text/html"}
    )
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
        HttpStatus status = this.getStatus(request);
        Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
        response.setStatus(status.value());
        ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
        return modelAndView != null ? modelAndView : new ModelAndView("error", model);
    }

    // 其他请求响应格式
    @RequestMapping
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        HttpStatus status = this.getStatus(request);
        if (status == HttpStatus.NO_CONTENT) {
            return new ResponseEntity(status);
        } else {
            Map<String, Object> body = this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.ALL));
            return new ResponseEntity(body, status);
        }
    }

    @ExceptionHandler({HttpMediaTypeNotAcceptableException.class})
    public ResponseEntity<String> mediaTypeNotAcceptable(HttpServletRequest request) {
        HttpStatus status = this.getStatus(request);
        return ResponseEntity.status(status).build();
    }


  
}

Spring Boot 在 ErrorMvcAutoConfiguration 中对异常处理提供了自动配置,该配置类向容器中注入了以下 4 个组件。

  • ErrorPageCustomizer:该组件会在在系统发生异常后,默认将请求转发到“/error”上。
  • BasicErrorController:处理默认的“/error”请求。(@ConditionalOnMissingBean 若存在自定义,则使用自定义组件)
  • DefaultErrorViewResolver:默认的错误视图解析器,将异常信息解析到相应的错误视图上。
  • DefaultErrorAttributes:用于页面上共享异常信息。(@ConditionalOnMissingBean 若存在自定义,则使用自定义组件)
        // 寻找 ErrorPage
        ErrorPage errorPage = context.findErrorPage(statusCode);
        if (errorPage == null) {
            // Look for a default error page
            errorPage = context.findErrorPage(0);
        }
        if (errorPage != null && response.isErrorReportRequired()) {
            response.setAppCommitted(false);
            request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE,
                              Integer.valueOf(statusCode));

            String message = response.getMessage();
            if (message == null) {
                message = "";
            }
            request.setAttribute(RequestDispatcher.ERROR_MESSAGE, message);
            request.setAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR,
                    errorPage.getLocation());
            request.setAttribute(Globals.DISPATCHER_TYPE_ATTR,
                    DispatcherType.ERROR);


            Wrapper wrapper = request.getWrapper();
            if (wrapper != null) {
                request.setAttribute(RequestDispatcher.ERROR_SERVLET_NAME,
                                  wrapper.getName());
            }
            request.setAttribute(RequestDispatcher.ERROR_REQUEST_URI,
                                 request.getRequestURI());
            // 该方法会转发到 ErrorPage 
            if (custom(request, response, errorPage)) {
                response.setErrorReported();
                try {
                    response.finishResponse();
                } catch (ClientAbortException e) {
                    // Ignore
                } catch (IOException e) {
                    container.getLogger().warn("Exception Processing " + errorPage, e);
                }
            }
        }

custom(Request request, Response response, ErrorPage errorPage)方法中重定向到错误页面

            if (response.isCommitted()) {
                // Response is committed - including the error page is the
                // best we can do
                rd.include(request.getRequest(), response.getResponse());
            } else {
                // Reset the response (keeping the real error code and message)
                response.resetBuffer(true);
                response.setContentLength(-1);
                // forward 服务端转发
                rd.forward(request.getRequest(), response.getResponse());

                // If we forward, the response is suspended again
                response.setSuspended(false);
            }

3、自定义 ErrorController 使用

配置不同的生效范围,分别处理 Controller 异常及 其他异常

  1. CustomControllerAdvice 处理 Controller 异常
  2. GlobalControllerAdvice 处理除 Controller 之外的其他异常
// Controller 异常处理
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE)
@RestControllerAdvice(basePackageClasses = CustomControllerAdvice.class)
public class CustomControllerAdvice extends AbstractErrorController {

    public CustomErrorControllerAdvice(ErrorAttributes errorAttributes) {
        super(errorAttributes);
    }

    /**
     * 参数校验异常处理
     *
     * @param ex
     * @return
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public BaseResult validErrorHandler(ConstraintViolationException ex) {
        String msg = ex.getConstraintViolations().iterator().next().getMessage();
        return BaseResult.fail(ErrorCodeEnum.PARAMS_FAIL.getErrorCode(), msg);
    }

    /**
     * 全局异常捕捉处理
     *
     * @param ex
     * @return
     */
    @ExceptionHandler(Exception.class)
    public BaseResult<Void> errorHandler(Exception ex) throws ServletException {
        log.error("controller execute error", ex);
        if (ex instanceof ServletException) {
            // 框架相关异常不处理
            throw (ServletException) ex;
        }
        return BaseResult.fail(ErrorCodeEnum.SYS_FAIL.getErrorCode(), ErrorCodeEnum.SYS_FAIL.getErrorMsg());
    }
}
// 全局异常处理
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE)
@RestControllerAdvice
@RequestMapping("/error")
public class GlobalControllerAdvice extends AbstractErrorController {


    public GlobalControllerAdvice(@Autowired ErrorAttributes errorAttributes) {
        super(errorAttributes);
    }

    @ExceptionHandler(NoHandlerFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public BaseResult<?> handleNoHandlerFoundException() {
        return BaseResult.fail(ErrorCodeEnum.URL_NOT_FOUND.getErrorCode(),
            ErrorCodeEnum.URL_NOT_FOUND.getErrorMsg());
    }

    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
    public BaseResult<?> handleHttpRequestMethodNotSupportedException(HttpServletRequest request) {
        return BaseResult.fail(ErrorCodeEnum.SYS_FAIL.getErrorCode(), ErrorCodeEnum.SYS_FAIL.getErrorMsg());
    }

    @ExceptionHandler(HttpMediaTypeNotSupportedException.class)
    @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
    public BaseResult<?> handleHttpMediaTypeNotSupportedException(HttpServletRequest request) {
        return BaseResult.fail(ErrorCodeEnum.SYS_FAIL.getErrorCode(), ErrorCodeEnum.SYS_FAIL.getErrorMsg());
    }

    // 自定义错误返回
    @RequestMapping
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public BaseResult<?> error(HttpServletRequest request) {
        return BaseResult.fail(ErrorCodeEnum.UNKNOWN_ERROR.getErrorCode(), ErrorCodeEnum.UNKNOWN_ERROR.getErrorMsg());
    }
}

总结

以上Spring专门为异常处理设计的机制。由于ControllerAdvice具有更细粒度的控制能力,所以我更偏爱于在系统中使用ControllerAdvice进行统一异常处理。

除了用异常来传递系统中的意外错误,也会用它来传递处于接口行为一部分的业务错误。
这也是异常的优点之一,如果接口的实现比较复杂,分多层函数实现,如果直接传递错误码,那么到Controller的路径上的每一层函数都需要检查错误码,退回到了C语言那种可怕的“写一行语句检查一下错误码”的模式。

当然,理论上,任何能够给Controller加切面的机制都能变相的进行统一异常处理。比如:

  1. 在拦截器内捕获Controller的异常,做统一异常处理。
  2. 使用Spring的AOP机制,做统一异常处理。

参考链接: