DispatcherServlet的九大组件——处理器异常解析器组件——源码和实现——万字长文

1:为什么要学习DispatcherServlet的九大组件

在如下的这篇文章中
从一个请求入口来带你探究DispatcherServlet的奥秘——SpringMVC的核心组件——万字长文
已经探讨过了,为什么一个请求最终会到DispatcherServlet中的doDispatch方法中实现和该方法中的执行可以分为11个逻辑代码块。那为什么今天要聊这个话题呢?

这些组件对我们今后的开发是否有帮助呢?以我之愚见,我觉得是有的,举一个例子:九大组件之中的处理器异常解析器组件,如果我们去探究HandlerExceptionResolver 的源码,在以后的开发中我们就可以自定义实现 HandlerExceptionResolver 处理异常(可以作为默认的全局异常处理规则)等等。

那这个九大组件和doDispatch方法又有什么关系呢?在上面的文章中的末尾我们已经提到
doDispatch方法中出现了很多组件,这些组件都是Spring MVC整个处理过程中不可获缺的部分。正是通过这些组件之间的搭配组合,才令整个Spring MVC框架完整地运行起来,使用框架进行开发时,各种便捷功能都是通过这些组件辅助完成的。这就像盖房子一样,通过一个个小组件,最终搭建起一个完整的艺术品。

2:处理器异常解析器介绍

在请求处理的过程中,如果发生任何异常,在最终执行处理结果方法procesHandlerException 时,会尝试把该异常解析为异常的ModelAndView结果,后续使用相同逻辑处理此ModelAndView。
从代码层面看上面的话,如下图
在这里插入图片描述

处理器异常解析器(HandlerExceptionResolver )只有一个方法,即

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

共有传入请求、响应、查找到的处理器、发生的异常4个参数,返回对该请求解析后的ModelAndView异常视图。

默认情况下,包括两个异常解析器: DefaultErrorAttributes 与HandlerExceptionResolver-Composite。第一个仅用于保存异常相关信息到请求属性中,并没有其他实际作用。第二个则组合了3个异常解析器,在该异常解析器的解析方法中,遍历内部的3个异常解析器,返回第一个非空的异常ModelAndView结果。

那在代码调试中怎么找到这两个异常解析器呢?我们在这先给一个调试截图,关于代码调试会在后面一一讲述
在这里插入图片描述

如上图可知3个异常解析器按顺序分别为ExceptionHandlerExceptionResolver、ResponseStatusExceptionResolver、DefaultHandlerExceptionResolver。

这3个类型都继承了AbstractHandlerExceptionResolver抽象类。在抽象类中,添加Ordered 接口用于支持指定异常解析器的顺序。同时其实现了HanderExceptionResolver 接口的resolveException方法,封装些异常解析时的统一操作,代码如下:

AbstractHandlerExceptionResolver类下的resolveException代码逻辑

@Override
	@Nullable
	public ModelAndView resolveException(
			HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
//调用本类的shouldApplyTo方法,并判断此异常解析器是否对该请求和处理器提供支持
		if (shouldApplyTo(request, handler)) {
//预处理响应,如在某些情况下对于错误结果不需要缓存,就在该方法中进行处理		
			prepareResponse(ex, response);
//执行真正的解析异常方法,由子类实现			
			ModelAndView result = doResolveException(request, response, handler, ex);
//结果不为空,返回结果			
			if (result != null) {
//..
				logException(ex, request);
			}
			return result;
		}
		else {
			return null;
		}
	}

下面的3,4,5章就为大家分别介绍ExceptionHandlerExceptionResolver、ResponseStatusExceptionResolver、DefaultHandlerExceptionResolver的实现和源码调试,第6章就是利用前面学习到的知识,自己定义一个HandlerExceptionResolver 来处理异常

3:ExceptionHandlerExceptionResolver

ExceptionHandler异常解析器用于支持通过注解@ExcptionHandler配置的异常处理方法。同时该类型通过重写shouldApplyTo,对处理器类型进行判断,只有类型为HandlerMethod的处理器产生的异常才视为支持,即只解析@RequestMapping注解相关逻辑中产生的异常。

千万不要小看这一段话,这一段话包含很多重要知识点,接下来为大家一一道来。

对于一个应用来说,正常情况还会发生各种意外情况。比如处理过程中,框架抛出各种异常,在处理器方法执行过程中抛出异常,发生异常情况时该如何处理呢?Spring MVC提供了处理器异常解析器用于处理执行过程中发生的各种异常。

在提供了整体的基于注解的开发方式后,异常处理器的逻辑也相应的提供了基于注解的方式进行声明。注解@ExceptionHandler就是用于声明异常处理方法,而ExceptionHandlerExceptionResolver就是对应于注解的异常解析器。同时这种解析器,仅针对HandlerMethod类型的处理器才适用,即这个处理器异常解析器组件整体与RequstMappingHandlerMapping和RequestMappingHandlerAdapter进行成套服务。

对于初学者来说,这两段话会听起来有点懵,不过在接下来的过程中,作者会尽量的使用源码与知识点结合的方式带大家搞明白

下面就来分析该异常解析器的详细逻辑,学会如何实现使用简单的注解声明就能达到的异常处理机制。

3.1 @ExceptionHandler 解析器概述

与@RequestMapping注解标记请求处理器方法类似,@ExceptionHandler 注解用于标记异常处理器方法。其整体特性与@RequestMapping相关特性基本相同。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionHandler {
//注解中提供属性value,可以配置Class数组,指定发生哪些异常时使用该异常处理器方法执行
	Class<? extends Throwable>[] value() default {};
}

注解中提供属性value,可以配置Class数组,指定发生哪些异常时使用该异常处理器方法执行,这与@RequestMapping的查找请求对应的处理器方法类似。

异常处理器方法享有与请求处理器方法相同的参数解析与返回值处理功能,这些功能的实现都是依赖于@ExceptionHandler对应的异常解析器ExceptionHandlerExceptionResolver。这个解析器在功能上与RequestMappingHandlerAdapter 的功能类似,最终返回的结果同样是ModelAndView类型,用于提供发生异常时需要返回的ModelAndView结果。

所以可以把@ExceptionHandler标记的方法理解为@RequestMapping相关逻辑执行过程中发生异常时的替代方法。接下来就是要说和这个注解相关的逻辑在ExceptionHandlerExceptionResolver中是如何实现的。

在第2章中已经了解异常解析器的相关知识,在执行异常解析器的解析异常方法resolveException时,在所有异常解析器的同一个抽 象类AbstractHandlerExceptionResolver中,提供了shouldApplyTo方法,只有这个返回为true 时,才执行真正的解析异常方法doResolveException。在ExceptionHandlerExceptionResolver中,shouldApplyTo方法由其父类AbstractHandlerMethodExceptionResolver实现,

上面的一段话出现了很多类和方法,可能我们已经眼花缭乱,这就是源码的精髓所在,在后面我们来实现全局异常的示例会带大家调试一遍源码,可能那样就会好多了吧。
AbstractHandlerMethodExceptionResolver类下的shouldApplyTo方法逻辑实现如下

/**是否可以被该异常解析器处理
@param request 原始的请求
@param handler 处理器查逻辑中找到的处理器,可以为空,如果在执行查找前发生异常就一定为空
*/
@Override
	protected boolean shouldApplyTo(HttpServletRequest request, @Nullable Object handler) {
//处理器如果为空,则交给父类判断	
		if (handler == null) {
//默认该逻辑返回true		
			return super.shouldApplyTo(request, null);
		}
		else if (handler instanceof HandlerMethod) {
//如果处理器类型是HandlerMethod. 将请求与处理器所在Bean交给父类判断		
			HandlerMethod handlerMethod = (HandlerMethod) handler;
			handler = handlerMethod.getBean();
			return super.shouldApplyTo(request, handler);
		}
		else if (hasGlobalExceptionHandlers() && hasHandlerMappings()) {
			return super.shouldApplyTo(request, handler);
		}
		else {
//否则不支持		
			return false;
		}
	}

从这里可以看出,此异常解析器就是针对处理器方法类型的处理器使用的。 在抽象类AbstractHandlerMethodExceptionResolver中封装了与HandlerMethod处理器相关的统一逻辑,除了shouldApplyTo 方法外,该类也实现了doResolveException 方法,并把处理器转换为HandlerMethod类型,交给doResolveHandlerMethodException方法处理。

AbstractHandlerMethodExceptionResolver类下的doResolveException 方法逻辑实现如下

	@Override
	@Nullable
	protected final ModelAndView doResolveException(
			HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
// 对handler进行类型转换,以供doResolveHandlerMethodException方法使用,返回ModelAndView结果
		HandlerMethod handlerMethod = (handler instanceof HandlerMethod ? (HandlerMethod) handler : null);
		return doResolveHandlerMethodException(request, response, handlerMethod, ex);
	}

所以真实地实现解析错误的逻辑在ExeptionHandlerExceptionResolver 的doResolveHandlerMethodException方法中(因为AbstractHandlerMethodExceptionResolver类下的doResolveHandlerMethodException方法是抽象方法,必须交给子类来实现),ExeptionHandlerExceptionResolver 类下的doResolveHandlerMethodException方法,由于篇幅有限,这里不在列出源码,只讲述一下这个方法的整体处理过程。两个要点:异常处理器方法的查找(getExceptionHandlerMethod)与异常处理器方法的执行

3.2 @ControllerAdvice+@ExceptionHandler处理全局异常——底层ExceptionHandlerExceptionResolver

3.1节主要是讲了,通过ExceptionHandlerExceptionResolver处理异常解析器解析异常的流程,那么我们在开发中什么时候会用到ExceptionHandlerExceptionResolver来解析异常呢?这里解析异常可以看做是HandlerExceptionResolver类中执行resolveException方法
这就是我们这节要讲的内容,使用@ControllerAdvice+@ExceptionHandler处理全局异常,它的底层就是使用ExceptionHandlerExceptionResolver。在这一节,我们先把主要代码部分展现出来,3.2节我们进行源码调试
在这里插入图片描述

实现流程:假设我们在登录页面登录成功(提交@PostMapping ("/loginadmin")请求),在后端的控制器在登录成功的分支下设置一个 int i=10/0;异常,并写一个全局异常类来捕获异常。
项目结构图
在这里插入图片描述

主要代码部分
UserController

@PostMapping ("/loginadmin")
    public String login(@RequestParam("username") String username,@RequestParam("password")  String password, Map<String,Object> map, HttpSession session){
       //...
        if (login.getUsername().equals(username) && login.getPassword().equals(password)){
          //..
//            ExceptionHandlerExceptionResolver
            int i=10/0;
            return "home";
        }else {
            map.put("msg","用户名或密码错误");
            return "login";
        }
    }

GlobalExceptionHandle

@Slf4j
@ControllerAdvice
public class GlobalExceptionHandle {
    @ExceptionHandler(ArithmeticException.class)//处理异常
    public String handlerException(ArithmeticException e){
        log.error("异常是{}",e);
        return "login";
    }
}

接下来的3.3节,将为大家进行源码调试,目的验证底层是ExceptionHandlerExceptionResolver类

3.3 全局异常的源码调试

根据第2章开头部分可知,在请求处理的过程中,如果发生任何异常,在最终执行处理结果方法procesHandlerException 时,会尝试把该异常解析为异常的ModelAndView结果。我们就可以在DispatcherServlet类中找到该方法并打上断点,然后再我们自定义的GlobalExceptionHandle 中的输出日志出也打上一个断点。如下图所示
在这里插入图片描述
步骤1:在登录界面输入正确的登录名和密码,点击登录,断点就会自动跳到如上的DispatcherServlet.java:1131处,并且页面会有一个一直等待的提示
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
步骤2:跳到processHandlerException方法里面,一步步调试(单步调试)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

步骤3:跳到这个AbstractHandlerExceptionResolver抽象类中。在AbstractHandlerExceptionResolver抽象类中,添加Ordered 接口用于支持指定异常解析器的顺序。同时该类实现了HanderExceptionResolver 接口的resolveException方法,封装些异常解析时的统一操作(shouldApplyTo)
在这里插入图片描述
步骤4:在抽象类AbstractHandlerMethodExceptionResolver中实现了doResolveException 方法,并把处理器转换为HandlerMethod类型,交给doResolveHandlerMethodException方法处理。
在这里插入图片描述
步骤5:所以真实地实现解析错误的逻辑在ExeptionHandlerExceptionResolver 的doResolveHandlerMethodException方法中,由此可以验证全局异常的底层是ExeptionHandlerExceptionResolver 类。
在这里插入图片描述

如果不点击上面的恢复执行,我们就一步步的实现单步调试,最后也会跳转到全局异常类中,如下图

在这里插入图片描述
在这里插入图片描述
如上就是ExceptionHandlerExceptionResolver类调试的全过程,调试过程中,还带大家复习了3.1节里面的知识,实践出真知

在这里插入图片描述
第三章的全局异常处理,在以后开发中会很常见,所以多说了几句,接下来的几个异常解析器,我们简单概括。

4:ResponseStatusExceptionResolver

ResponseStatusExceptionResolver异常解析器用于自动处理ResponseStatusException类型的异常或异常类上包含@ResponseStatus注解的异常信息。其处理逻辑代码如下:
ResponseStatusExceptionResolver类下的doResolveException方法

@Override
	@Nullable
	protected ModelAndView doResolveException(
			HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
		try {
// 如果是ResponseStatusException类型异常		
			if (ex instanceof ResponseStatusException) {
//则通过该异常执行解析响应状态异常逻辑			
				return resolveResponseStatusException((ResponseStatusException) ex, request, response, handler);
			}
//如果不是ResponseStatueException类型的异常,则尝试查找异常类型上的@ResponseStatus注解信息,包括父类中的注解
			ResponseStatus status = AnnotatedElementUtils.findMergedAnnotation(ex.getClass(), ResponseStatus.class);
//如果结果不为空			
			if (status != null) {
// 则返回解析响应状态后的结果		
				return resolveResponseStatus(status, request, response, handler, ex);
			}
//如果结果为空,则尝试获取该异常包装的异常,并递归调用本方法
			if (ex.getCause() instanceof Exception) {
				return doResolveException(request, response, handler, (Exception) ex.getCause());
			}
		}
		catch (Exception resolveEx) {
			if (logger.isWarnEnabled()) {
				logger.warn("Failure while trying to resolve exception [" + ex.getClass().getName() + "]", resolveEx);
			}
		}
		return null;
	}

ResponseStatusExceptionResolver类下的resolveResponseStatus方法

//解析响应状态异常,还有个重载方法,第一个参数为@ResponseStatus注解
	protected ModelAndView resolveResponseStatus(ResponseStatus responseStatus, HttpServletRequest request,
			HttpServletResponse response, @Nullable Object handler, Exception ex) throws Exception {
//通过异常信息获取状态码,对于参数是@ResponseStatus,则获取注解中的该信息
		int statusCode = responseStatus.code().value();
//获取异常原因,参数为注解时逻辑则从注解中获取该信息		
		String reason = responseStatus.reason();
//应用状态码和原因到响应中		
		return applyStatusAndReason(statusCode, reason, response);
	}

ResponseStatusExceptionResolver类下的applyStatusAndReason方法

//应用异常结果,返回模型与视图
protected ModelAndView applyStatusAndReason(int statusCode, @Nullable String reason, HttpServletResponse response)
			throws IOException {
//如果异常原因为空
		if (!StringUtils.hasLength(reason)) {
//通过响应的sendError方法发送对应状态的响应
//此时会进入Servlet原生的错误处理逻辑,查找状态码对应的异常处理Servlet
//在Spring Boot 中,最终会把该异常响应转发到/error路径对应的处理器进行处理		
			response.sendError(statusCode);
		}
		else {
//如果异常原因不为空,且信息源不为空,则尝试通过信息源解析此原因		
			String resolvedReason = (this.messageSource != null ?
					this.messageSource.getMessage(reason, null, reason, LocaleContextHolder.getLocale()) :
					reason);
//解析后同样通过原始sendError方法处理结果					
			response.sendError(statusCode, resolvedReason);
		}
//返回一个空的ModelAndView
		return new ModelAndView();
	}

基于该功能,可以编写一些自定义的异常类,并通过异常类上的@ResponseStatus 注解来定制该异常的响应状态码。说干就干,下面一小节,我们就来编写一个自定义异常类,并标记上@ResponseStatus ,然后再探究该过程异常解析的源码

4.2 @ResponseStatus+自定义异常——底层ResponseStatusExceptionResolver

我们只要把3.2节的示例做一个微小的改变即可。
实现流程:假设我们在登录页面登录成功(提交@PostMapping ("/loginadmin")请求),在后端的控制器在登录成功的分支下设置一个 throw new ForbiddenLoginException();异常,并写一个ForbiddenLoginException异常类来捕获异常。
项目结构图
在这里插入图片描述

主要代码部分
UserController

@PostMapping ("/loginadmin")
    public String login(@RequestParam("username") String username,@RequestParam("password")  String password, Map<String,Object> map, HttpSession session){
       //...
        if (login.getUsername().equals(username) && login.getPassword().equals(password)){
          //..
//            ExceptionHandlerExceptionResolver
            throw new ForbiddenLoginException();
           // return "home";
        }else {
            map.put("msg","用户名或密码错误");
            return "login";
        }
    }

ForbiddenLoginException

@ResponseStatus(value =  HttpStatus.FORBIDDEN,reason = "禁止登陆")
public class ForbiddenLoginException extends RuntimeException {
    public ForbiddenLoginException(){
    }
    public ForbiddenLoginException(String message){
        super(message);
    }
}

4.3 @ResponseStatus+自定义异常的源码调试

由3.3节的全局异常的源码调试过程可以知道,我们可以在AbstractHandlerExceptionResolver.java:141行打上断点(ResponseStatusExceptionResolver类继承了AbstractHandlerExceptionResolver抽象类),然后再ForbiddenLoginException.java:15行打上断点。如下图
在这里插入图片描述
步骤1:点击登录按钮,跳到指定方法位置
在这里插入图片描述

在这里插入图片描述
步骤2:跳到ResponseStatusExceptionResolver类下的resolveResponseStatus方法,在进行一步步调试(单步调试)
在这里插入图片描述
步骤3:跳到ResponseStatusExceptionResolver类下的resolveResponseStatus方法,进行单步调试
在这里插入图片描述

步骤4:跳到ResponseStatusExceptionResolver类下的applyStatusAndReason方法,进行单步调试
在这里插入图片描述
在这里插入图片描述

5:DefaultHandlerExceptionResolver

Spring MVC对于内置的一些异常会返回不同的状态码。如对于参数解析异常,会返回400(Bad Request错误请求);对于消息数据转换异常,则会返回500(Internal Server Error 服务器内部异常),这些策略都是通过默认异常解析器提供的,所以可以说DefaultHandlerExceptionResolver 是处理框架底层的异常。

DefaultHandlerExceptionResolver类下的doResolveException方法逻辑

@Override
	@Nullable
	protected ModelAndView doResolveException(
			HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
		try {
//异常类型为请求方法不被支持时,返回406		
			if (ex instanceof HttpRequestMethodNotSupportedException) {
				return handleHttpRequestMethodNotSupported(
						(HttpRequestMethodNotSupportedException) ex, request, response, handler);
			}
//省略一些异常。。。。。
//异常为缺少请求参数时,返回400			
			else if (ex instanceof MissingServletRequestParameterException) {
				return handleMissingServletRequestParameter(
						(MissingServletRequestParameterException) ex, request, response, handler);
			}
//省略一些异常。。。。。
//异常为没有处理器被找到
			else if (ex instanceof NoHandlerFoundException) {
				return handleNoHandlerFoundException(
						(NoHandlerFoundException) ex, request, response, handler);
			}
//省略一些异常。。。。。
		catch (Exception handlerEx) {
//..
		}
//	否则返回null,可以通过下一个视图解析进行解析	
		return null;
	}

我们接下来举一个例子进行简单代码调试
进入到登录界面,点击登录会提交loginadmin请求,登录成功后会跳转到主界面。我们在这个代码中做了一个手脚:第2行@RequestParam(“usernam”)中的usernam是错的,正确的是username

@PostMapping ("/loginadmin")
2    public String login(@RequestParam("usernam") String username,@RequestParam("password")  String password, Map<String,Object> map, HttpSession session){
   //.. 
        return "home";
        }else {
            map.put("msg","用户名或密码错误");
            return "login";
        }
        }

如下列出简单的调试过程
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

6:自定义HandlerExceptionResolver

上面的五个章节已经详细的介绍了HandlerExceptionResolver的三个实现类源码调试和实现,接下来我们就可以自定义异常类通过实现HandlerExceptionResolver接口,来捕获异常,如下是代码示例

@Component
2//@Order(value = Ordered.HIGHEST_PRECEDENCE)
public class CustomerHandlerException implements HandlerExceptionResolver{
    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        try {
            response.sendError(511,"我喜欢的错误");
        } catch (IOException e) {
            e.printStackTrace();
        }
        return new ModelAndView();
    }
}

第2行,如果添加@Order接口就可以用于支持指定异常解析器的顺序。value = Ordered.HIGHEST_PRECEDENCE是最高级别(可以作为默认的全局异常处理规则)
去掉第2行的注释,然后进源码调试,
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

加上第2行的注释,然后进源码调试,
在这里插入图片描述
在这里插入图片描述
上面的两个例子返回了不同的错误页面,加上@Order(value = Ordered.HIGHEST_PRECEDENCE)返回的是,我们CustomerHandlerException类去解析异常,不加@Order(value = Ordered.HIGHEST_PRECEDENCE)返回的是,我们ForbiddenLoginException类去解析异常。再次说明@Order接口就可以用于支持指定异常解析器的顺序。


版权声明:本文为qq_44891295原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。