21.11 异常的处理
21.11.1 HandlerExceptionResolver(处理器异常解析器)
Spring的handlerExceptionResolver的实现,可以处理在controller执行时的异常。
HandlerExceptionResolver与在web应用的web.xml中定义的异常映射很相像。但是这个解析器更灵活。比如,当异常抛出的时候,这个解析器能提供具体正在执行的handler处理器的信息。此外,以编程的方式来处理异常,可以在请求被forward到其他URL前,给你更多的选择性来对请求进行合适的响应。(与使用Servlet特定的异常映射是一样的结果)。
除了实现HandlerExceptionResolver接口之外(这个接口就是实现resolveException(Exception, Handler)方法并且返回一个ModelAndView),你也可以使用SimpleMappingExceptionResolver,或者创建@ExceptionHandler方法。SimpleMappingExceptionResolver可以让你获取到可能抛出异常的任何类的类名,并且把它映射到视图名上。这与Servlet API里的异常映射特性是等价的。但是,它在不同的handler处理器中,可以更精细地实现异常的映射。另一方面,@ExceptionHandler注解可以调用方法来处理异常。这个方法可以在本地的@Controller中定义,或者可以在@ControllerAdvice类中使用,用来在多个@Controller类中生效。具体解释详见下文。
21.11.2 @ExceptionHandler
HandlerExceptionResolver接口和SimpleMappingExceptionResolver的实现让你可以把异常映射到指定视图上,在forward这些视图前,可以声明并附带一些Java逻辑。然而,在一些情况下,特别是在依赖@ResponseBody方法,而不是视图解析的时候,它会更方便,因为可以直接设置响应的状态,并且可以把错误内容写入到响应的主体中。
可以使用@ExceptionHandler方法来实现上述功能。当在一个controller中使用了这个注解,,就可以处理这个controller里(或者它的子类)@RequestMapping方法产生的异常。你也可以在@ControllerAdvice类中声明一个@ExceptionHandler方法,这种情况下,可以处理多个controller下的@RequestMapping方法的异常。下面是在controller里使用@ExceptionHandler的例子:
@Controller
public class SimpleController{
//@RequestMapping具体方法略 ...
@ExceptionHandler(IOException.class)
publicResponseEntity<String> handleIOException(IOException ex) {
// 准备responseEntity
return responseEntity;
}
}
@ExceptionHandler的值可以被设为Exception类型的数组。如果抛出的异常匹配到这个列表中的其中一个,那么被标注为@ExceptionHandler的方法就会被调用。如果没有设置注解的值,那么异常类型将作为方法入参被使用。
与@RequestMapping注解的方法相似,@ExceptionHandler的方法入参和返回值都是很灵活的。比如,在Servlet环境中,HttpServletRequest可以被访问(PortletRequest同样可以在Portlet环境中访问)。返回的类型可以是代表了视图名的String、ModelAndView对象,ResponseEntity对象,或者,你可以添加@ResponseBody来使用message converter(消息转换器)来转换方法的返回值,然后写入到响应数据流中。
21.11.3 Spring MVC 标准异常的处理
当处理请求时,Spring MVC可能会抛出很多异常。SimpleMappingExceptionResolver可以轻松地把任何异常映射到一个所需的默认错误视图。然而,当需要运行于一些自动解析响应的客户端的时候,你可能会想要在响应里设置特定的状态码。根据异常里的状态码来代表客户端的4xx错误和服务器的5xx错误。
DefaultHandlerExceptionResolver(默认处理器异常解析器)可以把Spring MVC的异常翻译为特定的错误状态码。它默认使用MVC 命名空间、MVC Java配置来注册,同时,也可用DispatcherServlet注册(不用MVC命名空间或Java配置的时候)。下表是可被此解析器处理的异常和对应的响应状态码:
Exception(异常) | HTTP Status Code(HTTP状态码) |
BindException(绑定) | 400 (Bad Request) |
ConversionNotSupportedException(转换不被支持) | 500 (Internal Server Error) |
HttpMediaTypeNotAcceptableException(Http的媒体类型不被接受) | 406 (Not Acceptable) |
HttpMediaTypeNotSupportedException(Http的媒体类型不被支持) | 415 (Unsupported Media Type) |
HttpMessageNotReadableException(Http消息不可读) | 400 (Bad Request) |
HttpMessageNotWritableException(Http消息不可写) | 500 (Internal Server Error) |
HttpRequestMethodNotSupportedException(Http请求方法不被支持) | 405 (Method Not Allowed) |
MethodArgumentNotValidException(方法入参没被验证) | 400 (Bad Request) |
MissingServletRequestParameterException(缺失Servlet请求参数) | 400 (Bad Request) |
MissingServletRequestPartException(缺失Servlet请求部件) | 400 (Bad Request) |
NoHandlerFoundException(找不到Handler处理器) | 404 (Not Found) |
NoSuchRequestHandlingMethodException(没有此请求的处理方法) | 404 (Not Found) |
TypeMismatchException(类型不匹配) | 400 (Bad Request) |
MissingPathVariableException(路径变量缺失) | 500 (Internal Server Error) |
NoHandlerFoundException(未找到handler处理器) | 404 (Not Found) |
DefaultHandlerExceptionResolver设置响应的状态后,它的运作是透明的。但是当你的应用中,需要为每个错误的响应添加一些对开发友好的内容时(比如,提供REST API时),它会由于响应的主体中缺少错误内容而停止。你可以准备一个ModelAndView对象,然后通过视图解析来把错误内容进行渲染(比如:配置ContentNegotiatingViewResolver,MappingJackson2JsonView等等)。但是,你可能更想用@Exceptionhandler方法来替代之。
如果你偏好通过@ExceptionHandler方法来写入错误内容,你可以继承ResponseEntityExceptionHandler(响应实体异常处理器)来替代之。这种基于@ControllerAdvice类的方式,提供了@ExceptionHandler方法来处理标准的Spring MVC异常,然后返回一个ResponseEntity对象。这样,你就可以自定义响应,并且利用message conveters(消息转换器)来把错误内容写入到响应中。详见ResponseEntityExceptionHandler的javadoc文档。
21.11.4 使用@ResponseStatus来注解业务异常
业务的异常可以使用@ResponseStatus来注解。当异常发生时,ResponseStatusExceptionResolver(响应状态异常解析器)会根据异常相应地去设置相应的状态。默认情况下,是由DispatcherServlet注册ResponseStatusExceptionResolver来进行使用。
21.11.5 自定义Servlet容器默认的错误页面
当响应的状态被设置成一个错误状态码,并且相应的主体是空的,Servlet容器一般会渲染一个HTML格式的错误页面。为了自定义容器默认的错误页面,你可以在web.xml中声明<error-page>元素。在Servlet 3之前,这个元素必须映射一个特定状态码,或者异常类型。从Servlet 3开始,错误页面不需要被映射,这表示默认的Servlet容器错误页面可以被自定义为特定的地址了。
<error-page>
<location>/error</location>
</error-page>
请注意,错误页面实际的地址可以是一个JSP页面,或者是这个容器内的其他地址,包括某个通过@Controller方法来处理的方法。
当在写入错误信息时,在HttpServletResponse对象上设置的状态码和错误信息可以在controller里的request attributes(请求属性)被访问到:
@Controller
public class ErrorController {
@RequestMapping(path= "/error", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
public Map<String,Object> handle(HttpServletRequest request) {
Map<String, Object> map = newHashMap<String, Object>();
map.put("status",request.getAttribute("javax.servlet.error.status_code"));
map.put("reason",request.getAttribute("javax.servlet.error.message"));
return map;
}
}
或在一个JSP页面中:
<%@ page contentType="application/json" pageEncoding="UTF-8"%>
{
status:<%=request.getAttribute("javax.servlet.error.status_code") %>,
reason:<%=request.getAttribute("javax.servlet.error.message") %>
}