SpringMVC-SpringMVC如何处理Controller抛出的异常

本文基于spring 5.5.2.release

本文要介绍的异常不仅仅是Controller抛出的异常,还包括自定义拦截器抛出的异常,另外运行过程中springmvc自身也会抛出异常,接下来我们分析一下springmvc如何处理这些异常。

一、什么场景下会抛出异常

在进入正题前,先来分析一下都有哪些地方会抛出异常。

  1. springmvc自身因为缺陷或者其他原因导致异常,不过因为缺陷导致的异常几乎不可能出现;
  2. 开发人员自己编写的Controller处理过程中抛出的异常,这种异常占绝大部分,有些异常是程序主动抛出的,有些异常是因为程序缺陷导致的;
  3. 开发人员自定义拦截器抛出的异常;
  4. Controller的入参定义了校验规则,不符合校验规则抛出异常,或者入参转换未转换成功而抛出异常;
  5. 未找到处理当前请求的Controller;
  6. 未找到视图对象。

二、如何处理异常

springmvc处理http请求的核心类是DispatcherServlet,而该类中处理请求的核心方法是doDispatch(),所有的http请求都会转发到该方法中。

//下面代码做了删减
//入参分别是请求对象和响应对象
//所有的http请求都转发到该方法
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
	HttpServletRequest processedRequest = request;
	HandlerExecutionChain mappedHandler = null;
	try {
		ModelAndView mv = null;
		Exception dispatchException = null;
		try {
			//查找Handler对象,可以简单的理解为Handler对象就是Controller
			mappedHandler = getHandler(processedRequest);
			//如果没有找到对应的Handler,那么便无法处理请求
			//下面这个if判断对应了第一节的异常5
			if (mappedHandler == null) {
				//下面这个方法默认是设置Response对象的状态码为404,这个状态码也是该请求的响应报文头的状态码
				//也可以修改配置,让下面这个方法抛出异常
				noHandlerFound(processedRequest, response);
				return;
			}
			HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
			//调用拦截器,可能会抛出异常3
			if (!mappedHandler.applyPreHandle(processedRequest, response)) {
				return;
			}

			//调用Controller处理http请求
			//下面方法可能会抛出第一节提到的异常2/3/5
			mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
			//调用拦截器,可能会抛出异常3
			mappedHandler.applyPostHandle(processedRequest, response, mv);
		}
		catch (Exception ex) {
			dispatchException = ex;
		}
		catch (Throwable err) {
			dispatchException = new NestedServletException("Handler dispatch failed", err);
		}
		//根据ModelAndView查找对应的View对象,这里可能会抛出异常6
		//而且下面方法里面还会处理上面已经catch住的异常
		processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
	}
	catch (Exception ex) {
		triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
	}
	catch (Throwable err) {
		triggerAfterCompletion(processedRequest, response, mappedHandler,
				new NestedServletException("Handler processing failed", err));
	}
	finally {
		//代码省略
	}
}
protected void noHandlerFound(HttpServletRequest request, HttpServletResponse response) throws Exception {
	//throwExceptionIfNoHandlerFound默认为false,如果是true的话,抛出异常
	if (this.throwExceptionIfNoHandlerFound) {
		throw new NoHandlerFoundException(request.getMethod(), getRequestUri(request),
				new ServletServerHttpRequest(request).getHeaders());
	}
	else {
		//标记响应报文的状态码为404
		response.sendError(HttpServletResponse.SC_NOT_FOUND);
	}
}

在doDispatch()方法中,如果抛出异常,首先使用dispatchException记录异常对象,然后调用processDispatchResult()方法,下面来看一下这个方法如何处理:

//代码有删减
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
		@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
		@Nullable Exception exception) throws Exception {

	boolean errorView = false;
	//exception就是上面提到的异常对象
	if (exception != null) {
		//ModelAndViewDefiningException异常表示需要转向一个特殊的异常页面
		//该异常可以在处理http请求的任何位置抛出,对象内部有一个ModelAndView属性用于记录将要转向的页面
		if (exception instanceof ModelAndViewDefiningException) {
			mv = ((ModelAndViewDefiningException) exception).getModelAndView();
		}
		else {
			Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
			//使用HandlerExceptionResolver解析器查找异常对应的View对象
			mv = processHandlerException(request, response, handler, exception);
			errorView = (mv != null);
		}
	}
	//渲染视图,这里渲染的视图可能是正常视图,也可能是根据Exception对象找到的视图
	if (mv != null && !mv.wasCleared()) {
		render(mv, request, response);
	}
}
//本方法主要是根据HandlerExceptionResolver查找View对象,如果找不到便抛出异常
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
		@Nullable Object handler, Exception ex) throws Exception {

	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) {
		//如果没找到View对象,也没有模型对象,那么返回null
		if (exMv.isEmpty()) {
			request.setAttribute(EXCEPTION_ATTRIBUTE, ex);
			return null;
		}
		//如果没找到View对象,那么使用默认View对象
		if (!exMv.hasView()) {
			String defaultViewName = getDefaultViewName(request);
			if (defaultViewName != null) {
				exMv.setViewName(defaultViewName);
			}
		}
		WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
		return exMv;
	}
	throw ex;
}

上面的代码比较多,这里做一下总结:

  1. 如果在执行过程中抛出异常,包括执行Controller和拦截器,那么进入processDispatchResult()方法;
  2. 在processDispatchResult()方法里面,如果抛出的异常是ModelAndViewDefiningException,那么从该异常对象中获取View对象,如果不是该异常,那么遍历HandlerExceptionResolver查找一个合适的View对象;
  3. 如果能找到View对象,便渲染该对象,如果找不到,从doDispatcher()方法总体上来看有两种结果,一是将异常继续往上层抛出,另一个是不做处理直接返回。

根据上面的分析,如果找不到View对象,有两种结果,一是抛异常,二是不做任何处理返回,那么下面我们也分两种情况来看,首先看不抛异常的。
不抛出异常
springmvc执行过程中,有些地方进行检查,发现有问题,便设置Response对象的状态,比如上面代码中的noHandlerFound()方法,标记状态码为404:

//sendError()会设置errorState=1,state=404,这里的属性state其实就是响应报文的状态码,初始值为200
response.sendError(HttpServletResponse.SC_NOT_FOUND);

这样在不抛出异常的情况下,返回到Tomcat,Tomcat检测Response对象的状态,如果发现属性errorState不等于0,那么表示处理过程有问题,Tomcat之后根据Response对象的属性state查找对应的URL,默认都是“/error”,我们可以配置Tomcat,将每个响应报文的状态码对应不同的URL。Tomcat找到URL后,内部直接转发,发起对该URL的http请求。待该URL返回响应后,将响应内容作为报文体返回到客户端,其中返回客户端的报文头内容和最初的响应保持一致。这样便完成了一次完整的http响应。
还有一种没有找到视图资源但不抛异常场景。比如设置如下两个参数:

spring.mvc.view.prefix=/
spring.mvc.view.suffix=.jsp

当执行完下面这个Controller代码后:

@Controller
public class StopApplicationAction{
	@RequestMapping("/test")
	public String stop()throws Exception{
		return "test";
	}
}

springmvc会创建一个视图名为“/test.jsp”的视图对象(也就是View对象),当渲染该视图对象时,从WEN-INF目录下查找“test.jsp”文件,如果没有该文件,那么渲染是无法正常进行的。springmvc接下来怎么做呢?
springmvc会在内部直接发起一个URL为“/test.jsp”的http请求,之后将该URL的响应内容作为响应报文体返回给客户端。发起这个URL请求是借助Tomcat的类RequestDispatcher.forward()方法完成的,不过原理与上面提到的Tomcat的内部转发原理是一样的。

抛出异常
springmvc抛出异常后,会直接抛给Tomcat。Tomcat可以捕获所有的异常,然后执行下面这个方法:

    private void exception(Request request, Response response,
                           Throwable exception) {
        request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, exception);
        //设置Response对象的响应状态码500
        response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        //设置Response对象的errorState=1
        response.setError();
    }

之后Tomcat检测Response对象errorState是否等于0,如果不等于0,接下来根据Response对象的属性state查找对应的URL,之后的处理逻辑和上面不抛出异常的处理逻辑一样了。

三、总结

本文主要介绍了springmvc可能抛出异常的几个场景,之后介绍springmvc和Tomcat如何处理这些异常。
总的来说,如果异常抛给了Tomcat,Tomcat首先设置响应报文的状态码为500,然后内部发起500对应的URL请求,将该URL请求的响应内容作为报文体、状态码为500返回给客户端。
如果不抛出异常,那么依赖于springmvc或者自定义代码对Response对象中的errorStat和state两个属性的设置了。

【Servlet】关于RequestDispatcher的原理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值