作者简介:大家好,我是码炫码哥,前中兴通讯、美团架构师,现任某互联网公司CTO,兼职码炫课堂主讲源码系列专题
代表作:《jdk源码&多线程&高并发》,《深入tomcat源码解析》,《深入netty源码解析》,《深入dubbo源码解析》,《深入springboot源码解析》,《深入spring源码解析》,《深入redis源码解析》等
联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬。码炫课堂的个人空间-码炫码哥个人主页-面试,源码等
回答
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("/mx")
public String mx() {
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);
}
}
当 mx() 抛出 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);
}
// ...
}
Spring MVC 统一异常处理方式解析
630

被折叠的 条评论
为什么被折叠?



