介绍 SpringMVC 处理异常之前,我们先来看一下不对异常处理的情况时什么样的。
首先,我们来新建一个 Spring 工程。创建一个Handler
package com.bupt.exception.handler; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @Controller public class MyHandler { @RequestMapping("/testExceptionHandlerExceptionResolver") public String testExceptionHandlerExceptionResolver(@RequestParam("i") int i) { System.out.println("result:" + (10 / i)); return "success"; } }
编写配置文件 springmvc.xml 和 web.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd"> <context:component-scan base-package="com.bupt.exception"/> <mvc:annotation-driven/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/views/"/> <property name="suffix" value=".jsp"/> </bean> </beans>
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" id="WebApp_ID" version="3.1"> <servlet> <servlet-name>springDispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:springmvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>springDispatcherServlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>
编写访问页面 index.jsp 和跳转 success.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<body>
<a href="testExceptionHandlerExceptionResolver?i=10">Test ExceptionHandlerExceptionReslover</a>
</body>
</html>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<body>
<h4>This is SUCCESS page</h4>
</body>
</html>
当启动服务器访问页面时可以看到如下结果
控制台输出
当我们在地址栏中将 i 的值改为0时,页面结果和控制台输出如下
可以看到,此时程序会产生一个数学异常,但我们并没有对此异常进行处理,所以它将这个异常直接往外抛。
但实际中,我们需要对这个异常进行处理,获取异常信息等,并且必要时根据异常的不同,将转发至不同的页面进行处理。
所以我们需要来看一下 spring 的异常处理机制。
想要 SpringMVC 处理异常,我们首先需要将异常解析器装配进来
Spring MVC通过 HandlerExceptionResolver 处理程序的异常,包括处理器映射、数据绑定以及处理器执行时发生的异常。
HandlerExceptionResolver仅有一个接口方法:
ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
当发生异常时,Spring MVC 将调用 resolveException() 的方法,并转到 ModelAndView 对应视图中,作为一个异常页面反馈给用户。
HandlerExceptionResolver 拥有的实现类如下图所示,它包括5个实现类,但 AnnotationMethodHandlerExceptionResolver 已经弃用。
DispatcherServlet 默认装配的 HandlerExceptionResolver 分为两种情况:
1. 没有使用<mvc:annotation-drivern/>配置时,装配如下
2. 使用了<mvc:annotation-drivern/>配置时,装配如下
其实,正常开发时,都会使用 <mvc:annotation-drivern/> 这个配置。
并且 AnnotationMethodHandlerExceptionResolver 这个类已经失效,其实它需要用 ExceptionHandlerExceptionResolver 来代替使用。
所以,我们在使用 Spring MVC 异常处理时都会加上 <mvc:annotation-drivern> 这个配置
现在我们分别来介绍,上面三个类的作用。
1. ExceptionHandlerExceptionResolver
其主要处理 Handler 中用 @ExceptionHandler 注解定义的方法
我们在上面的 MyHandler 中增加如下代码
//在 @ExceptionHandler 方法的入参中可以加入 Exception 参数,该参数即对应发生的异常对象
@ExceptionHandler({ArithmeticException.class})
public String handlerArithmeticException(Exception ex)
{
System.out.println("出现异常: " + ex);
return "error";
}
编写 error.jsp 页面
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<body>
<h4>This is Error page</h4>
</body>
</html>
同样将地址栏的 i 改为0,得到的结果如下
可以看到,当出现异常时,页面跳转到了相应的错误提示页面,控制台也将此异常获取到了。
我们还可以将异常信息在页面上显示
这里我们需要修改 @ExceptionHandler 修饰的方法
//@ExceptionHandler 方法的入参中不能传入 Map。若希望把异常信息传导到页面上,需要使用 ModelAndView 作为返回值 @ExceptionHandler({ArithmeticException.class}) public ModelAndView handlerArithmeticException(Exception ex) { System.out.println("出现异常: " + ex); ModelAndView mv = new ModelAndView("error"); mv.addObject("exception", ex); return mv; }
修改 error.jsp 页面
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <body> <h4>This is Error page</h4> ${exception } </body> </html>
再次访问时,就能将错误信息显示在页面上了
我们可以看到 @ExceptionHandler 注解是需要加上数组类型的参数的,希望处理什么类型的异常就需要往里面增加相应类型的参数。
当出现一个异常匹配多个处理方法时,就涉及到 @ExceptionHandler 方法标记的异常优先级的问题。
在这种情况下,程序往往会选择匹配程度更高的那一个异常处理方法。
如:发生数学异常时,程序同时有处理 ArithmeticException 和 RuntimeException 的两个方法,程序会精确执行 ArithmeticException 处理方法,而不去执行 RuntimeException 的处理方法。只要在不存在 ArithmeticException 处理方法时,程序才会退而求其次去由 RuntimeException 处理方法处理异常。
之前,我们所讲的异常处理都是针对于某个 Handler 而言,当再来一个 Handler 时就需要在这个 Handler 类中重新编写异常处理方法,这样会使我们写很多重复的代码。
一个好的异常处理机制不应该像上面所讲的那样,它应该有一个集中的处理点负责处理异常,在真正的业务逻辑的处理过程中,我们只关心正常的业务流程,一旦遇到异常,我们只管抛出对应的异常和相关信息就行。
SpringMVC 就提供了这样的机制,开发者可以自己定义一个 ExceptionHandler 类处理所有的异常,在这个类上加上 @ControllerAdvice 注解,这个类就以 AOP 的形式注册到 SpringMVC 的处理链条中了,在应用任何地方抛出 Exception,最后都会调用到这个类的方法上,在此类中,可以和前面一样使用 @ExceptionHandler 注解来区别不同的 Exception。
新建一个处理所有异常的类
package com.bupt.exception.exceptionhandler; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.servlet.ModelAndView; @ControllerAdvice public class HandleException { @ExceptionHandler({ArithmeticException.class}) public ModelAndView handleArithmeticException(Exception ex) { System.out.println("出异常了: " + ex); ModelAndView mv = new ModelAndView("error"); mv.addObject("exception", ex); return mv; } }
将之前的 MyHandler 类中处理异常的方法注释掉,只留下处理请求映射的方法。
同样访问 i 为0的情况,可以看到,仍然能正常处理异常。
需要注意的是:当发生异常时,如果同时存在处理所有异常的类和在 Handler 中存在匹配的异常处理方法时,最后执行的异常处理方法是 Handler 中匹配的方法而不去执行异常类中的方法。
2. ResponseStatusExceptionResolver
ResponseStatusExceptionResolver 是 HandlerExceptionResolver 接口的一个实现,它使用 @ResponseStatus 注解来将异常映射为指定的 Http 状态码。
@ResponseStatus 注解用于标记一个方法或异常类,它有两个属性值 value 和 reason 可以用于返回给响应。当方法被调用或异常发生时,属性值就会作用于响应。
代码演示如下:
重新编写 MyHandler 类
package com.bupt.exception.handler; import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam;
import com.bupt.exception.MyException.UserDefineException; @Controller public class MyHandler { @RequestMapping("/testResponseStatusExceptionResolver") public String testResponseStatusExceptionResolver(@RequestParam("i") int i) { if(i == 11) { throw new UserDefineException(); } System.out.println("testResposneStatusResolver..."); return "success"; } }
index.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<body>
<a href="testResponseStatusExceptionResolver?i=11">Test ResponseStatusExceptionResolver</a>
</body>
</html>
编写自定义的异常类
package com.bupt.exception.MyException; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; @ResponseStatus(value=HttpStatus.FORBIDDEN, reason="发生异常") public class UserDefineException extends RuntimeException { private static final long serialVersionUID = 1L; }
当访问index.jsp页面的超链接时,可以看到如下跳转页面
此时页面显示的异常信息,与我们在自定义异常类上的 @ResponseStatus(value=HttpStatus.FORBIDDEN, reason="发生异常") 注解内容一致。
我们再将注解改为 @ResponseStatus(value=HttpStatus.NOT_FOUND, reason="发生异常!!!"),结果页面为
由此可以看出,通过对 @ResponseStatus 注解属性值的改写,可以达到自定义异常信息的效果
以上是将注解作用于异常类的情况,现在我们来看作用于方法上
在 MyHandler 类中的方法上增加 @ResponseStatus 注解
@ResponseStatus(value=HttpStatus.FORBIDDEN, reason="作用于方法") @RequestMapping("/testResponseStatusExceptionResolver") public String testResponseStatusExceptionResolver(@RequestParam("i") int i) { if(i == 11) { throw new UserDefineException(); } System.out.println("testResposneStatusResolver..."); return "success"; }
访问路径和结果如下图
如果我传入的参数是i=10,程序应该正常执行跳转 success.jsp 页面,但页面却跳转至错误页面。
究其原因还是因为 @ResponseStatus 注解,前面讲过此注解可以用于注解异常类和方法,只要有被修饰的异常发生或被修饰的方法得到调用,注解中的属性值就会传递给响应,从而定制我们想要的效果页面。
在这里,MyHandler 中的方法被调用了,同时有 @ResponseStatus 修饰它,所以它会跳转错误页面,注解中的信息也会在页面显示。
当然,程序还是会正常执行,即打印语句还是会在控制台打印,只不过它不会跳转至正确的页面。
这里还需要注意的是:当发生异常,程序同时有被 @ExceptionHandler 修饰的方法和被 @ResponseStatus 修饰的类时,异常先去匹配 @ExceptionHandler,如果匹配成功则执行被此注解修饰的方法,而不再执行 @ResponseStatus 注解的逻辑;只有匹配不成功时,才执行 @ResponseStatus 的逻辑。
如:我在 MyHandler 中修改方法
@RequestMapping("/testResponseStatusExceptionResolver") public String testResponseStatusExceptionResolver(@RequestParam("i") int i) { if(i == 11) { throw new UserDefineException(); } System.out.println("testResposneStatusResolver..."); return "success"; } @ExceptionHandler({RuntimeException.class}) public ModelAndView handleException(Exception ex) { System.out.println("---> 出现异常: " + ex); ModelAndView mv = new ModelAndView("error"); return mv; }
当我再次传入 i=11 时,因为 UserDefineException 继承了 RuntimeException,而且存在此异常的处理方法。所以,页面不再显示我们在 UserDefineException 类中自定义的错误信息,而是去执行被 @ExceptionHandler 修饰的方法逻辑,所以页面跳转至 error.jsp
3. DefaultHandlerExceptionResolver
SpringMVC 默认装配了 DefaultHandlerExceptionResolver,它会将 SpringMVC 框架的一些特殊异常转换为相应的响应状态码,具体说明如下表
异常类型 | 响应状态码 |
ConversionNotSupportedException | 500(Web服务内部错误) |
HttpMediaTypeNotAcceptableException | 406(无和请求accept匹配的MIME类型) |
HttpMediaTypeNotSupportedException | 415(不支持的MIME类型) |
HttpMessageNotReadableException | 400(坏的请求) |
HttpMessageNotWritableException | 500 |
HttpRequestMethodNotSupportedException | 405(不支持的请求方法) |
MissingServletRequestParameterException | 400 |
NoSuchRequestHandlingMethodException | 404(找不到匹配的资源) |
TypeMismatchException | 400 |
它具体起到的作用,可以用代码来演示
MyHandler 新增方法
@RequestMapping(value="/testDefaultHandlerExceptionResolver", method=RequestMethod.POST) public String testDefaultHandlerExceptionResolver() { return "success"; }
index.jsp 新增连接
<br><br><a href="testDefaultHandlerExceptionResolver">Test DefaultHandlerExceptionResolver</a>
当访问新增的连接时,由于处理映射的方法要求采用 POST 方法提交请求,但我们点击超链接使用的是 GET 提交请求,此时程序将报错。跳转页面如下图
这个页面与我们之前使用 @ResponseStatus 自定义的错误页面很像,显然,这是由 SpringMVC 默认机制进行处理后的页面,处理这些异常的解析器就是 DefaultHandlerExceptionResolver 这个类。
4. SimpleMappingExceptionResolver
如果希望对所有异常进行统一处理,可以使用 SimpleMappingExceptionResolver,它将异常类名映射为视图名,即发生异常时使用对应的视图报告异常。
个人感觉这个类完成的工作类似于被 @ControllerAdvice 注解修饰的类完成的工作。
需要注意的是:
MyHandler中不能有被 @ExceptionHandler({RuntimeException.class}) 修饰的方法,以及被 @ControllerAdvice 修饰的类中也不能有 @ExceptionHandler({RuntimeException.class}) 修饰的方法。不然将执行被 @ExceptionHandler 修饰的方法逻辑,而使得 SimpleMappingExceptionResolver 和 @ControllerAdvice 不起作用。(说明 @ExceptionHandler 优先级更高)
具体用法如下,MyHandler 类中新增方法,
@RequestMapping("/testSimpleMappingExceptionResolver") public String testSimpleMappingExceptionResolver(@RequestParam("i") int i) { String[] vals = new String[10]; System.out.println(vals[i]); return "success"; }
index.jsp新增连接
<br><br><a href="testSimpleMappingExceptionResolver?i=21">Test SimpleMappingExceptionResolver</a>
很显然,当访问上面的连接时,程序将会抛出一个 ArrayIndexOutOfBoundsException 异常。现在我们通过 SimpleMappingExceptionResolver 来处理当发生这个异常时,应该映射去哪个页面。
在 SpringMVC.xml 中增加 SimpleMappingExceptionResolver 配置信息
<!-- 配置使用 SimpleMappingExceptionResolver 来映射异常 --> <bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<!-- 解析器默认将异常信息放置到请求域中,value值指定attributeName,没有指定时,默认值为exception --> <property name="exceptionAttribute" value="ex"></property>
<property name="exceptionMappings"> <props> <!-- key值来指定映射何种异常,需要写全类名;prop值来指定映射到哪个页面 --> <prop key="java.lang.ArrayIndexOutOfBoundsException">error</prop> </props> </property> </bean>
error.jsp增加如下代码
${requestScope.ex }
点击超链接跳转页面如下