12、SpringMVC进阶:异常处理流程源码分析

前言

首先写一个接口,会抛出算术异常ArithmeticException。

    @GetMapping("/testException")
    @ResponseBody
    public String testException() {
   
     
        int i= 4/0;
        return "testException";
    }

浏览器访问,发现返回了一个页面,但是是说500异常,找不到/error访问路径。
*
使用PostMan访问,发现返回了Json数据。
*
那么Spring MVC中时如何进行异常处理的呢?

核心类简介

spring-framework

spring原生框架提供了核心的异常处理机制。

HandlerExceptionResolver

HandlerExceptionResolver接口,控制器异常解析器,对处理映射器或执行期间抛出的异常进行解析处理。

其定义了一个解析异常方法,尝试解决执行期间抛出的给定异常,返回一个ModelAndView对象,然后可以通过ModelAndView 返回异常页面。

public interface HandlerExceptionResolver {
   
     
	ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex);
}

在spring-framework中,HandlerExceptionResolver有这些实现类。
*

HandlerExceptionResolverComposite

HandlerExceptionResolverComposite是一个异常解析器的组合类,它成员属性维护了一个解析器集合。

@Nullable
private List<HandlerExceptionResolver> resolvers;

SimpleMappingExceptionResolver

SimpleMappingExceptionResolver是一个基础的异常解析器,可以将将异常类名映射为视图名,即发生异常时使用对应的视图返回异常。

可以通过这个解析器,配置异常页面。

ExceptionHandlerExceptionResolver

ExceptionHandlerExceptionResolver处理使用@ExceptionHandler注解方法,并调用它来处理引发的异常。

DefaultHandlerExceptionResolver

DefaultHandlerExceptionResolver可以解析 Spring MVC 异常并将它们转换为相应的HTTP 状态代码。

ResponseStatusExceptionResolver

处理使用了@ResponseStatus注解的相关异常。

spring-boot-project

在spring-boot的spring-boot-project模块中,定义了两个接口,用来处理异常。
*

ErrorController

异常处理控制器接口,我们可以自己写一个@Controller控制器实现该接口,来实现异常时,错误处理。

public interface ErrorController {
   
     
}

ErrorAttributes

ErrorAttributes错误属性接口,提供了对错误属性的封装。

public interface ErrorAttributes {
   
     
	String ERROR_ATTRIBUTE = ErrorAttributes.class.getName() + ".error";
	// 将错误信息封装为Map,然后可以使用Json 返回给客户端。
	default Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
   
     
		return Collections.emptyMap();
	}
	// 在webRequest中获取异常
	Throwable getError(WebRequest webRequest);
}

DefaultErrorAttributes

DefaultErrorAttributes实现了ErrorAttributes和HandlerExceptionResolver接口。

它的作用主要是将错误信息放在request对象中。

常用的错误属性如下:

属性
timestamp 异常发生的时间
status 状态吗
error 错误原因
exception 异常名
message 异常信息
trace 异常栈信息
path 异常路径

spring-boot-autoconfigure

spring-boot-autoconfigure模块也添加了一些异常处理相关的类
*

ErrorViewResolver

ErrorViewResolver接口,只有一个方法用于解析指定详细信息的错误视图。

@FunctionalInterface
public interface ErrorViewResolver {
   
     
    ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model);
}

DefaultErrorViewResolver

DefaultErrorViewResolver是ErrorViewResolver 的默认实现类。可以在异常时,搜索异常页面。

如果404异常时,将会在下面这里路径下搜索异常页面并展示:

/templates/error/404.x
/static/error/404.html
/templates/error/4xx.x
/static/error/4xx.html

AbstractErrorController

AbstractErrorController是ErrorController的抽象子类,添加了一些使用ErrorViewResolvers解析的重要方法。

BasicErrorController

BasicErrorController是spring boot 提供的一个控制器方法,是ErrorController的实现类,映射/error路径。

访问路径:

@RequestMapping("${server.error.path:${error.path:/error}}")

成员属性,

	private final ErrorProperties errorProperties;

提供了两个重要的控制器方法,当/error请求的MediaType为text/html,也就是页面访问时,会进入到errorHtml,返回ModelAndView 。

当其他方式访问,直接返回ResponseEntity,也就是Json信息。

	@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());
		ModelAndView modelAndView = resolveErrorView(request, response, status, model);
		return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
	}

	@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);
	}

ErrorMvcAutoConfiguration

ErrorMvcAutoConfiguration的作用,主要是:

  • 注入BasicErrorController错误控制器
  • 注入DefaultErrorViewResolver
  • 默认错误页面视图内部类StaticView

异常解析器加载流程

1. 进入DispatcherServlet初始化组件

在DispatcherServlet类中,有个成员属性维护了一个HandlerExceptionResolver集合,用来存放所有的异常解析器。

    @Nullable
    private List<HandlerExceptionResolver> handlerExceptionResolvers;

之前在DispatcherServlet初始化源码分析中,有分析到DispatcherServlet最后会调用initStrategies方法进行组件的初始化,而处理器异常解析器也是在这里初始化加载的。
*

2.初始化解析器

initHandlerExceptionResolvers 会初始化DispatcherServlet使用的HandlerExceptionResolver。

	private void initHandlerExceptionResolvers(ApplicationContext context) {
   
     
		this.handlerExceptionResolvers = null;
		// 1. 是否检测所有 HandlerExceptionResolver,默认为true
		if (this.detectAllHandlerExceptionResolvers) {
   
     
			// 1.1 在ApplicationContext中查找所有HandlerExceptionResolver,包括parent上下文。
			Map<String, HandlerExceptionResolver> matchingBeans = BeanFactoryUtils
					.beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false);
			if (!matchingBeans.isEmpty()) {
   
     
				// 1.2. 将查询到的容器添加到handlerExceptionResolvers集合中并排序
				this.handlerExceptionResolvers = new ArrayList<>(matchingBeans.values());
				// We keep HandlerExceptionResolvers in sorted order.
				AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers);
			}
		}
		else {
   
     
			try {
   
     
				// 2. HandlerExceptionResolver为false
				// 2.1 查询名称为handlerExceptionResolver的Bean,并赋值给handlerExceptionResolvers
				HandlerExceptionResolver her =
						context.getBean(HANDLER_EXCEPTION_RESOLVER_BEAN_NAME, HandlerExceptionResolver.class);
				this.handlerExceptionResolvers = Collections.singletonList(her);
			}
			catch (NoSuchBeanDefinitionException ex) {
   
     
				// Ignore, no HandlerExceptionResolver is fine too.
			}
		}
		// 3. handlerExceptionResolvers为null时,则为默认HandlerExceptionResolver。
		if (this.handlerExceptionResolvers == null) {
   
     
			this.handlerExceptionResolvers = getDefaultStrategies(context, HandlerExceptionResolver.class);
			if (logger.isTraceEnabled()) {
   
     
				logger.trace("No HandlerExceptionResolvers declared in servlet '" + getServletName() +
						"': using default strategies from DispatcherServlet.properties");
			}
		}
	}

可以看到最后添加了两个异常解析器,DefaultErrorAttributes、HandlerExceptionResolverComposite(组合解析器)。
*

异常解析器工作流程

1. DispatchServlet结果处理

可以在DispatchServlet 的doDispatch方法中看到,在执行了控制器方法时,都进行了catch。

异常是在processDispatchResult方法进行处理的,可以看到它的入参传入了异常对象。
*

processDispatchResult负责处理调用的结果,解析我们返回的结果或者处理异常为ModelAndView。

    private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception {
   
     
        boolean errorView = false;
        // 1. 如果异常对象不为空
        if (exception != null) {
   
     
        	// 1.1 如果是ModelAndViewDefiningException,解析将异常解析为ModelAndView
            if (exception instanceof ModelAndViewDefiningException) {
   
     
                this.logger.debug("ModelAndViewDefiningException encountered", exception);
                mv = ((ModelAndViewDefiningException)exception).getModelAndView();
            } else {
   
     
            	// 2. 如果不是ModelAndViewDefiningException异常,进入异常处理方法,返回ModelAndView。
                Object handler = mappedHandler != null ? mappedHandler.getHandler() : null;
                mv = this.processHandlerException(request, response, handler, exception);		// 2.1 将错误页面标记为存在
                errorView = mv != null;
            }
        }
		// 3. 如果 视图存在,处理视图,返回页面
        if (mv != null && !mv.wasCleared()) {
   
     
            this.render(mv, request, response);
            if (errorView) {
   
     
                WebUtils.clearErrorRequestAttributes(request);
            }
        } 
        // others
    }

2. DispatchServlet 异常处理

接着进入DispatchServlet 的processHandlerException方法开始对异常进行处理。通过加载的HandlerExceptionResolver结合确定异常视图模型。

processHandlerException只要解析到一个错误视图模型对象,就会跳出循环。

    protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) throws Exception {
   
     
        request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
        ModelAndView exMv = null;
        if (this.handlerExceptionResolvers != null) {
   
     
        	// 1. 循环DispatchServlet 中的异常解析器
            Iterator var6 = this.handlerExceptionResolvers.iterator();
			
            while(var6.hasNext()) {
   
     
                HandlerExceptionResolver resolver = (HandlerExceptionResolver)var6.next();
                // 2. 解析异常,返回ModelAndView 
                exMv = resolver.resolveException(request, response, handler, ex);
                if (exMv != null) {
   
     
                    break;
                }
            }
        }
        // others...
    }

3. 循环异常解析器

processHandlerException方法会循环DispatchServlet 中的异常解析器,第一个是DefaultErrorAttributes,它的主要作用是将异常设置到request对象的属性中。
*
接着进入到组合解析器中HandlerExceptionResolverComposite。这个解析器的resolveException方法,会循环其成员属性中的所有解析器。
*
首先进入ExceptionHandlerExceptionResolver解析器的resolveException方法。

    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
   
     
    	// 1. 解析器不支持handler类型,直接返回null
        if (!this.shouldApplyTo(request, handler)) {
   
     
            return null;
        } else {
   
     
        	// 2. 如果配置response缓存,则缓存处理,默认false.
            this.prepareResponse(ex, response);
            //  3. 解析异常
            ModelAndView result = this.doResolveException(request, response, handler, ex);
         // others....
    }

接着进入到doResolveHandlerMethodException方法,正式解析异常,因为我们这里没有配置@ExceptionHandler注解,ExceptionHandlerExceptionResolver解析器返回的结果也是null。

接着进入到组合解析器的第二个解析器ResponseStatusExceptionResolver,对使用了@ResponseStatus响应状态码注解进行解析。

    @Nullable
    protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
   
     
        try {
   
     
        	// 1. 如果是ResponseStatusException,设置响应消息头,并response.sendError。
            if (ex instanceof ResponseStatusException) {
   
     
                return this.resolveResponseStatusException((ResponseStatusException)ex, request, response, handler);
            }
			// 2. 根据异常,查询状态码。这里ArithmeticException,未找到状态码,返回null
            ResponseStatus status = (ResponseStatus)AnnotatedElementUtils.findMergedAnnotation(ex.getClass(), ResponseStatus.class);
            // 3. 获取到了ResponseStatus,则设置响应消息头,并response.sendError。
            if (status != null) {
   
     
                return this.resolveResponseStatus(status, request, response, handler, ex);
            }
		// others
    }

最后进入到DefaultHandlerExceptionResolver解析器,会判断是不是spring MVC中的异常,如果是,直接设置对应状态码,并调用response.sendError(404);,返回一个空的ModelAndView对象。

    protected ModelAndView handleNoHandlerFoundException(NoHandlerFoundException ex, HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException {
   
     
        pageNotFoundLogger.warn(ex.getMessage());
        response.sendError(404);
        return new ModelAndView();
    }

4. 回到processHandlerException

经过上述解析器后,最终我们的ArithmeticException异常,返回的ModelAndView为null。
*
processHandlerException继续往下走,这里的处理逻辑为:

  • 如果解析器ModelAndView为null,直接throw 异常,在控制台打印。
  • ModelAndView为空,则会在request中设置异常属性
  • ModelAndView不为空,存在View,查看是否有默认异常视图,有则设置View为默认视图。
    *
    processDispatchResult执行完毕,最终没有一个解析器处理了我们的异常。

返回null,表明异常仍未解决,如果异常最后还在,则允许冒泡到Servlet容器,交给Tomcat处理。

5. 异常进入StandardHostValve(Tomacat)

在之前DispathServlet已经执行完毕了,但是解析器也没有对异常进行解决,最终由Tomacat来处理。

StandardHostValve是的Tomacat的核心类,其中invoke方法根据Request匹配Context,并将Request传递给匹配到的Context,spring MVC没有解决的异常,最终由它处理。

StandardHostValve会去获取response的status如果response是错误转状态则继续去获取错误码对应的ErrorPage,如果没有定义则获取默认的ErrorPage(/error),并重定向/error请求。
*
所以/error重定向请求会再次进入到DispathServlet中的=》doService=》doDispatch等方法,直接去访问/error路径。
*

6. /error访问

/error访问路径会进入到BasicErrorController中。

    @RequestMapping(
        produces = {
   
     "text/html"}
    )
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
   
     
    	// 1. 获取HttpStatus 500 SERVER_ERROR
        HttpStatus status = this.getStatus(request);
        // 2. 获取错误信息
        Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
        // 3. 设置响应状态码
        response.setStatus(status.value());
        // 4. 调用DefaultErrorViewResolver
        ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
        // 5. 返回视图
        return modelAndView != null ? modelAndView : new ModelAndView("error", model);
    }

调用DefaultErrorViewResolver解析器,会查到相关视图模板,这里没有配置,所以返回了null。所以这里/error访问的控制器,最后返回了 new ModelAndView(“error”, model);。

    private ModelAndView resolve(String viewName, Map<String, Object> model) {
   
     
    	// 视图名  error/5xx
        String errorViewName = "error/" + viewName;
        // 查询可用的模板
        TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext);
        return provider != null ? new ModelAndView(errorViewName, model) : this.resolveResource(errorViewName, model);
    }

7. 进入processDispatchResult

接着进入到processDispatchResult方法,因为这里返回的ModelAndView为/error,参数exception为null,所以会进入render方法。
*

render会调用视图解析器,最终调用StaticView视图的render方法。
*
直接拼接错误信息,然后response直接把信息页面返回个浏览器了。
*

8. PostMan访问

当使用PostMan时,这里/error 会直接访问到BasicErrorController的error控制器。

    @RequestMapping
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
   
     
        HttpStatus status = this.getStatus(request);
        if (status == HttpStatus.NO_CONTENT) {
   
     
            return new ResponseEntity(status);
        } else {
   
     
            Map<String, Object> body = this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.ALL));
            return new ResponseEntity(body, status);
        }
    }

直接返回了Json信息。
*

版权声明:本文不是「本站」原创文章,版权归原作者所有 | 原文地址: