Java自学者论坛

 找回密码
 立即注册

手机号码,快捷登录

恭喜Java自学者论坛(https://www.javazxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,会员资料板块,购买链接:点击进入购买VIP会员

JAVA高级面试进阶训练营视频教程

Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程Go语言视频零基础入门到精通Java架构师3期(课件+源码)
Java开发全终端实战租房项目视频教程SpringBoot2.X入门到高级使用教程大数据培训第六期全套视频教程深度学习(CNN RNN GAN)算法原理Java亿级流量电商系统视频教程
互联网架构师视频教程年薪50万Spark2.0从入门到精通年薪50万!人工智能学习路线教程年薪50万大数据入门到精通学习路线年薪50万机器学习入门到精通教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程MySQL入门到精通教程
查看: 445|回复: 0

Spring系列(七) Spring MVC 异常处理

[复制链接]
  • TA的每日心情
    奋斗
    2024-4-6 11:05
  • 签到天数: 748 天

    [LV.9]以坛为家II

    2034

    主题

    2092

    帖子

    70万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    705612
    发表于 2021-9-2 15:56:25 | 显示全部楼层 |阅读模式

    Servlet传统异常处理

    Servlet规范规定了当web应用发生异常时必须能够指明, 并确定了该如何处理, 规定了错误信息应该包含的内容和展示页面的方式.(详细可以参考servlet规范文档)

    处理方式

    • 处理状态码<error-code>
    • 处理异常信息<exception-type>
    • 处理服务地址<location>

    Spring MVC 处理方式

    所有的请求必然以某种方式转化为响应.

    • Spring中特定的异常将自动映射为特定的HTTP状态码
    • 使用@ResponseStatus注解可以映射某一异常到特定的HTTP状态码
    • Controller方法上可以使用@ExceptionHandler注解使其用来处理异常
    • 使用@ControllerAdvice 方式可以统一的方式处理全局异常

    Spring boot 方式

    • 实现ErrorPageRegistrar: 确定是页面处理的路径必须固定,优点是比较通用
    • 注册ErrorPage
    • 实现ErrorPage对应的服务

    源码分析

    一.接口HandlerExceptionResolver

    该接口定义了Spring中该如何处理异常. 它只有一个方法resolveException(), 接口源码如下:

     // 由对象实现的接口,这些对象可以解决在处理程序映射或执行期间引发的异常,在典型的情况下是错误视图。在应用程序上下文中,实现器通常被注册为bean。
     // 错误视图类似于JSP错误页面,但是可以与任何类型的异常一起使用,包括任何已检查的异常,以及针对特定处理程序的潜在细粒度映射。
    public interface HandlerExceptionResolver {
    	@Nullable
    	ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);
    }
    

    Spring 为该接口提供了若干实现类如下:

    HandlerExceptionResolverComposite  				委托给其他HandlerExceptionResolver的实例列表
    AbstractHandlerExceptionResolver				抽象基类
    	AbstractHandlerMethodExceptionResolver		支持HandlerMethod处理器的抽象基类
    		ExceptionHandlerExceptionResolver		通过 @ExceptionHandler 注解的方式实现的异常处理
    	DefaultHandlerExceptionResolver				默认实现, 处理spring预定义的异常并将其对应到错误码
    	ResponseStatusExceptionResolver				通过 @ResponseStatus 注解映射到错误码的异常
    	SimpleMappingExceptionResolver				允许将异常类映射到视图名
    

    二. DefaultHandlerExceptionResolver

    这个类是Spring提供的默认实现, 用于将一些常见异常映射到特定的状态码. 这些状态码定义在接口HttpServletResponse中, 下面是几个状态码的代码片段

    public interface HttpServletResponse extends ServletResponse {
    	...
        public static final int SC_OK = 200;
        public static final int SC_MOVED_PERMANENTLY = 301;
        public static final int SC_MOVED_TEMPORARILY = 302;
        public static final int SC_FOUND = 302;
        public static final int SC_UNAUTHORIZED = 401;
        public static final int SC_INTERNAL_SERVER_ERROR = 500;
    	...
    }
    

    实际上, DefaultHandlerExceptionResolver中并没有直接实现接口的resolveException方法, 而是实现了抽象类AbstractHandlerExceptionResolverdoResolveException()方法, 后者则在实现了接口的方法中委托给抽象方法doResolveException, 这个方法由子类去实现.

    AbstractHandlerExceptionResolverresolveException方法代码如下:

    @Override
    @Nullable
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
    
    	// 判断是否当前解析器可用于handler
    	if (shouldApplyTo(request, handler)) { 
    		prepareResponse(ex, response);
    		ModelAndView result = doResolveException(request, response, handler, ex);
    		if (result != null) {
    			// Print warn message when warn logger is not enabled...
    			if (logger.isWarnEnabled() && (this.warnLogger == null || !this.warnLogger.isWarnEnabled())) {
    				logger.warn("Resolved [" + ex + "]" + (result.isEmpty() ? "" : " to " + result));
    			}
    			// warnLogger with full stack trace (requires explicit config)
    			logException(ex, request);
    		}
    		return result;
    	}
    	else {
    		return null;
    	}
    }
    

    接下来我们看DefaultHandlerExceptionResolver实现的doResolveException方法. 代码如下;

    @Override
    	@Nullable
    	protected ModelAndView doResolveException(
    			HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
    
    		try {
    			if (ex instanceof HttpRequestMethodNotSupportedException) {
    				return handleHttpRequestMethodNotSupported(
    						(HttpRequestMethodNotSupportedException) ex, request, response, handler);
    			}
    			else if (ex instanceof HttpMediaTypeNotSupportedException) {
    				return handleHttpMediaTypeNotSupported(
    						(HttpMediaTypeNotSupportedException) ex, request, response, handler);
    			}
    			....
    			else if (ex instanceof NoHandlerFoundException) {
    				return handleNoHandlerFoundException(
    						(NoHandlerFoundException) ex, request, response, handler);
    			}
    			.....
    		}
    		catch (Exception handlerEx) {
    			if (logger.isWarnEnabled()) {
    				logger.warn("Failure while trying to resolve exception [" + ex.getClass().getName() + "]", handlerEx);
    			}
    		}
    		return null;
    	}
    

    可以看到代码中使用了大量的分支语句, 实际上是将方法传入的异常类型通过instanceof运算符测试, 通过测试的转化为特定的异常. 并调用处理该异常的特定方法. 我们挑一个比如处理NoHandlerFoundException这个异常类的方法, 这个方法将异常映射为404错误.

    protected ModelAndView handleNoHandlerFoundException(NoHandlerFoundException ex,
    		HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException {
    
    	pageNotFoundLogger.warn(ex.getMessage());
    	response.sendError(HttpServletResponse.SC_NOT_FOUND); //设置为404错误
    	return new ModelAndView(); //返回个空视图
    }
    

    上面分析了Spring默认的异常处理实现类DefaultHandlerExceptionResolver.它处理的异常是Spring预定义的几种常见异常, 它将异常对应到HTTP的状态码. 而对于不属于这些类型的其他异常, 我们可以使用ResponseStatusExceptionResolver来处理, 将其对应到HTTP状态码.

    三. ResponseStatusExceptionResolver

    如何使用?

    @GetMapping("/responseStatus")
    @ResponseBody
    public String responseStatus() throws MyException {
    	throw new MyException();
    }
    
    @ResponseStatus(code = HttpStatus.BAD_GATEWAY)
    public class MyException extends Exception{}
    

    只需要在异常上使用@ResponseStatus注解即可将特定的自定义异常对应到Http的状态码.

    四. ExceptionHandlerExceptionResolver

    使用类似于普通的controller方法, 使用@ExceptionHandler注解的方法将作为处理该注解参数中异常的handler. 比如, 在一个controller中, 我们定义一个处理NPE的异常处理handler方法, 可以用来处理该controller中抛出的NPE. 代码如下:

     @GetMapping("/npe1")
    @ResponseBody
    public String npe1() throws NullPointerException {
    	throw new NullPointerException();
    }
    
    @GetMapping("/npe2")
    @ResponseBody
    public String npe2() throws NullPointerException {
    	throw new NullPointerException();
    }
    
    @ExceptionHandler(value = {NullPointerException.class})
    @ResponseBody
    public String npehandler(){
    	return "test npe handler";
    }
    

    无论是请求/npe1还是请求/npe2, 系统都会抛出异常, 并交给对应的处理程序npehandler去处理. 使用@ExceptionHandler(value = {NullPointerException.class})注解的方法可以处理本controller范围内的所有方法排除的npe异常, 如果要将其作为应用中所有controller的异常处理器, 就要将其定义在@ControllerAdvice注解的类中.

    @ControllerAdvice
    public class ControllerAdvicer {
    
        @ExceptionHandler(value = {NullPointerException.class})
        @ResponseBody
        public String npehandler(){
            return "test npe handler in advice";
        }
    }
    

    要了解其原理, 需要查看ExceptionHandlerExceptionResolver中的方法doResolveHandlerMethodException

    @Override
    @Nullable
    protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request,
    		HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) {
    	
    	// 获取异常对用的处理器, 就是@ExceptionHandler注解的方法包装, 注意参数handlerMethod, 在方法内部, 它将用来获取所在Controller的信息
    	ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
    	if (exceptionHandlerMethod == null) {
    		return null;
    	}
    
    	if (this.argumentResolvers != null) {
    		exceptionHandlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
    	}
    	if (this.returnValueHandlers != null) {
    		exceptionHandlerMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
    	}
    
    	ServletWebRequest webRequest = new ServletWebRequest(request, response);
    	ModelAndViewContainer mavContainer = new ModelAndViewContainer();
    
    	try {
    		if (logger.isDebugEnabled()) {
    			logger.debug("Invoking @ExceptionHandler method: " + exceptionHandlerMethod);
    		}
    		Throwable cause = exception.getCause();
    		// 调用异常处理handler的方法.
    		if (cause != null) {
    			// Expose cause as provided argument as well
    			exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, cause, handlerMethod);
    		}
    		else {
    			// Otherwise, just the given exception as-is
    			exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, handlerMethod);
    		}
    	}
    	catch (Throwable invocationEx) {
    		// Any other than the original exception is unintended here,
    		// probably an accident (e.g. failed assertion or the like).
    		if (invocationEx != exception && logger.isWarnEnabled()) {
    			logger.warn("Failed to invoke @ExceptionHandler method: " + exceptionHandlerMethod, invocationEx);
    		}
    		// Continue with default processing of the original exception...
    		return null;
    	}
    
    	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;
    	}
    }
    

    可以看到在两个中文注释的地方, 其一是方法的开始部分获取到了异常的handler, 其二是调用这个handler的方法. 调用方法应该很好理解, 我们接下来查看方法getExceptionHandlerMethod.

    // 找到给定异常对应的@ExceptionHandler注解方法, 默认先在controller类的继承结构中查找, 否则继续在@ControllerAdvice注解的 bean中查找.
    @Nullable
    protected ServletInvocableHandlerMethod getExceptionHandlerMethod(
    		@Nullable HandlerMethod handlerMethod, Exception exception) {
    
    	Class<?> handlerType = null;
    
    	if (handlerMethod != null) {
    		// Local exception handler methods on the controller class itself.
    		// To be invoked through the proxy, even in case of an interface-based proxy.
    		handlerType = handlerMethod.getBeanType();
    		ExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(handlerType);
    		if (resolver == null) {
    			resolver = new ExceptionHandlerMethodResolver(handlerType);
    			this.exceptionHandlerCache.put(handlerType, resolver);
    		}
    		Method method = resolver.resolveMethod(exception);
    		if (method != null) {
    			return new ServletInvocableHandlerMethod(handlerMethod.getBean(), method);
    		}
    		// For advice applicability check below (involving base packages, assignable types
    		// and annotation presence), use target class instead of interface-based proxy.
    		if (Proxy.isProxyClass(handlerType)) {
    			handlerType = AopUtils.getTargetClass(handlerMethod.getBean());
    		}
    	}
    
    	// 在@ControllerAdvice注解的类中遍历查找
    	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;
    }
    

    我们可以看到,它会首先查找controller中的方法, 如果找不到才去查找@ControllerAdvice注解的bean. 也就是说controller中的handler的优先级要高于advice.

    上面我们了解了几个Exceptionresolver的使用, 并通过源代码简单看了他们各自处理的原理. 但这些Resolver如何加载我们还不知道, 接下来我们重点看下他们是如何加载进去的.

    四. ExceptionResolver的加载

    在本系列的上一篇Spring系列(六) Spring Web MVC 应用构建分析中, 我们大致提到了DispatcherServlet的启动调用关系如下:

    整理下调用关系: DispatcherServlet initHandlerMappings <-- initStrategies <-- onRefresh <--
    FrameworkServlet initWebApplicationContext <-- initServletBean <--
    HttpServletBean init <--
    GenericServlet init(ServletConfig config)
    最后的GenericServlet是servlet Api的.

    正是在initStrategies方法中, DispatcherServlet做了启动的一系列工作, 除了initHandlerMappings还可以看到一个initHandlerExceptionResolvers的方法, 其源码如下:

    // 初始化HandlerExceptionResolver, 如果没有找到任何命名空间中定义的bean, 默认没有任何resolver
    private void initHandlerExceptionResolvers(ApplicationContext context) {
    	this.handlerExceptionResolvers = null;
    
    	if (this.detectAllHandlerExceptionResolvers) {
    		// 找到所有ApplicationContext中定义的 HandlerExceptionResolvers 包括在上级上下文中.
    		Map<String, HandlerExceptionResolver> matchingBeans = BeanFactoryUtils
    				.beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false);
    		if (!matchingBeans.isEmpty()) {
    			this.handlerExceptionResolvers = new ArrayList<>(matchingBeans.values());
    			// 保持有序.
    			AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers);
    		}
    	}
    	else {
    		try {
    			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.
    		}
    	}
    
    	// 确保有Resolver, 否则使用默认的
    	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");
    		}
    	}
    }
    

    好了, 现在我们加载了应用程序中所有定义的Resolver. 当有请求到达时, DispatcherServletdoDispatch方法使用请求特定的handler处理, 当handler发生异常时, 变量dispatchException的值赋值为抛出的异常, 并委托给方法processDispatchResult

    doDispatch的代码, 只摘录出与本议题有关的.

    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    	....
    	try {
    		ModelAndView mv = null;
    		mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    	}catch (Exception ex) {
    		dispatchException = ex;
    	}
    	catch (Throwable err) {
    		// As of 4.3, we're processing Errors thrown from handler methods as well,
    		// making them available for @ExceptionHandler methods and other scenarios.
    		dispatchException = new NestedServletException("Handler dispatch failed", err);
    	}
    	processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    	....
    }
    
    // 处理handler的结果
    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);
    		}
    	}
    
    	// handler是否返回了view
    	if (mv != null && !mv.wasCleared()) {
    		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) {
    		mappedHandler.triggerAfterCompletion(request, response, null);
    	}
    }
    

    processDispatchResult方法中可以看到, 如果参数exception不为null, 则会处理异常, 对于ModelAndViewDefiningException类型的异常单独处理, 对于其他类型的异常, 转交给processHandlerException方法处理, 这个方法就是异常处理逻辑的核心.

    @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);
    
    	// 使用注册的Resolver处理
    	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);
    		}
    		if (logger.isDebugEnabled()) {
    			logger.debug("Using resolved error view: " + exMv);
    		}
    		WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
    		return exMv;
    	}
    
    	throw ex;
    }
    

    从上面代码可以看到, this.handlerExceptionResolvers就是在程序启动时初始化注册的, spring通过遍历Resolver列表的方式处理异常, 如果返回结果不为null, 说明处理成功, 就跳出循环.

    总结

    Spring的异常解析器实现全部继承自接口ResponseStatusExceptionResolver, 上面我们详细了解了该接口在Spring中的几种实现, 比如处理预定义异常的DefaultHandlerExceptionResolver, 可以映射异常到状态码的ResponseStatusExceptionResolver, 还有功能更为强大的ExceptionHandlerExceptionResolver. 同时也简单了解了其使用方式,使用@ExceptionHandler来将方法标记为异常处理器, 结合@ControllerAdvice处理全局异常.

    最后我们探究了异常处理器的加载和处理方式, 我们知道了其通过 DispatcherServlet 的初始化方法initHandlerMappings完成加载器列表的注册初始化, 并且在具体处理请求的doDispatch中检测异常, 最终processDispatchResult方法委托给processHandlerException, 该方法循环注册的异常处理器列表完成处理过程.

    哎...今天够累的,签到来了1...
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|小黑屋|Java自学者论坛 ( 声明:本站文章及资料整理自互联网,用于Java自学者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2024-5-17 00:26 , Processed in 0.073141 second(s), 29 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2021, Tencent Cloud.

    快速回复 返回顶部 返回列表