回答
Spring MVC 提供了三种方式来进行统一异常处理:
- 使用
@ExceptionHandler
:使用@ExceptionHandler
标注在 Controller 的方法上面,当 Controller 中的方法抛出指定异常时,Spring 会自动调用标记了@ExceptionHandler
的方法进行处理。但是该方式只对特定Controller 有效。 - 实现
HandlerExceptionResolver
接口:HandlerExceptionResolver
是 Spring MVC 提供的一个异常处理接口,它允许我们再应用层处理所有异常,当一个 Controller 抛出异常时,Spring 会通过HandlerExceptionResolver
来决定如何处理该异常,我们可以实现该接口来定制全局的异常处理逻辑。 - 使用
@ControllerAdvice
+@ExceptionHandler
:目前最主流的处理方式。@ControllerAdvice
允许我们定义一个全局异常处理器,结合@ExceptionHandler
,@ControllerAdvice
就可以捕获所有 Controller 中的异常并统一处理。
详解
使用 @ExceptionHandler
示例如下:
@Controller
public class SkJavaController {
@RequestMapping("/skjava")
public String skjava() {
if (true) {
throw new RuntimeException("抛个异常看看...");
}
return "success";
}
// 使用 @ExceptionHandler 注解处理 RuntimeException 异常
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<String> handleRuntimeException(RuntimeException ex) {
return new ResponseEntity<>("发生了 RuntimeException: " + ex.getMessage(), HttpStatus.BAD_REQUEST);
}
}
当 skjava()
抛出 RuntimeException 异常时,Spring MVC 会 handleRuntimeException()
方法处理该异常,并返回一个带有错误信息和状态码的响应。但是这种方式至对特定的 Controller 有效。如果想要为整个应用提供全局异常处理,则需要使用下面两种方式。
当然,这种方式也不是一无是处,当我们只需要在某些特定的 Controller 中处理特定的异常时,@ExceptionHandler
还是非常合适的。
实现 HandlerExceptionResolver 接口
HandlerExceptionResolver
是 Spring MVC 提供的一个异常处理接口,它允许我们再应用层处理所有异常,当一个 Controller 抛出异常时,Spring 会通过 HandlerExceptionResolver
来决定如何处理该异常,我们可以实现该接口来定制全局的异常处理逻辑。
示例:
@Component
public class SkJavaExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 在这里可以根据异常类型返回不同的视图或响应
ModelAndView modelAndView = new ModelAndView();
if (ex instanceof NullPointerException) {
modelAndView.setViewName("errorPage");
modelAndView.addObject("message", "空指针异常发生");
} else {
modelAndView.setViewName("genericError");
modelAndView.addObject("message", "系统发生了错误");
}
return modelAndView;
}
}
这种方式可以全局捕获所有异常,并且能够根据异常的不同可以灵活定制不同的处理逻辑。
使用 @ControllerAdvice + @ExceptionHandler
这是目前统一异常处理的最主流方式。
@ControllerAdvice
是一个类级别注解,允许我们定义一个全局异常处理器。结合 @ExceptionHandler
,@ControllerAdvice
就可以捕获所有控制器中的异常,并统一处理。@ControllerAdvice
本质上是全局异常处理的一个封装器。
@ControllerAdvice
可以作用于整个应用,处理所有控制器层抛出的异常,而不需要在每个控制器中都单独写异常处理方法。它使得异常处理代码得以集中管理,提升了代码的清晰度和维护性。
示例:
@ControllerAdvice
public class GlobalExceptionHandler {
// 捕获所有异常
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleException(Exception ex) {
return new ResponseEntity<>("发生了异常:" + ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
}
// 捕获特定异常
@ExceptionHandler(NullPointerException.class)
public ResponseEntity<String> handleNullPointerException(NullPointerException ex) {
return new ResponseEntity<>("空指针异常:" + ex.getMessage(), HttpStatus.BAD_REQUEST);
}
}
@ControllerAdvice + @ExceptionHandler 原理分析
@ControllerAdvice 和 @ExceptionHandler 的初始化
@ControllerAdvice
的定义如下:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
//..
}
使用 @Component
,所以 @ControllerAdvice
本质上就是一个 Spring 组件,Spring 容器在启动时会扫描到它并将其注册到 Spring 容器中。@ControllerAdvice
的处理逻辑在 ExceptionHandlerExceptionResolver 中。
public void afterPropertiesSet() {
// Do this first, it may add ResponseBodyAdvice beans
initExceptionHandlerAdviceCache();
// ...
}
调用 initExceptionHandlerAdviceCache()
初始化 ExceptionHandler:
private void initExceptionHandlerAdviceCache() {
if (getApplicationContext() == null) {
return;
}
// 委托 ControllerAdviceBean 来扫描 @ControllerAdvice
List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
for (ControllerAdviceBean adviceBean : adviceBeans) {
Class<?> beanType = adviceBean.getBeanType();
if (beanType == null) {
throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
}
// 封装为 ExceptionHandlerMethodResolver
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
if (resolver.hasExceptionMappings()) {
// 将 ExceptionHandlerMethodResolver 存储到 exceptionHandlerAdviceCache
this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
}
if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
this.responseBodyAdvice.add(adviceBean);
}
}
// ...
}
委托 ControllerAdviceBean 来扫描 @ControllerAdvice
:
public static List<ControllerAdviceBean> findAnnotatedBeans(ApplicationContext context) {
// ...
List<ControllerAdviceBean> adviceBeans = new ArrayList<>();
for (String name : BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory, Object.class)) {
if (!ScopedProxyUtils.isScopedTarget(name)) {
// 扫描 @ControllerAdvice
ControllerAdvice controllerAdvice = beanFactory.findAnnotationOnBean(name, ControllerAdvice.class);
if (controllerAdvice != null) {
// Use the @ControllerAdvice annotation found by findAnnotationOnBean()
// in order to avoid a subsequent lookup of the same annotation.
adviceBeans.add(new ControllerAdviceBean(name, beanFactory, controllerAdvice));
}
}
}
OrderComparator.sort(adviceBeans);
return adviceBeans;
}
当扫描所有的 @ControllerAdvice
标注的类后,ExceptionHandlerExceptionResolver 会对其进行遍历,然后封装为 ExceptionHandlerMethodResolver,并保存到 exceptionHandlerAdviceCache
。
ExceptionHandlerMethodResolver 是 Spring MVC 的异常解析器,其初始化如下:
public ExceptionHandlerMethodResolver(Class<?> handlerType) {
for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) {
for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) {
addExceptionMapping(exceptionType, method);
}
}
}
public static final MethodFilter EXCEPTION_HANDLER_METHODS = method -> AnnotatedElementUtils.hasAnnotation(method, ExceptionHandler.class);
ExceptionHandlerMethodResolver 会查找 @ControllerAdvice
标注的类中包含有 @ExceptionHandler
的方法,同时对 @ExceptionHandler
进行解析,获取其 exceptionType ,将他们添加到 mappedMethods 中,其中 key 为 exceptionType ,value 为对应的 Method。
@ControllerAdvice 和 @ExceptionHandler 处理异常
DispatcherServlet
负责请求的处理流程,其中 processHandlerException()
负责处理异常:
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
@Nullable Object handler, Exception ex) throws Exception {
// ...
// Check registered HandlerExceptionResolvers...
ModelAndView exMv = null;
if (this.handlerExceptionResolvers != null) {
for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
exMv = resolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break;
}
}
}
// ...
}
一次调用 HandlerExceptionResolver 的 resolveException()
,我们一路跟踪,最终会到 ExceptionHandlerExceptionResolver 中的 doResolveHandlerMethodException()
:
protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request,
HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) {
// 获取具体的异常处理方法
ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
// ...
ServletWebRequest webRequest = new ServletWebRequest(request, response);
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
ArrayList<Throwable> exceptions = new ArrayList<>();
try {
// ...
// 执行异常处理方法
exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, arguments);
}
catch (Throwable invocationEx) {
// ...
}
// ...
}
调用 getExceptionHandlerMethod()
获取具体的异常处理 HandlerMethod:
protected ServletInvocableHandlerMethod getExceptionHandlerMethod(
@Nullable HandlerMethod handlerMethod, Exception exception) {
Class<?> handlerType = null;
if (handlerMethod != null) {
// 从 exceptionHandlerCache 中获取 ExceptionHandlerMethodResolver
handlerType = handlerMethod.getBeanType();
ExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(handlerType);
if (resolver == null) {
// 没有的话,就封装,加入缓存
resolver = new ExceptionHandlerMethodResolver(handlerType);
this.exceptionHandlerCache.put(handlerType, resolver);
}
// 提取对应的处理方法
Method method = resolver.resolveMethod(exception);
if (method != null) {
return new ServletInvocableHandlerMethod(handlerMethod.getBean(), method, this.applicationContext);
}
// ...
}
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。