文章目录
1.概述
通常在一个web应用当中,当用户的操作不当,或者程序出现bug,就会出现大量的异常。其中有些异常是需要暴露给用户的,返回一个异常处理界面给用户,而不是直接出现404、500等不人性化的界面。
这就需要有统一的调度,统一的处理这些异常。将程序的异常转换为用户可读的异常。
这篇博客将从异常处理的使用到原理实现进行讲解。
2.ExceptionHandler的使用
比较常用的两种方式,一种是通过xml配置SimpleMappingExceptionResolver,根据异常的类型返回异常处理的视图。另一种是@ExceptionHandler注解,配合@ControllerAdvice使用,编写异常处理类。
2.1 SimpleMappingExceptionResolver 配置
通过xml的配置方式,当出现某个异常的时候,可以返回指定的异常处理界面。配置的方式如下:
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="defaultErrorView" value="error"/>
<property name="exceptionMappings" >
<props>
<prop key="java.io.IOException">IoError</prop>
<prop key="java.lang.IllegalArgumentException">IllegalError</prop>
</props>
</property>
</bean>
配置属性 key是异常的名称,标签中的值就是异常处理对应的页面
例如上面异常为java.io.IOException 它的异常处理的对应页面就是IoError.jsp
2.2 @ExceptionHandler
当一个Controller中有方法加了@ExceptionHandler之后,这个Controller其他方法中没有捕获的异常就会以参数的形式传入加了@ExceptionHandler注解的那个方法中。
例如,在下面的方法中处理异常并返回ModelAndView。
@ExceptionHandler
public ModelAndView handleException(Exception ex) {
System.out.println(ex);
ModelAndView mv = new ModelAndView();
mv.addObject("exception", new RuntimeException("这个是由@ExceptionHandler捕捉的异常"));
mv.setViewName("error");
return mv;
}
@ExceptionHandler 还提供了一个value属性,该属性是一个数组,可以指定多个异常的名称。这样就可以写多个handleException,并且通过value来指定要处理的异常的名称,处理不同的异常。
例如
@ExceptionHandler(value = {IllegalArgumentException.class, IOException.class})
但是@ExceptionHandler 只能拦截这一个Controller中的方法抛出的异常。
所以Spring来提供了一个注解@ControllerAdvice,该注解是加在类上的。表明该类中的ExceptionHandler可以拦截所有Controller中的异常。
@Controller
@ControllerAdvice
public class MethodHandler {
@ExceptionHandler(value = {IllegalArgumentException.class, IOException.class})
public ModelAndView handleException(Exception ex) {
System.out.println(ex);
ModelAndView mv = new ModelAndView();
mv.addObject("exception", new RuntimeException("这个是由@ExceptionHandler捕捉的异常"));
mv.setViewName("error");
return mv;
}
}
所以可以专门写一个Controller 用来写ExceptionHandler,进行统一的异常拦截处理。
3.ExceptionHandler的初始化
上面讲了通过xml配置的方式或者@ExceptionHandler注解的方式可以得到ExceptionHandler。是如何做到的呢?
Spring容器初始化阶段,在初始化ExceptionHandlerExceptionResolver的时候,会执行afterPropertiesSet()方法,这是Bean生命周期中的一步。进一步的初始化Bean。在ExceptionHandlerExceptionResolver的afterPropertiesSet()方法中,会调用initExceptionHandlerAdviceCache()代码如下:
private void initExceptionHandlerAdviceCache() {
if (getApplicationContext() == null) {
return;
}
// 扫描 @ControllerAdvice 注解的Bean,并进行排序
List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
// 遍历 ControllerAdviceBean 数组
for (ControllerAdviceBean adviceBean : adviceBeans) {
Class<?> beanType = adviceBean.getBeanType();
if (beanType == null) {
throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
}
// 扫描该 ControllerAdviceBean 对应的类型
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
// 有 @ExceptionHandler 注解,则添加到 exceptionHandlerAdviceCache 中
if (resolver.hasExceptionMappings()) {
this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
}
// 如果该 beanType 类型是 ResponseBodyAdvice 子类,则添加到 responseBodyAdvice 中
if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
this.responseBodyAdvice.add(adviceBean);
}
}
if (logger.isDebugEnabled()) {
int handlerSize = this.exceptionHandlerAdviceCache.size();
int adviceSize = this.responseBodyAdvice.size();
if (handlerSize == 0 && adviceSize == 0) {
logger.debug("ControllerAdvice beans: none");
}
else {
logger.debug("ControllerAdvice beans: " +
handlerSize + " @ExceptionHandler, " + adviceSize + " ResponseBodyAdvice");
}
}
}
-
首先找到加了注解@ControllerAdvice的Bean
-
遍历找到的所有的Bean,根据Bean类型构建ExceptionHandlerMethodResolver对象。ExceptionHandlerMethodResolver(beanType)方法如下:
public ExceptionHandlerMethodResolver(Class<?> handlerType) { // 遍历 @ExceptionHandler 注解的方法 for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) { // 遍历处理的异常集合 for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) { // 添加到 mappedMethods 中 addExceptionMapping(exceptionType, method); } } }
- 逻辑很简单,就是遍历这个类中的所有的方法,找到加了注解@ExceptionHandler的方法。然后遍历这个方法中的异常类型的映射。也就是该方法可以处理的异常类型。将它添加到mappedMethods当中。key是异常类型,value是对应的异常处理的方法。
-
这样一个类就对应这一个ExceptionHandlerMethodResolver对象,保存在exceptionHandlerAdviceCache缓存当中。就可以做到不仅一个Controller可以使用了。单例池中结果如下:
-
之后在DispatcherServlet初始化的时候,会调用initHandlerExceptionResolvers(),该方法从spring容器中找HandlerExceptionResolver类型的Bean,添加到成员变量handlerExceptionResolvers当中。并排序。
对应xml配置的就更为简单了。
容器初始化的时候,扫描xml配置文件,解析Bean标签,构建SimpleMappingExceptionResolver对象。填充xml中配置的属性。就完成了SimpleMappingExceptionResolver的初始化。
4.ExceptionHandler的触发时机
在doDispatch中有一个局部变量Exception dispatchException = null,用于存储catch到的异常。并在调用processDispatchResult的时候,会将这个局部变量传入。processDispatchResult代码如下:
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {
boolean errorView = false;
if (exception != null) {//异常视图处理
if (exception instanceof ModelAndViewDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
}
else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
mv = processHandlerException(request, response, handler, exception);//执行异常处理
errorView = (mv != null);
}
}
// Did the handler return a view to render?
if (mv != null && !mv.wasCleared()) {
render(mv, request, response);//解析视图 分发结果
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
else {
if (logger.isTraceEnabled()) {
logger.trace("No view rendering, null ModelAndView returned.");
}
}
if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Concurrent handling started during a forward
return;
}
if (mappedHandler != null) {
// Exception (if any) is already handled..
mappedHandler.triggerAfterCompletion(request, response, null);//触发拦截器完成处理
}
}
调用processHandlerException来调用异常处理器处理异常,代码如下:
public ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
// 判断是否可以应用
if (shouldApplyTo(request, handler)) {
// 阻止缓存
prepareResponse(ex, response);
// 执行解析异常,返回 ModelAndView 对象 由子类实现
ModelAndView result = doResolveException(request, response, handler, ex);
// 如果 ModelAndView 对象非空,则进行返回
if (result != null) {
// Print debug message when warn logger is not enabled.
if (logger.isDebugEnabled() && (this.warnLogger == null || !this.warnLogger.isWarnEnabled())) {
logger.debug("Resolved [" + ex + "]" + (result.isEmpty() ? "" : " to " + result));
}
// Explicitly configured warn logger in logException method.
// 打印异常日志
logException(ex, request);
}
// 返回 ModelAndView 对象
return result;
}
else {
return null;
}
}
真正的异常处理是调用doResolveException,由子类实现,根据不同类型的HandlerExceptionResolver执行不同的逻辑。
根据上面异常处理的时机,可以得出的结论是异常处理拦截的是视图解析之前的逻辑。也就是从getHandler开始到执行了拦截器后置处理的地方。