1 异常处理

Spring 提供了多种方式将异常转换为响应:

  • 特定的 Spring 异常将会自动映射为指定的 HTTP 状态码;
  • 异常上可以添加 @ResponseStatus 注解,从而将其映射为某一个 HTTP 状态码;
  • 在方法上可以添加 @ExceptionHandler 注解,使其用来处理异常。

1.1 将异常映射为 HTTP 状态码

Spring 异常HTTP 状态码
BindException400 - Bad Request
ConversionNotSupportedException500 - Internal Server Error
HttpMediaTypeNotAcceptableException406 - Not Acceptable
HttpMediaTypeNotSupportedException415 - Unsupported Media Type
HttpMessageNotReadableException400 - Bad Request
HttpMessageNotWritableException500 - Internal Server Error
HttpRequestMethodNotSupportedException405 - Method Not Allowed
MethodArgumentNotValidException400 - Bad Request
MissingServletRequestParameterException400 - Bad Request
MissingServletRequestPartException400 - Bad Request
NoSuchRequestHandlingMethodException404 - Not Found
TypeMismatchException400 - Bad Request

表中的异常一般会由 Spring 自身抛出,作为 DispatcherServlet 处理过程中或执行校验时出现问题的结果。例如,如果 DispatcherServlet 无法找到适合处理请求的控制器方法,那么将会抛出 NoSuchRequestHandlingMethod-Exception 异常,最终的结果就是产生 404 状态码的响应(Not Found)。

除了上述的默认映射外,Spring 提供了一种机制,能够通过 @ResponseStatus 注解将异常映射为 HTTP 状态码。

假如当从数据库寻找某对象失败时抛出 SpittleNotFoundException 异常,若不进行处理,一般情况下会返回 500 状态码。此处,对状态码进行了变更,通过 @ResponseStatus 注解将异常映射为 404 Not Found。若控制器抛出该异常,那么将返回该状态码。

package spittr.web;
 
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
 
@ResponseStatus(value=HttpStatus.NOT_FOUND, reason="Spittle Not Found")
public class SpittleNotFoundException extends RuntimeException {
}

2 全局异常处理

2.1 SpringMVC 全局异常处理的四种方式

在项目上线之后,往往会出现一些不可预料的异常信息,对于逻辑性或设计性问题,开发人员或者维护人员需要通过日志,查看异常信息并排除异常;而对于用户,则需要为其呈现出其可以理解的异常提示页面,让用户有一个良好的使用体验。所以异常的处理对于一个 Web 项目来说是非常重要的。Spring MVC 提供了强大的异常处理机制。

Spring MVC 提供的异常处理主要有以下四种方式:

  • 使用 Spring MVC 提供的简单异常处理器 SimpleMappingExceptionResolver
  • 实现异常处理接口 HandlerExceptionResolver
  • 使用 @ExceptionHandler 注解实现异常处理
  • 使用 @ControllerAdvice + @ExceptionHandler 注解 (推荐)

注意:如果 XML 中也配置了相同的映射关系,那么 SpringMVC 会优先采纳基于注解的映射

2.2 通过 SimpleMappingExceptionResolver 实现

SimpleMappingExceptionResolver 异常处理器是 SpringMVC 定义好的异常处理器。使用 SimpleMappingExceptionResolver 进行异常处理的优缺点:

  • 优点:集成简单、有良好的扩展性、对已有代码没有入侵性等
  • 缺点:该方法仅能获取到异常信息,若在出现异常时,对需要获取除异常以外的数据的情况不适用。

下面在 SpringMVC 的 XML 文件中配置 SimpleMappingExceptionResolver 对象,配置如下。

<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver" id="exceptionResolver">
    <!-- 指定默认的异常响应页面。若发生的异常不是exceptionMappings中指定的异常,则使用默认异常响应页面。 -->
    <property name="defaultErrorView" value="error"></property>
 
    <!-- exceptionAttribute属性:设置将异常对象存入 request 域时使用的属性名 -->
    <!-- 如果没有配置这个属性,那么默认使用"exception"作为属性名。源码中文档说明如下:
           * Set the name of the model attribute as which the exception should be exposed. Default is "exception". -->
    <property name="exceptionAttribute" value="exception"></property>
 
    <!-- 用于指定具体的不同类型的异常所对应的异常响应页面。 -->
    <property name="exceptionMappings">
        <props>
            <!-- key属性:指定异常类型 -->
            <!-- 文本标签体:指定和异常对应的逻辑视图名称 -->
            <prop key="java.lang.ArithmeticException">show-message</prop>
            <prop key="java.lang.RuntimeException">show-runtime-message</prop>
            <prop key="java.lang.Exception">show-exception-message</prop>
        </props>
    </property>
</bean>

上面配置了三个异常类型,那么它的匹配规则是什么呢?是从最大的异常开始,还是精确匹配呢?

  • 匹配规则 1:如果异常对象能够在映射关系中找到精确匹配的规则,那么就执行这个精确匹配的规则
  • 匹配规则 2:如果异常对象能够在映射关系中找到多个匹配的规则,优先采纳精确匹配的规则
  • 匹配规则 3:如果异常对象能够在映射关系中找到多个匹配的规则,且没有精确匹配的,那么会采纳范围更接近的那个
  • 匹配规则 4:如果注解中也配置了相同的映射关系,那么 SpringMVC 会优先采纳基于注解的映射

结论:在整个 SpringMVC 全局异常处理中,当异常发生时,会优先采取精确匹配的规则,没有的话会采纳范围更接近的那个,其它的同理。

下面创建模拟出现异常的 Controller 方法:

@RequestMapping(value = "/exception")
public String exceptionHandler(){
    // 模拟出现异常
    System.out.println(10 / 0);
    return "success";
}

用于展示异常信息的页面:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
  <head>
    <title>异常信息页面</title>
  </head>
  <body>
    <h1>系统信息</h1>
    异常对象:${requestScope.exception}<br />
    异常消息:${requestScope.exception.message}<br />
  </body>
</html>

测试的结果如下图所示:

image

2.3 通过实现 HandlerExceptionResolver 接口

上面使用的是 Spring MVC 定义好的 SimpleMappingExceptionResolver 异常处理器,可以实现发生指定异常后跳转到指定的页面。但若要实现在捕获到指定异常时,执行一些额外操作它是完成不了的。此时,就需要自定义异常处理器,需要使用到 HandlerExceptionResolver 接口。

首先新建一个自定义异常类 CustomException:

@Data
public class CustomException extends Exception{
    private String message;
    public CustomException(String message) {
        super(message);
        this.message = message;
    }
}

然后创建一个实现 HandlerExceptionResolver 接口的实现类,并且实现其唯一的方法 resolveException (),这种方式可以进行全局的异常处理。

@Component
public class GlobalException implements HandlerExceptionResolver {
 
    @Override
    public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception exception) {
        System.out.println(exception.getMessage());
        CustomException customException = null;
 
        if (exception instanceof CustomException){
            customException = (CustomException) exception;
        }else {
            customException = new CustomException("出现了未知的错误!!!");
        }
 
        String message = customException.getMessage();
        System.out.println(message);
        System.out.println("---------");
 
        ModelAndView mv = new ModelAndView();
        mv.addObject("exception",customException);
        mv.setViewName("show-exception-message");
        return mv;
    }
}

ResolveException 方法的参数“Exception e”即为 Controller 或其下层抛出的异常。参数“Object o”就是处理器适配器要执行的 Handler 对象。ResolveException 方法的返回值类型是 ModelAndView,也就是说,可以通过这个返回值类设置发出异常时显示的页面。

2.4 使用 @ExceptionHandler 注解

@ExceptionHandler 注解用来将一个方法标注为异常处理方法。 该注解中只有一个可选的属性 value,是一个 Class<?>数组,用于指定该注解的方法所要处理的异常类,即所要匹配的异常。被该注解修饰的方法的返回值为异常处理后的跳转页面,其返回值可以是 ModelAndView、String,或 void;方法名随意,方法的参数可以是 Exception 及其子类对象、Model、HttpServletRequest、HttpServletResponse 等。系统会自动为这些方法参数赋值。

@ExceptionHandler 注解处理异常的作用域:单个类,只针对当前 Controller。

@Controller
public class ExceptionController {
    @RequestMapping(value = "/exception1")
    public String exception1() {
        System.out.println(10 / 0);
        return "success";
    }
 
    @RequestMapping(value = "/exception2")
    public void exception2() throws CustomException {
        throw new CustomException("我抛出了一个异常!!!");
    }
 
    //处理自定义异常
	// Exception e 参数必须,Model 不必须
    @ExceptionHandler({CustomException.class, ArithmeticException.class})
    public String exceptionHandler1(Exception e, Model model) {
        // 打印错误信息
        System.out.println(e.getMessage());
        e.printStackTrace();
        // 将错误数据存入请求域
        model.addAttribute("exception", e);
        return "show-annotation-message";
    }
}

注意:如果在 Controller 中单独使用这个注解是有缺陷的,就是不能够全局处理异常,因为进行异常处理的方法必须与出错的方法在同一个 Controller 里面,也就是说每个 Controller 类中都要写一遍,所以实用性不高。

解决方案:可以将处理异常的信息抽取出来放在一个 BaseController,然后对需要处理异常的 Controller 继承该类即可。

public class BaseController {
    @ExceptionHandler({CustomException.class, ArithmeticException.class})
    public String exceptionHandler1(Exception e, Model model) {
        System.out.println(e.getMessage());
        e.printStackTrace();
        model.addAttribute("exception", e);
        return "show-annotation-message";
    }
}

但是还是存在同样的问题,每个类都得继承它,可见这种方式同样不可取,所以一般使用下面这种方式:@ControllerAdvice 和@ ExceptionHandle 注解配合使用。

2.5 用 @ControllerAdvice+@ ExceptionHandler 注解(推荐)

上面说到 @ExceptionHandler 注解标注的异常处理方法必须与出错的方法在同一个 Controller 里面,所以这种方式是只对应单个 Controller 类。那么此时有一种更好的解决方案:可以使用@ControllerAdvice+@ExceptionHandler 注解来解决,这个是 Spring 3.2 带来的新特性。一般情况下,可以设定 Exception 接口,然后根据不同的对象 or 服务去实现。在实现类上添加 @ControllerAdvice,实现方法上添加 @ExceptionHandler。

两者一起使用的作用域:全局异常处理,针对全部 Controller 中的指定异常类

@ControllerAdvice 和@ ExceptionHandler 这两个注解配合使用的代码如下:

// 异常处理类
@ControllerAdvice
public class MyException {
    // 在@ExceptionHandler 注解中指定异常类型
    @ExceptionHandler(value = {CustomException.class, ArithmeticException.class})
    public ModelAndView exceptionMapping(Exception exception) {
        // 可以将异常对象存入模型;将展示异常信息的视图设置为逻辑视图名称
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.addObject("exception", exception);
        modelAndView.setViewName("show-annotation-message");
        return modelAndView;
    }
 
	//处理其它异常
	@ExceptionHandler
    public ModelAndView exceptionMapping(Exception exception) {
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.addObject("exception", exception);
        modelAndView.setViewName("defaultException");
        return modelAndView;
    }
}

注:@ControllerAdvice 注解的内部是使用@Component 注解修饰的,可以点进源码查看运行:

image

@Component,@Service,@Controller,@Repository 注解修饰的类,就是把这个类的对象交由 Spring IOC 容器来管理,相当于配置文件中的 <bean id="" class=""/>

SpringMVC 的配置文件
1)组件扫描器,扫描@Controller 注解
2)组件扫描器,扫描@ControllerAdvice 所在的包名
3)声明注解驱动

<!--处理需要的两步-->
<context:component-scan base-package="com.bjpowernode.handler" />
<mvc:annotation-driven />

2.6 BeanValidator 异常处理 TODO