本文基于spring 5.5.2.release
本文要介绍的异常不仅仅是Controller抛出的异常,还包括自定义拦截器抛出的异常,另外运行过程中springmvc自身也会抛出异常,接下来我们分析一下springmvc如何处理这些异常。
一、什么场景下会抛出异常
在进入正题前,先来分析一下都有哪些地方会抛出异常。
- springmvc自身因为缺陷或者其他原因导致异常,不过因为缺陷导致的异常几乎不可能出现;
- 开发人员自己编写的Controller处理过程中抛出的异常,这种异常占绝大部分,有些异常是程序主动抛出的,有些异常是因为程序缺陷导致的;
- 开发人员自定义拦截器抛出的异常;
- Controller的入参定义了校验规则,不符合校验规则抛出异常,或者入参转换未转换成功而抛出异常;
- 未找到处理当前请求的Controller;
- 未找到视图对象。
二、如何处理异常
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;
}
上面的代码比较多,这里做一下总结:
- 如果在执行过程中抛出异常,包括执行Controller和拦截器,那么进入processDispatchResult()方法;
- 在processDispatchResult()方法里面,如果抛出的异常是ModelAndViewDefiningException,那么从该异常对象中获取View对象,如果不是该异常,那么遍历HandlerExceptionResolver查找一个合适的View对象;
- 如果能找到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两个属性的设置了。