SpringMVC源码分析-400异常处理流程及解决方法

本文涉及SpringMVC异常处理体系源码分析,SpringMVC异常处理相关类的设计模式,实际工作中异常处理的实践。

问题场景

假设我们的SpringMVC应用中有如下控制器:

代码示例-1
@RestController("/order")
public class OrderController{
    
  @RequestMapping("/detail")
  public Object orderDetail(int orderId){
      // ... 
  }
  
}

这个控制器中接收了一个参数:int 类型的orderId。假设我在请求的使传递的参数为orderId=99999999999或者orderId=53844181132132asdf。很显然,我们的第一个参数超出了int的范围,第二个参数类型不符合。这时肯定会报400错误,假设我们的应用是部署在tomcat里边的,我们会得到的错误页面是这样的:

代码示例-2
<html>
    <head><title>Apache Tomcat/7.0.42 - Error report</title>
        <style>
            <!--H1 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:22px;} H2 {font-family:Tahoma,sans-serif;color:white;background-color:#525D76;font-size:16px;} H3 {font-family:Tahoma,sans-serif;color:white;background-color:#525D76;font-size:14px;} BODY {font-family:Tahoma,sans-serif;color:black;background-color:white;} B {font-family:Tahoma,sans-serif;color:white;background-color:#525D76;} P {font-family:Tahoma,sans-serif;background:white;color:black;font-size:12px;}A {color : black;}A.name {color : black;}HR {color : #525D76;}-->
        </style> 
    </head>
    <body>
        <h1>HTTP Status 400 - </h1>
        <HR size="1" noshade="noshade">
        <p><b>type</b> Status report</p>
        <p><b>message</b> <u></u></p>
        <p>
            <b>description</b>
            <u>The request sent by the client was syntactically incorrect.</u>
        </p><HR size="1" noshade="noshade">
        <h3>Apache Tomcat/7.0.42</h3>
    </body>
</html>

当我们碰到这个错的时候,实际上都没有进入目标方法,控制台也看不到controller方法执行的日志相关信息。根据经验,我们知道这是请求错误,是请求参数不匹配导致的(实际抛出的异常是:org.springframework.beans.TypeMismatchException: Failed to convert value of type)。也许你会说解决这个问题,只需要传递正确的参数就可以了,但是spring是怎么处理这个错误的,流程是怎样?如果了解这些,对于我们解决问题更有帮助。

源码调试分析

为了追踪处理过程,我会使用断点调试的方式。我们知道,SpringMVC的核心是DispatchServlet。所有的请求会被DispatchServlet接收,并在其doDispatch(...)方法中处理。doDispatch()方法会找到对应的handler,然后invoke。所以我们在doDispatch方法中打个断点。我们使用postman发起一个请求,并传递一个错误的参数。先贴一点doDispatch()方法的代码:

代码示例-3
protected void doDispatch(HttpServletRequest request,HttpServletResponse response) throws Exception {
		//删除一些代码
		try {
			ModelAndView mv = null;
			Exception dispatchException = null;

			try {
				// 删除一些代码方便阅读
				try {
					// Actually invoke the handler.
					mv = ha.handle(processedRequest,response,mappedHandler.getHandler());
				}
				finally {
					if (asyncManager.isConcurrentHandlingStarted()) {
						return;
					}
				}
				applyDefaultViewName(request,mv);
				mappedHandler.applyPostHandle(processedRequest,mv);
			}
			catch (Exception ex) {
				dispatchException = ex;  // 这里捕获了异常TypeMismatchException
			}
			processDispatchResult(processedRequest,mappedHandler,mv,dispatchException);
		}
		catch (Exception ex) {
		}
		finally {
			// 删除一些代码
		}
	}

当请求进入doDispatch()方法之后,单步执行发现,发生了一个异常,然后,这个异常被catch住了,catch块里边进行了如下操作:

代码示例-4
dispatchException = ex;

异常的详细信息是:

代码示例-5
org.springframework.beans.TypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'java.lang.Integer'; nested exception is java.lang.NumberFormatException: For input string: "53844181132132asdf"

继续执行,走出了catch块之后,便进入了processDispatchResult方法:

代码示例-5
private void processDispatchResult(HttpServletRequest request,HttpServletResponse response,HandlerExecutionChain mappedHandler,ModelAndView mv,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,handler,exception);// 执行这个方法
				errorView = (mv != null);
			}
		}
 		// 方便阅读,删除了其他代码
  
} 

这个方法中对异常进行判断,发现不是“ModelAndViewDefiningException”就交给processHandlerException()方法继续处理。processHandlerException方法代码如下:

代码示例-6
protected ModelAndView processHandlerException(HttpServletRequest request,Object handler,Exception ex) throws Exception {
		// Check registered HandlerExceptionResolvers...
		ModelAndView exMv = null;
		for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) {
			exMv = handlerExceptionResolver.resolveException(request,ex);
			if (exMv != null) {
				break;
			}
		}
		// 去掉了一些代码
		throw ex;
	}

这里的for循环是为了找一个handler来处理这个异常。这里的handler列表有:

  • ExceptionHandlerExceptionResolver
  • ResponseStatusExceptionResolver
  • DefaultHandlerExceptionResolver
  • 自定义的ExceptionResolver 1
  • ...
  • 自定义的ExceptionResolver N

异常体系的设计模式

在上面的代码中,通过for循环需要在众多的handler中找一个HandlerExceptionResolver的实现类来处理异常。这里的handler列表是在应用初始化的时候就创建了,前三个是spring内部自带的,后面是我们自定义的(如果有的话)。处理异常的方法是resolveException(),它其实是在HandlerExceptionResolver接口中定义的,该接口只有一个方法resolveException(),代码如下:

代码示例-7
public interface HandlerExceptionResolver {
	ModelAndView resolveException(
		HttpServletRequest request,Exception ex);
}

Spring自带的ExceptionHandlerExceptionResolverResponseStatusExceptionResolverDefaultHandlerExceptionResolver都是继承自AbstractHandlerExceptionResolver类,这个类是一个抽象类,它实现了HandlerExceptionResolver接口,它对HandlerExceptionResolver接口约定的方法的所实现代码是这样的:

代码示例-8	
public ModelAndView resolveException(HttpServletRequest request,Exception ex) {
		if (shouldApplyTo(request,handler)) {
          
			logException(ex,request);
			prepareResponse(ex,response);
			return doResolveException(request,ex);
		}
		else {
			return null;
		}
	}

这个方法其实是一个模板,这里使用的是模板方法设计模式。这个模板定义了处理异常的逻辑,return null或者进入if执行“三步走”,看上面代码,这三步分别是:

  • logException(ex,request);
  • prepareResponse(ex,response);
  • doResolveException(request,ex);

这里的第三部doResolveException(request,ex)是一个抽象方法,它也是我们的模板方法。它的声明是这样的:

代码示例-9
protected abstract ModelAndView doResolveException(HttpServletRequest request,Exception ex);

这个抽象方法就是留个子类来实现的。模板我定好了,子类想咋处理就怎么实现。无论你咋实现,反正我这“三步走”是已经定好的了。所以,模板方法设计模式就是这样:“定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。TemplateMethod使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。”

继续回到代码逻辑,刚才讲到,我们的for循环遍历当前的handler,并调用当前handler的resolveException方法。正如 [代码示例-8 ] 所示这个resolveException方法是个模板方法,它的第一步就是一个if判断,这个判断的方法代码如下:

代码示例-10	
protected boolean shouldApplyTo(HttpServletRequest request,Object handler) {
		if (handler != null) {
			if (this.mappedHandlers != null && this.mappedHandlers.contains(handler)) {
				return true;
			}
			if (this.mappedHandlerClasses != null) {
				for (Class handlerClass : this.mappedHandlerClasses) {
					if (handlerClass.isInstance(handler)) {
						return true;
					}
				}
			}
		}
		return (this.mappedHandlers == null && this.mappedHandlerClasses == null);
	}

this.mappedHandlers 是一个 Set,它存储了当前异常处理器有哪些handler。如果这个set不为空,并且包含了当前的目标handler,那就说明这个异常处理器可以处理当前的目标handler。(这里所说的handler其实就是controller的目标方法,以开篇的例子来说,这个handler类包含的信息、目标方法,总之handler指明我们要调用的是OrderController类的orderDetail方法)。

于是,我们的for循环,依次发现了ExceptionHandlerExceptionResolver不能处理,ResponseStatusExceptionResolver也不能处理,下一个轮到DefaultHandlerExceptionResolver的时候,可以了,进入了if里边的“三步走”。最终执行了该类对模板方法doResolveException的实现代码,这个代码是这样的:

代码示例-11
@Override
	protected ModelAndView doResolveException(HttpServletRequest request,Exception ex) {

		try {
			if (ex instanceof NoSuchRequestHandlingMethodException) {
				return handleNoSuchRequestHandlingMethod(...);
			}
			// 删除部分else if   instanceof 判断
			else if (ex instanceof TypeMismatchException) {
              // 执行到了这里
				return handleTypeMismatch((TypeMismatchException) ex,request,handler);
			}
			// 删除部分else if   instanceof 判断
			else if (ex instanceof BindException) {
				return handleBindException((BindException) ex,handler);
			}
		}
		catch (Exception handlerException) {
		}
		return null;
	}

这个方法,对异常类型进行判断,上面提到,由于我们传递的错误参数导致了TypeMismatchException异常,所以,根据上面的代码,我们本次的错误被handleTypeMismatch()方法处理了。handleTypeMismatch方法的代码非常的简单,全部代码如下:

protected ModelAndView handleTypeMismatch(TypeMismatchException ex,HttpServletRequest request,Object handler) throws IOException {

		response.sendError(HttpServletResponse.SC_BAD_REQUEST);
		return new ModelAndView();
}

执行到这里,最终返回了一个 new ModelAndView()对象。根据 [ 代码示例-6 ] 中的代码所示,程序终于可以跳出这个for循环了。进入下面的if语句之后,由于得到的是一个空的 ModelAndView对象,所以执行了exMv.isEmpty()的代码,return 了null。

接下来程序便回到了processDispatchResult方法,调用了mappedHandler.triggerAfterCompletion(request,null);之后,一切便结束了。这里的方法调用是责任链设计模式,本篇不在过多的解释,意思就是异常处理之后,继续交给后续的intercepter处理。最终,我们便看到了开篇所给出的400页面。

如何解决参数异常导致的400错误

经过上面的分析,我们已经知道了这个400错误是如何发生的。那么改如何解决呢?通常情况下,我们的应用都会有很多controller和方法,这么多的controller和方法我们不可能一个个的去处理。所以,通常来说,定义一个全局的处理器会是一个比较好的选择。spring给了我们很多的选择。(感兴趣的可以看:https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc)

本例中,我为了处理这个400错误,使用了如下的方式。新建一个类GlobalDefaultExceptionHandler,并保证该类可以被spring容器初始化,其代码如下:

@ControllerAdvice
public class GlobalDefaultExceptionHandler {
    @ExceptionHandler(value = TypeMismatchException.class)
    @ResponseBody
    public Object defaultErrorHandler1(HttpServletRequest req,Exception e) throws Exception {
        
        if (AnnotationUtils.findAnnotation(e.getClass(),ResponseStatus.class) != null) {
            throw e;
        }
        ResaultBean res = new ResaultBean("请求的参数中有格式错误");
        return res;
    }
  
    @ExceptionHandler(value = HttpRequestMethodNotSupportedException.class)
    public Object defaultErrorHandler2(HttpServletRequest req,ResponseStatus.class) != null) {
            throw e;
        }
        ModelAndView mav = new ModelAndView();
        mav.addObject("exception",e);
        mav.addObject("url",req.getRequestURL());
        mav.setViewName("error");
        return mav;
    }
  
}

本例中将@ ControllerAdvice 和 @ ExceptionHandler搭配使用,实现了对TypeMismatchException和HttpRequestMethodNotSupportedException的处理。当有这两个异常发生时,分别会执行这里的逻辑,并返回我们自定义的结果。

注意,在defaultErrorHandler1()方法中,我们还搭配了@ ResponseBody注解,使用过springmvc的同学都知道,到我们在controller的某个方法上注解@ ResponseBody的时候,表示这个方法返回的是json,而不是某个视图页面。同理,这里的异常处理加上@ ResponseBody注解,表示对这个异常的处理结果返回的也是。开发api的同学需要正是这个配置,而不是在“正常情况下返回json,错误的情况下400页面html”那就很糟糕了。另外@ ExceptionHandler 搭配 ResponseBody使用好像是在spring 3.1之后才支持的,之前是只能返回ModelAndView 和String ( 也是一个页面配置)。但是这个可以忽略,因为现在大家用的都是高版本的了。

defaultErrorHandler2()中,返回的是ModelAndView。即,我们可以也可以指定返回某个页面。在这个例子中,我使用了两个@ExceptionHandler注解分别处理了两个异常情况。你当然可以使用@ExceptionHandler(value = Exception.class)来处理所有的异常了。

@ExceptionHandler的原理其实就是,就是将其所注解的处理类,配置到了ExceptionHandlerExceptionResolver类的exceptionHandlerCache中,上面说的for循环在挑选处理器的时候,会找到ExceptionHandlerExceptionResolver来处理。后面就映射到了我们自定义处理类GlobalDefaultExceptionHandler中的相应方法。然后我们看到的结果就是:

{
    "code": 10001,"message": "请求的参数中有格式错误"
}

至此,400错误的发生和解决算是粗略的讲完了。这里我虽然是调试了代码,并分析了相关的执行流程,以及设计模式。但是还是感觉略知一二。要想完全弄清楚,还是需要继续深入的。Spring真的强大的,设计的好,功能全,代码写的也漂亮。值得学习啊。

附加一点:

如何处理请求处理过程中发送的异常

本文主要是想通过源码来分析400错误发生的过程,顺带的了解一下SpringMVC异常处理方面的设计。这里补充一点,如果我们想处理请求过程中发生的异常。那么我们只需要实现HandlerExceptionResolver接口即可。实现的方法如下:

public class ApiHandlerExceptionResolver implements HandlerExceptionResolver {
 @Override
	public ModelAndView resolveException(HttpServletRequest request,Exception exception) {
        ModelAndView model = new ModelAndView();
       // do something ...
      
      return model;
    } 
} 

通过这个ApiHandlerExceptionResolver,当我们的controller方法在执行过程中,抛出了异常(自己并未try,catch捕获的)比如说空指针异常,数组越界异常等。就可以走这里了,而不是返回一个tomcat 500错误页面。这个配置算是比较常用的,所以不再解释。反而是上面所说的400处理,即请求处理之前的错误一些应用中并未配置。

相关文章

开发过程中是不可避免地会出现各种异常情况的,例如网络连接...
说明:使用注解方式实现AOP切面。 什么是AOP? 面向切面编程...
Spring MVC中的拦截器是一种可以在请求处理过程中对请求进行...
在 JavaWeb 中,共享域指的是在 Servlet 中存储数据,以便在...
文件上传 说明: 使用maven构建web工程。 使用Thymeleaf技术...
创建初始化类,替换web.xml 在Servlet3.0环境中,Web容器(To...