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
- resolveException(); 中实现异常处理逻辑。
- 将实现类注册为 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 异常及 其他异常
- CustomControllerAdvice 处理 Controller 异常
- 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加切面的机制都能变相的进行统一异常处理。比如:
- 在拦截器内捕获Controller的异常,做统一异常处理。
- 使用Spring的AOP机制,做统一异常处理。
参考链接: