Spring MVC 框架学习九:Spring MVC 中的异常处理

介绍 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 }

点击超链接跳转页面如下

原文地址:https://www.cnblogs.com/2015110615L/p/5804143.html