使用Http状态码
通常,处理Web请求时引发的任何未处理的异常都会导致服务器返回HTTP 500响应。但是,您自己编写的任何异常都可以使用@ResponseStatus注解(它支持HTTP规范定义的所有HTTP状态代码)。当从一个控制器方法抛出一个带注解的异常,而不在其他地方处理时,
它会自动导致相应的HTTP响应返回指定的状态码。
例如,这里有一个订单不存在的异常。
@ResponseStatus(value=HttpStatus.NOT_FOUND, reason="No such Order") // 404
public class OrderNotFoundException extends RuntimeException {
// ...
}
这里是抛出它的控制器方法:
@RequestMapping(value="/orders/{id}", method=GET)
public String showOrder(@PathVariable("id") long id, Model model) {
Order order = orderRepository.findOrderById(id);
if (order == null) throw new OrderNotFoundException(id);
model.addAttribute(order);
return "orderDetail";
}
如果此方法处理的URL包含未知的订单ID,则会返回HTTP 404响应。
基于Controller的异常处理
使用@ExceptionHandler
你能将@ExceptionHandler添加到任何控制器来处理由同一控制器中的请求处理@RequestMapping抛出的异常。
这些方法可以:
1.处理没有@ResponseStatus注解的异常(通常是你没有写的预定义异常)
2.将用户重定向到专用的error视图
3.构建一个完全自定义的error响应
下面的控制器演示了这三个选项:
@Controller
public class ExceptionHandlingController {
// @RequestHandler methods
...
// Exception handling methods
// Convert a predefined exception to an HTTP Status code
@ResponseStatus(value=HttpStatus.CONFLICT,
reason="Data integrity violation") // 409
@ExceptionHandler(DataIntegrityViolationException.class)
public void conflict() {
// Nothing to do
}
// Specify name of a specific view that will be used to display the error:
@ExceptionHandler({SQLException.class,DataAccessException.class})
public String databaseError() {
// Nothing to do. Returns the logical view name of an error page, passed
// to the view-resolver(s) in usual way.
// Note that the exception is NOT available to this view (it is not added
// to the model) but see "Extending ExceptionHandlerExceptionResolver"
// below.
return "databaseError";
}
// Total control - setup a model and return the view name yourself. Or
// consider subclassing ExceptionHandlerExceptionResolver (see below).
@ExceptionHandler(Exception.class)
public ModelAndView handleError(HttpServletRequest req, Exception ex) {
logger.error("Request: " + req.getRequestURL() + " raised " + ex);
ModelAndView mav = new ModelAndView();
mav.addObject("exception", ex);
mav.addObject("url", req.getRequestURL());
mav.setViewName("error");
return mav;
}
}
在这些方法中,您都可以选择进行额外的处理-最常见的例子是打印异常到日志文件。
你可以传入servlet相关的对象到异常处理的方法,例如HttpServletRequest,HttpServletResponse,HttpSession ,Principle。
重要提示:Model可能不是任意@ExceptionHandler方法的参数。相反,使用ModelAndView在方法内部设置模型,如上面的handleError()所示。
异常和视图
向模型添加异常时要小心。用户不希望看到包含Java异常详细信息和堆栈跟踪的Web页面。但是,将页面中的异常详细信息作为注释来发布可能很有用。如果使用JSP,你可以做这样来输出异常和相应的堆栈跟踪(使用隐藏的<div>是另一种方法)。
<h1>Error Page</h1>
<p>Application has encountered an error. Please contact support on ...</p>
<!--
Failed URL: ${url}
Exception: ${exception.message}
<c:forEach items="${exception.stackTrace}" var="ste"> ${ste}
</c:forEach>
-->
使用Thymeleaf模板可以参照demo中的support.html页面 。结果看起来像这样。
全局异常处理
使用@ControllerAdvice
@ControllerAdvice允许你在整个应该用程序中使用相同的异常处理技术,而不仅仅是一个单独的控制器。你可以将它们看成注解驱动的拦截器。
任何使用@ControllerAdvice注解的类都将成为controller-advice,并支持三种类型的方法:
- 用@ExceptionHandler注释的异常处理方法。
- 使用@ModelAttribute注解的模型增强方法(用于向模型添加其他数据)。请注意,这些属性不适用于异常处理视图。
- 使用@InitBinder注解的绑定初始化方法(用于配置表单处理)。
上面看到的任何异常处理程序都可以在 controller-advice类中定义-但现在它们适用于从任何控制器抛出的异常。这有一个简单的例子:
@ControllerAdvice
class GlobalControllerExceptionHandler {
@ResponseStatus(HttpStatus.CONFLICT) // 409
@ExceptionHandler(DataIntegrityViolationException.class)
public void handleConflict() {
// Nothing to do
}
}
如果你想有处理任何异常的默认方法。你需要确保被注解的异常都能被这个框架处理。代码看起来像这样:
@ControllerAdvice
class GlobalDefaultExceptionHandler {
public static final String DEFAULT_ERROR_VIEW = "error";
@ExceptionHandler(value = Exception.class)
public ModelAndView
defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception {
// If the exception is annotated with @ResponseStatus rethrow it and let
// the framework handle it - like the OrderNotFoundException example
// at the start of this post.
// AnnotationUtils is a Spring Framework utility class.
if (AnnotationUtils.findAnnotation
(e.getClass(), ResponseStatus.class) != null)
throw e;
// Otherwise setup and send the user to a default error-view.
ModelAndView mav = new ModelAndView();
mav.addObject("exception", e);
mav.addObject("url", req.getRequestURL());
mav.setViewName(DEFAULT_ERROR_VIEW);
return mav;
}
}
深入
HandlerExceptionResolver
在DispatcherServlet的应用程序上下文中声明的任何实现HandlerExceptionResolver的Spring bean将被用于截获和处理在MVC系统中引发并且不由Controller处理的任何异常。接口看起来像这样:
public interface HandlerExceptionResolver {
ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex);
}
handler引用了产生异常的controller(请记住,@Controller实例只是Spring MVC支持的一种类型的处理程序。例如:HttpInvokerExporter和WebFlow执行程序也是类型的处理程序)。
在幕后,MVC默认创建三个这样的解析器。正是这些解析器实现了上面讨论的行为。
- ExceptionHandlerExceptionResolver匹配未被@ExceptionHandler注解捕获的异常
- ResponseStatusExceptionResolver查找由@ResponseStatus注解的未捕获的异常(如第1节中所述)
- DefaultHandlerExceptionResolver转换标准的Spring异常并转换它们 到HTTP状态代码(我没有提到过,因为它在Spring MVC的内部)
它们被串联起来,在 顺序排列的列表中进行处理(Spring内部创建一个专用的HandlerExceptionResolverComposite来做这件事)。
请注意,resolveException的方法签名不包含模型。这就是为什么@ExceptionHandler方法不能与模型一起注入的原因。
如果你愿意,你可以实现自己的HandlerExceptionResolver来设置你自己的自定义异常处理系统。处理程序通常实现Spring的Ordered接口,因此你可以定义处理程序运行的顺序。
SimpleMappingExceptionResolver
Spring很早就提供了一个简单但方便的HandlerExceptionResolver实现,你可能已经发现它已经在你的应用程序中被使用 - SimpleMappingExceptionResolver。它提供了以下选项:
- 将异常类名映射到视图名称 - 只需指定类名,不需要包。
- 为任何其他地方未处理的异常指定默认(fallback)错误页面。
- 打印日志(默认情况不启用)。
- 设置要添加到模型的异常属性的名称,所以它可以在View中使用 (如JSP)。默认情况下,该属性被命名为exception。设置为null以禁用。请记住,从@ExceptionHandler方法返回的视图无法访问该异常,但定义到SimpleMappingExceptionResolver的视图可以执行此操作。
这是使用XML的典型配置:
<bean id="simpleMappingExceptionResolver" class=
"org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<map>
<entry key="DatabaseException" value="databaseError"/>
<entry key="InvalidCreditCardException" value="creditCardError"/>
</map>
</property>
<!-- See note below on how this interacts with Spring Boot -->
<property name="defaultErrorView" value="error"/>
<property name="exceptionAttribute" value="ex"/>
<!-- Name of logger to use to log exceptions. Unset by default,
so logging is disabled unless you set a value. -->
<property name="warnLogCategory" value="example.MvcLogger"/>
</bean>
或者使用Java配置:
@Configuration
@EnableWebMvc // Optionally setup Spring MVC defaults (if you aren't using
// Spring Boot & haven't specified @EnableWebMvc elsewhere)
public class MvcConfiguration extends WebMvcConfigurerAdapter {
@Bean(name="simpleMappingExceptionResolver")
public SimpleMappingExceptionResolver
createSimpleMappingExceptionResolver() {
SimpleMappingExceptionResolver r =
new SimpleMappingExceptionResolver();
Properties mappings = new Properties();
mappings.setProperty("DatabaseException", "databaseError");
mappings.setProperty("InvalidCreditCardException", "creditCardError");
r.setExceptionMappings(mappings); // None by default
r.setDefaultErrorView("error"); // No default
r.setExceptionAttribute("ex"); // Default is "exception"
r.setWarnLogCategory("example.MvcLogger"); // No default
return r;
}
...
}
defaultErrorView属性特别有用,因为它确保任何未捕获的异常生成一个合适的应用程序定义的错误页面。(大多数应用程序服务器的默认设置是显示Java堆栈跟踪 - 这是用户永远不会看到的)。
扩展SimpleMappingExceptionResolver
扩展SimpleMappingExceptionResolver一般是由于一下几个原因:
- 使用构造函数直接设置属性 - 例如启用异常记录并设置logger使用
- 通过重写buildLogMessage来重写默认为log消息。默认实现总是返回这个固定文本:Handler execution resulted in exception
- 重写doResolveException为错误视图提供更多信息
例如
public class MyMappingExceptionResolver extends SimpleMappingExceptionResolver {
public MyMappingExceptionResolver() {
// Enable logging by providing the name of the logger to use
setWarnLogCategory(MyMappingExceptionResolver.class.getName());
}
@Override
public String buildLogMessage(Exception e, HttpServletRequest req) {
return "MVC exception: " + e.getLocalizedMessage();
}
@Override
protected ModelAndView doResolveException(HttpServletRequest req,
HttpServletResponse resp, Object handler, Exception ex) {
// Call super method to get the ModelAndView
ModelAndView mav = super.doResolveException(req, resp, handler, ex);
// Make the full URL available to the view - note ModelAndView uses
// addObject() but Model uses addAttribute(). They work the same.
mav.addObject("url", request.getRequestURL());
return mav;
}
}
这些代码在demo应用的ExampleSimpleMappingExceptionResolver。
扩展ExceptionHandlerExceptionResolver
重写它的doResolveHandlerMethodException方法来扩展ExceptionHandlerExceptionResolver 。它具有几乎相同的签名(它仅仅使用HandlerMethod 替换Handler)。
为了确保它能用,设置order属性的值(在新类的构造函数中)小于MAX_INT让它能在默认的ExceptionHandlerExceptionResolver实例之前运行(创建自己的处理程序实例比试图修改/替换Spring创建的更容易)。有关更多信息,请参阅演示应用程序中的ExampleExceptionHandlerExceptionResolver。
Errors and REST
RESTful GET请求也可能会产生异常,我们已经看到如何返回标准的HTTP错误响应代码。但是,如果要返回有关错误的信息,该怎么办?这很容易做到。首先定义一个错误类:
public class ErrorInfo {
public final String url;
public final String ex;
public ErrorInfo(String url, Exception ex) {
this.url = url;
this.ex = ex.getLocalizedMessage();
}
}
现在你能返回一个这样的实例:
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MyBadDataException.class)
@ResponseBody ErrorInfo
handleBadRequest(HttpServletRequest req, Exception ex) {
return new ErrorInfo(req.getRequestURL(), ex);
}
什么时候使用?
像往常一样,Spring喜欢给你提供选择,所以你应该怎么做?这里有一些经验。如果您对XML配置或注解有偏好,这也没关系。
- 对于你写的异常,考虑给他们添加@ResponseStatus。
- 对于@ControllerAdvice类上实现@ExceptionHandler方法或者使用SimpleMappingExceptionResolver的其他所有异常。你可能已经为你的应用程序配置了SimpleMappingExceptionResolver,在这种情况下,向它添加新的异常类比实现@ControllerAdvice更容易。
- 对于控制器特定的异常处理,将@ExceptionHandler方法添加到您的控制器。
- 警告:在同一个应用程序中,要小心混合太多这些选项。如果同一个异常被处理多次,你可能无法得到期望的结果。控制器上的@ExceptionHandler方法始终在任何@ControllerAdvice实例上的那些方法之前选中。未定义处理controller-advices的顺序。
示例程序
演示应用程序可以在 github上找到。它使用Spring Boot和Thymeleaf构建了一个简单的Web应用程序。