其实这些都应该是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解析处理生成白页的。