其实这些都应该是SpringMVC的内容,但想到是在学SpringBoot时学到的更深层次一点的内容
就当做是SpringBoot的内容吧,反正SpringBoot也是整合各个框架的一个框架。
先总结一下,SpringBoot或者是SpringMVC异常处理的几种方式
配合thymeleaf模板引擎的使用下,我们直接在thymeleaf/error/ 目录下放置4xx.html,5xx.html
就可以实现,当目标方法执行错误时,如果是状态码是4xx之类的错误就能跳转到4xx.html,5xx之类的错误
就能跳转5xx.html页面,我们在4xx.html和5xx.html可以通过thymeleaf表达式获取错误信息
${status},${message}等等,其中的原理接下来就讲!我们还可以使用@ControllerAdvice+@ExceptionHandler注解来处理全局的异常,该注解底层是
ExceptionHandlerExceptionResolver处理器异常解析器在工作@ResponseStatus+自定义异常类的方式处理目标方法异常,
@ResponseStatus(reason = "用户登录被拒绝",value = HttpStatus.NOT_ACCEPTABLE) public class UserNotFoundException extends RuntimeException {}
@ResponseStatus注解,底层是由ResponseStatusExceptionResolver异常解析器来处理目标方法发生的
异常,在该异常解析器中,会把@ResponseStatus注解中的数据(reason,value)封装成ModelAndView对象
并且再次发送一个/error请求,response.sendError(statusCode,resolvedReason)(表示本请求立即结束,
并发送一个新的请求,/error),该/error请求则会被底层的BasicErrorController进行处理。Spring底层的异常,如参数类型转换异常,底层是DefaultHandlerExceptionResolver处理框架底层的异常
自定义实现HandleExceptionResolver接口处理异常,可以作为默认的全局异常处理规则 在接口方法中,
直接response.sendError(“500”,”错误消息”),表示结束当前请求,重新发送/error请求,让底层默认的
BasicErrorController处理请求 然后BasicErrorController又调用defaultErrorViewResolver对状态码进行解析,并返回ModelAndView,其中视图名要么是/error下的状态码,要么是error,这取决于你有没有在
templates/error下放置状态码.html页面。@Order(value = Ordered.HIGHEST_PRECEDENCE) //优先级,数字越小优先级越高 @Component public class MyHandlerExceptionResolver implements HandlerExceptionResolver { @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { try { response.sendError(500,"错误信息:随便写"); } catch (IOException e) { e.printStackTrace(); } return new ModelAndView(); } }
ErrorViewResolver实现自定义处理异常
通过之前的分析,我们已经知道,SpringBoot在底层已经默认注册了一个DefautErrorViewResolver,
这个错误视图解析器根据状态码找到ViewName,如果你在templates/error目录下设置了以状态码为html
名称的页面,即404.html,5xx.html等等,则viewname=error/404.html,如果你没有设置,则viewName
等于error,最终会找到底层的StaticView对象,也就是渲染白页,即没有设置任何异常处理,就使用这个对象
渲染视图。你可以自定义ErrorViewResolver,达到并不根据状态码设置viewName,或者并不从
templates/error目录下寻找页面。以上就是针对异常的处理方法,可能看的有点懵逼,下面讲讲异常处理的流程
异常处理流程
请求一进来,进入doDispatcher方法,执行目标方法,如果目标方法报错,则返回的ModelAndView为null
// Actually invoke the handler. mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); 如果目标方法发生异常,则此处返回为null.
因为有异常,mv返回为null,接下来就会被catch捕获到,将发生的异常赋值进dispatcherException对象
catch (Exception ex) { dispatchException = ex; }
紧接着继续进行视图渲染步骤,也可以是异常处理环节
//进入该方法进行异常处理(也是我们熟知的视图渲染方法) processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); 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方法说明异常解析器对异常已经解析完毕,到了视图解析的环节。 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); } }
进入
mv = processHandlerException(request, response, handler, exception);
方法,我们可以
看到,在该方法中,使用容器中所有的异常解析器对该异常进行解析。@Nullable 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) { //遍历所有的异常解析器对发生的异常进行解析,返回ModelAndView对象 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; }
那么容器中默认有哪些异常解析器呢?
DefaultErrorAttribute(它是ErrorMvcAutoConfiguration异常自动装配类中注册的)
HandleExceptionResolverComposite(异常的组合类,在该类中又有三个处理器异常解析器)
- ExceptionHandlerExceptionResolver(处理目标方法标注了@ExceptionHandle注解的异常)
- ResponseStatusExceptionResolver(处理目标方法/自定义异常类标注了@ResponseStatus注解
的异常) - DefaultHandlerExceptionResolver(处理springMVC自带的异常,例如参数类型转换等异常)
回到异常流程中,我们的目标方法异常假设是算术异常,并且也没有为其配置异常处理,即没有使用
@ExceptionHandler,没有使用@ResponseStatus+自定义异常类,则容器中所有的异常解析器都处理
不了我们的异常,就会使得exMv(ModelAndView)还是为null,则异常继续往上抛出异常继续往上抛,会经过一系列的方法,执行拦截器的AfterCompletion等等,最终底层会再次发送/error
请求,这个请求是tomcat发送的(response.sendError(statusCode,resolvedReason)),这个请求会被容器中
BasicErrorController控制器进行处理,这个控制器是容器初始化时ErrorMvcAutoConfiguration自动配置类
注册的。//在ErrorMvcAutoConfiguration注册BasicErrorController @Bean @ConditionalOnMissingBean( value = {ErrorController.class}, search = SearchStrategy.CURRENT ) public BasicErrorController basicErrorController(ErrorAttributes errorAttributes, ObjectProvider<ErrorViewResolver> errorViewResolvers) { return new BasicErrorController(errorAttributes, this.serverProperties.getError(), (List)errorViewResolvers.orderedStream().collect(Collectors.toList())); }
/* tomcat发送的/error请求,如果是浏览器客户端,则进入下面这个方法,在该方法中, 调用this.resolveErrorView(request,response,status,model);方法,对请求进行处理 而之前我们自己发送的请求产生的异常在request对象中。 */ @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.isIncludeStackTrace(request, MediaType.TEXT_HTML))); response.setStatus(status.value()); ModelAndView modelAndView = this.resolveErrorView(request,response,status,model); return modelAndView != null ? modelAndView : new ModelAndView("error", model); }
来到
this.resolveErrorView(request,response,status,model);
方法,我们可以看到,这里是通过
错误视图解析器的resolveErrorView对请求进行解析,默认情况下容器中只有一个错误视图解析器,
DefaultErrorViewResolver,就是异常自动配置类在容器初始化时注册的。// @Bean @ConditionalOnBean({DispatcherServlet.class}) @ConditionalOnMissingBean({ErrorViewResolver.class}) DefaultErrorViewResolver conventionErrorViewResolver() { return new DefaultErrorViewResolver(this.applicationContext, this.resourceProperties); }
//可以看到,是调用了容器中的错误视图解析器对请求进行解析,返回一个ModelAndView对象 protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status, Map<String, Object> model) { Iterator var5 = this.errorViewResolvers.iterator(); ModelAndView modelAndView; do { if (!var5.hasNext()) { return null; } ErrorViewResolver resolver = (ErrorViewResolver)var5.next(); modelAndView = resolver.resolveErrorView(request, status, model); } while(modelAndView == null); return modelAndView; }
紧接着,来看看DefaultErrorViewResolver是如何对请求进行解析的吧
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) { ModelAndView modelAndView = this.resolve(String.valueOf(status.value()), model); if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) { modelAndView = this.resolve((String)SERIES_VIEWS.get(status.series()), model); } return modelAndView; } //此处的viewName就是状态码404/5xx等等, private ModelAndView resolve(String viewName, Map<String, Object> model) { String errorViewName = "error/" + viewName; TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext); /* 如果模板引擎在templates/error目录下找到对应的html(以状态码为名),则errorViewName 等于/error/500.html,如果没有找到(你没在该目录下配置对应的html页面),则errorViewName 等于error字符串。 DefaultErrorViewResolver解析器就是根据状态码来设置ModelAndView中的viewName */ return provider != null ? new ModelAndView(errorViewName, model) : this.resolveResource(errorViewName, model); }
OK,至此BasicErrorController已经对/error请求进行处理,并返回了ModelAndView对象,从上面我们可以
知道,ModelAndView中的viewName,要么等于”error”(你没有在templates/error目录下配置状态码.html)
要么等于”/error/500.html”,接下来就是使用容器中的视图解析器根据ViewName解析出View对象,如果viewName等于”/error/500.html”,则thymeleafViewResolver会解析出thymeleafView,接下去就是调用thymeleafView的render方法,进行渲染数据,跳转页面,
如果viewName等于”error”,则会被BeanNameViewResolver解析器进行处理,这个解析器在异常自动
配置类中,也被配置了一个,这个解析器的作用是,以viewName作为Id在容器中找同id的组件,正好
自动配置类也已经在容器启动时配置了一个View,叫StaticView。这个view就是springmvc底层的白页
表示,你什么异常都没处理,目标方法报错后则由StaticView进行渲染产生白页。//异常自动配置类ErrorMvcAutoConfiguration中配置了BeanNameViewResolver视图解析器 @Bean @ConditionalOnMissingBean public BeanNameViewResolver beanNameViewResolver() { BeanNameViewResolver resolver = new BeanNameViewResolver(); resolver.setOrder(2147483637); return resolver; }
private static class StaticView implements View { private static final MediaType TEXT_HTML_UTF8; private static final Log logger; private StaticView() {} public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception { if (response.isCommitted()) { String message = this.getMessage(model); logger.error(message); } else { response.setContentType(TEXT_HTML_UTF8.toString()); StringBuilder builder = new StringBuilder(); Date timestamp = (Date)model.get("timestamp"); Object message = model.get("message"); Object trace = model.get("trace"); if (response.getContentType() == null) { response.setContentType(this.getContentType()); } //添加白页 builder.append("<html><body><h1>Whitelabel Error Page</h1>") .append("<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>") .append("<div id='created'>") .append(timestamp) .append("</div>") .append("<div>There was an unexpected error (type=") .append(this.htmlEscape(model.get("error"))) .append(", status=") .append(this.htmlEscape(model.get("status"))) .append(").</div>"); if (message != null) { builder.append("<div>") .append(this.htmlEscape(message)) .append("</div>"); } if (trace != null) { builder.append("<div style='white-space:pre-wrap;'>") .append(this.htmlEscape(trace)).append("</div>"); } builder.append("</body></html>"); response.getWriter().append(builder.toString()); } }
好了,至此我已经讲完了一个没有被处理的异常是怎样被SpringMVC解析处理生成白页的。