Spring Boot 默认的异常处理规则
- 默认情况下Spring Boot提供/error处理所有错误映射
- 对于非浏览器的客户端请求会返回Json数据,包含错误信息,Http状态码,异常信息,堆栈信息. 对于浏览器客户端会响应一个 whitelabel 默认HTML白页错误视图
自定义错误处理
-
自定义错误页 当前项目静态路径 或者 模板引擎查找路径下 创建 error目录,该目录下放入4xx.html 5xx.html 404.html等错误页面 如果使用了模板引擎还可以在页面中获取请求域中的数据
-
使用@ControllerAdvice + @ExceptionHandler 定义异常处理逻辑 底层是因为有ExceptionHandlerExceptionResolver的支持
@Slf4j @ControllerAdvice public class MyExceptionHandler { @ExceptionHandler(value = {NullPointerException.class}) public Map<String, Object> handlerNEP(Throwable e) { log.info("null pointer exceptioin !"); return Collections.singletonMap("msg", e.getMessage()); } @ExceptionHandler(value = ArithmeticException.class) public Map<String, Object> handlerArithmetic(Throwable e) { log.info("handlerArithmetic !"); return Collections.singletonMap("msg", e.getMessage()); } }
-
@ResponseStatus + 自定义异常 底层使用ResponseStatusExceptionResolver ,把ResponseStatus的注解信息拿到,底层调用response.sendError(statusCode, resolvedReason);tomcat发送的/error
-
SpringMVC 底层的异常 如参数转换异常 DefaultHandlerExceptionResolver 来处理底层框架的异常 底层调用response.sendError(statusCode, resolvedReason);tomcat发送的/error
-
自定义HandlerExceptionResolver
@Slf4j public class MyCustomerHandlerExceptionResolver implements HandlerExceptionResolver, Ordered { @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { if(ex instanceof MyRuntimeException){ log.error("出现了异常! ", ex); try { response.getWriter().write(ex.getMessage()+" [MyCustomerHandlerExceptionResolver!]"); } catch (IOException e) { e.printStackTrace(); } // 可以自定义往哪里跳 一旦返回了mav 之后 就确定用这个HandlerExceptionResolver 处理 ModelAndView mav = new ModelAndView(); return mav; } return null; } @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; } }
/** * 扩展自己的resolver * @param resolvers */ @Override public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) { resolvers.add(new MyCustomerHandlerExceptionResolver()); }
-
ErrorViewResolver 自定义处理异常
- response.sendError error请求转给controller
- 异常没人能处理 Tomcat底层 response.sendError error请求会转给controller
- basicErrorController 要去的页面地址是ErrorResolver
异常处理默认配置原理
ErrorMvcAutoConfiguration 类管理 SpringMVC的异常处理的默认配置 下面核心分析该配置类中做了哪些工作
添加了如下的组件
-
DefaultErrorAttributes 组件 默认的异常处理包含的属性信息 他实现了ErrorAttributes, HandlerExceptionResolver接口,说明他本身就是一个HandlerExceptionResolver SpringMVC发生错误之后默认的处理返回的数据项就定义在这个类中
-
BasicErrorController 组件 本质就是一个Controller组件 用于接收/error 请求响应
@Controller @RequestMapping("${server.error.path:${error.path:/error}}") // 获取默认的错误路径 public class BasicErrorController extends AbstractErrorController{ // 返回html错误页面 @RequestMapping(produces = MediaType.TEXT_HTML_VALUE) public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { HttpStatus status = getStatus(request); Map<String, Object> model = Collections .unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML))); response.setStatus(status.value()); // 本质是遍历所有的ErrorViewResolver 找到一个合适返回MAV // 默认Spring MVC 只有一个DefaultErrorViewResolver 他里面实现了自定去找我们404.html 4xx.html 5xx.html的逻辑 ModelAndView modelAndView = resolveErrorView(request, response, status, model); return (modelAndView != null) ? modelAndView : new ModelAndView("error", model); } // 返回 json 数据 @RequestMapping public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) { HttpStatus status = getStatus(request); if (status == HttpStatus.NO_CONTENT) { return new ResponseEntity<>(status); } Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL)); return new ResponseEntity<>(body, status); } }
-
DefaultErrorViewResolver 默认的错误视图解析器 该解析器实现了 404.html 5xx.html等相关逻辑页面跳转的逻辑
@Override public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) { // 创建一个MAV返回 传入HttpStatus的 value 如404 ModelAndView modelAndView = resolve(String.valueOf(status.value()), model); if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) { modelAndView = resolve(SERIES_VIEWS.get(status.series()), model); } return modelAndView; } private ModelAndView resolve(String viewName, Map<String, Object> model) { // 拼接 如error/404 String errorViewName = "error/" + viewName; // 查看是否有模板引擎 TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext); // 如果项目中有模板引擎交给模板引擎处理 if (provider != null) { return new ModelAndView(errorViewName, model); } // 遍历寻找 解析视图的地址 errorViewName => /error/404 return resolveResource(errorViewName, model); } private ModelAndView resolveResource(String viewName, Map<String, Object> model) { // 遍历项目/error/目录下的xxx.html文件 是否有跟HttpStatus匹配的 如果没有返回null 此时回到resolveErrorView 方法的第一行执行完毕 如果精准匹配到状态码的新页面 进入下一个逻辑 拿SERIES_VIEWS 再进resolve方法再来匹配 /** ModelAndView modelAndView = resolve(String.valueOf(status.value()), model); // 继续那SERIES_VALUE -> 4xx 5xx if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) { modelAndView = resolve(SERIES_VIEWS.get(status.series()), model); } return modelAndView; */ for (String location : this.resourceProperties.getStaticLocations()) { try { Resource resource = this.applicationContext.getResource(location); resource = resource.createRelative(viewName + ".html"); if (resource.exists()) { return new ModelAndView(new HtmlResourceView(resource), model); } } catch (Exception ex) { } } return null; }
private static final Map<Series, String> SERIES_VIEWS; static { Map<Series, String> views = new EnumMap<>(Series.class); views.put(Series.CLIENT_ERROR, "4xx"); views.put(Series.SERVER_ERROR, "5xx"); SERIES_VIEWS = Collections.unmodifiableMap(views); }
异常处理步骤
-
当调用mv = ha.handle(processedRequest, response, mappedHandler.getHandler());方法之后如果出现异常会进去catch 代码块 将异常包装一下 再次进入processDispatchResult 转发解析逻辑
-
processDispatchResult 一上来就判断 有没有出现异常 如果出现了异常 调用processHandlerException方法进行处理
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception { boolean errorView = false; if (exception != null) { // 如果是ModelAndViewDefiningException 特殊处理 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); } } // ... 省略
-
processHandlerException 方法 循环遍历出所有的handlerExceptionResolver调用他的resolveException方法 哪个Resolver返回ModelAndView 就用这个MAV对象进行返回
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) throws Exception { // Success and error responses may use different content types request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); // 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; } } } if (exMv != null) { if (exMv.isEmpty()) { request.setAttribute(EXCEPTION_ATTRIBUTE, ex); return null; } // We might still need view name translation for a plain error model... if (!exMv.hasView()) { String defaultViewName = getDefaultViewName(request); if (defaultViewName != null) { exMv.setViewName(defaultViewName); } } if (logger.isTraceEnabled()) { logger.trace("Using resolved error view: " + exMv, ex); } else if (logger.isDebugEnabled()) { logger.debug("Using resolved error view: " + exMv); } WebUtils.exposeErrorRequestAttributes(request, ex, getServletName()); return exMv; } throw ex; }
默认Spring MVC有4个 HandlerExceptionResolver
\
-
分别来看看他们4个HandlerExceptionResolver干了什么事情
-
DefaultErrorAttributes 基本啥也没干 就在 请求域中设置了一个属性 因为他返回为null进入下一个resolver
@Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { storeErrorAttributes(request, ex); return null; } private void storeErrorAttributes(HttpServletRequest request, Exception ex) { request.setAttribute(ERROR_ATTRIBUTE, ex); }
-
第二个Resolver 是HandlerExceptionResolverComposite 其实是一个组合的Resolver 他里面包含了3个 resolver ,默认我们自己定义的 HandlerExceptionResolver 通过WebMVCConfigurer的extendHandlerExceptionResolvers方法添加会被加到这个集合中,调用add方法传入 索引可以指定我们自定义resovler的顺序
@Override public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) { resolvers.add(0,new MyCustomerHandlerExceptionResolver()); }
这个组合ResolverComposite 底层也是遍历他所有的resolver
@Override @Nullable public ModelAndView resolveException( HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) { if (this.resolvers != null) { // 遍历他所有包装的resolver for (HandlerExceptionResolver handlerExceptionResolver : this.resolvers) { ModelAndView mav = handlerExceptionResolver.resolveException(request, response, handler, ex); if (mav != null) { return mav; } } } return null; }
-
ExceptionHandlerExceptionResolver 是@ControllerAdvice + @ExceptionHanlder的底层原理核心逻辑在该类中的 doResolveHandlerMethodException方法中,该方法内部的getExceptionHandlerMethod是去找哪个@ExceptionHandler方法来处理
@Override @Nullable protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) { // 最核心的方法 找到哪个处理器来处理 ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception); // 省略非核心逻辑... ModelAndViewContainer mavContainer = new ModelAndViewContainer(); // MAVC 的处理逻辑 if (mavContainer.isRequestHandled()) { return new ModelAndView(); } else { ModelMap model = mavContainer.getModel(); HttpStatus status = mavContainer.getStatus(); ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, status); mav.setViewName(mavContainer.getViewName()); if (!mavContainer.isViewReference()) { mav.setView((View) mavContainer.getView()); } // 如果是重定向页面如何处理 if (model instanceof RedirectAttributes) { Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes(); RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes); } return mav; } }
-
进一步研究getExceptionHandlerMethod 如何帮我们找到具体的HandlerMethod ,先去各种缓存中查询,最核心的在于 最后的遍历循环所有的ExceptionHandlerAdvice 这一个Map Key是ControllerAdviceBean相当于是标记了@ControllerAdvice的类 value是 ExceptionHandlerMethodResolver 相当于是包装了所有该类下标记了@ExceptionHandler注解的方法
@Nullable protected ServletInvocableHandlerMethod getExceptionHandlerMethod( @Nullable HandlerMethod handlerMethod, Exception exception) { Class<?> handlerType = null; // 省略各种从缓存中查询 for (Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) { ControllerAdviceBean advice = entry.getKey(); if (advice.isApplicableToBeanType(handlerType)) { ExceptionHandlerMethodResolver resolver = entry.getValue(); Method method = resolver.resolveMethod(exception); if (method != null) { return new ServletInvocableHandlerMethod(advice.resolveBean(), method); } } } return null; }
底层核心查找逻辑getMappedMethod方法判断异常类型是不是@ExceptionHandler中定义的异常
-
以上逻辑走完只是找到了 应该用哪个@ExceptionHandler方法来处理 还没有真正执行,真正执行的逻辑在ExceptionHnadlerExceptionResolver后面的exceptionHandlerMethod.invokeAndHandle方法
逻辑跟正常流程类似也是调用反射去执行@ExceptionHandler的方法,此时的returnValue 就是调用异常处理器的方法的返回值,后面逻辑一样调用 返回值处理器 进行返回值的处理
-
此时才返回到了DispatcherServlet# processHandlerException方法拿到了ModelAndView,而且如果ModelAndView没有View会默认设置一个根据请求路径得到的默认值 继续走页面渲染逻辑