超越自我
VMware 提供培训和认证,助您加速进步。
了解更多注意: 2018 年 4 月修订
Spring MVC 提供了几种相辅相成的异常处理方法,但在教授 Spring MVC 时,我经常发现我的学生对此感到困惑或不适应。
今天我将向您展示各种可用选项。我们的目标是尽可能 不 在 Controller 方法中显式处理异常。它们是一种跨领域关注点,最好在专门的代码中单独处理。
有三种选项:按异常、按控制器或全局。
演示本文讨论要点的应用程序可在 http://github.com/paulc4/mvc-exceptions 找到。有关详细信息,请参阅下方的示例应用程序。
注意: *演示应用程序已进行改进和更新(2018 年 4 月),使用 Spring Boot 2.0.1,并且(希望)更容易使用和理解。我还修复了一些损坏的链接(感谢反馈,抱歉花了一些时间)。*
Spring Boot 允许以最少的配置来设置 Spring 项目,如果您的应用程序创建时间不超过几年,您很可能正在使用它。
Spring MVC 开箱即用不提供默认(备用)错误页面。设置默认错误页面的最常见方法一直是 SimpleMappingExceptionResolver
(事实上自 Spring V1 起就是如此)。我们稍后会讨论这一点。
然而,Spring Boot 确实 提供了一个备用错误处理页面。
在启动时,Spring Boot 会尝试查找 /error
的映射。按照约定,以 /error
结尾的 URL 映射到同名的逻辑视图:error
。在演示应用程序中,这个视图反过来映射到 error.html
Thymeleaf 模板。(如果使用 JSP,它会根据您的 InternalResourceViewResolver
设置映射到 error.jsp
)。实际映射取决于您或 Spring Boot 设置的任何 ViewResolver
。
如果找不到 /error
的视图解析器映射,Spring Boot 会定义自己的备用错误页面——即所谓的“白标签错误页面”(一个最小页面,只包含 HTTP 状态信息和任何错误详情,例如未捕获异常的消息)。在示例应用程序中,如果您将 error.html
模板重命名为(例如)error2.html
然后重启,您将看到它被使用。
如果您发出 RESTful 请求(HTTP 请求指定了除 HTML 之外的期望响应类型),Spring Boot 将返回与“白标签”错误页面中相同错误信息的 JSON 表示。
$> curl -H "Accept: application/json" http://localhost:8080/no-such-page
{"timestamp":"2018-04-11T05:56:03.845+0000","status":404,"error":"Not Found","message":"No message available","path":"/no-such-page"}
Spring Boot 还为容器设置了一个默认错误页面,这相当于 web.xml
中的 <error-page>
指令(尽管实现方式非常不同)。在 Spring MVC 框架之外抛出的异常,例如来自 servlet Filter 的异常,仍然会由 Spring Boot 备用错误页面报告。示例应用程序也展示了这一点。
本文末尾对 Spring Boot 的错误处理有更深入的讨论。
本文其余部分适用于使用或不使用 Spring Boot 的 Spring 应用程序。.
不耐烦的 REST 开发人员可能会选择直接跳到有关自定义 REST 错误响应的部分。但是,他们随后应该阅读全文,因为大部分内容同样适用于所有 Web 应用程序,无论是 REST 还是其他类型。
通常,处理 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 响应。
您可以向任何控制器添加额外的(@ExceptionHandler
)方法,以专门处理同一控制器中请求处理(@RequestMapping
)方法抛出的异常。此类方法可以
@ResponseStatus
注解的异常(通常是您未编写的预定义异常)以下控制器展示了这三个选项
@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 异常详情和堆栈跟踪的网页。您可能有一些安全策略,明确 禁止 在错误页面中放入 任何 异常信息。这是确保您覆盖 Spring Boot 白标签错误页面的另一个原因。
确保异常被有效记录,以便您的支持和开发团队在事件发生后进行分析。
请记住,以下做法可能很方便,但在生产环境中 并非 最佳实践.
在页面 源代码 中将异常详情隐藏为注释可能很有用,以帮助进行 测试。如果使用 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 的等效用法,请参阅演示应用程序中的 support.html。结果如下所示。
控制器通知(Controller Advice)允许您使用完全相同的异常处理技术,但将其应用于整个应用程序,而不仅仅是单个控制器。您可以将它们视为注解驱动的拦截器。
任何使用 @ControllerAdvice
注解的类都会成为一个控制器通知,并支持三种类型的方法
@ExceptionHandler
注解的异常处理方法。@ModelAttribute
。请注意,这些属性 不 对异常处理视图可用。
@InitBinder
.
我们只关注异常处理——有关 @ControllerAdvice
方法的更多信息,请查阅在线手册。
您在上面看到的任何异常处理器都可以在控制器通知类上定义——但现在它们适用于从 任何 控制器抛出的异常。这里有一个简单的例子
@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;
}
}
在 DispatcherServlet
的应用程序上下文中声明的任何实现 HandlerExceptionResolver
的 Spring Bean 都将用于拦截和处理 MVC 系统中引发但未由 Controller 处理的任何异常。接口如下所示
public interface HandlerExceptionResolver {
ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex);
}
handler
指的是生成异常的控制器(请记住 @Controller
实例只是 Spring MVC 支持的一种处理程序类型。例如:HttpInvokerExporter
和 WebFlow Executor 也属于处理程序类型)。
在幕后,MVC 默认创建了三个这样的解析器。正是这些解析器实现了上面讨论的行为
ExceptionHandlerExceptionResolver
将未捕获的异常与处理程序(控制器)和任何控制器通知上合适的 @ExceptionHandler
方法进行匹配。ResponseStatusExceptionResolver
查找由 @ResponseStatus
注解的未捕获异常(如第 1 节所述)DefaultHandlerExceptionResolver
转换标准 Spring 异常并将其转换为 HTTP 状态码(我在上面没有提到这一点,因为它属于 Spring MVC 内部)。这些解析器按列出的顺序链式处理——在内部,Spring 会创建一个专门的 Bean (HandlerExceptionResolverComposite
) 来完成这项工作。
请注意,resolveException
方法签名不包含 Model
。这就是为什么 @ExceptionHandler
方法不能注入 model 的原因。
如果需要,您可以实现自己的 HandlerExceptionResolver
来设置自己的自定义异常处理系统。处理程序通常实现 Spring 的 Ordered
接口,这样您就可以定义处理程序的运行顺序。
Spring 长期以来提供了一个简单但方便的 HandlerExceptionResolver
实现,您很可能已经在您的应用程序中使用了它——即 SimpleMappingExceptionResolver
。它提供了以下选项:
exception
属性的名称,以便在 View 中使用它(例如 JSP)。默认情况下,此属性名为 exception
。设置为 null
则禁用。请记住,从 @ExceptionHandler
方法返回的视图 无法 访问异常,但为 SimpleMappingExceptionResolver
定义的视图 可以。
这是一个使用 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;
}
...
}
或使用 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>
defaultErrorView 属性特别有用,因为它确保任何未捕获的异常都会生成一个合适的应用程序定义的错误页面。(大多数应用服务器的默认行为是显示 Java 堆栈跟踪——这是您的用户 绝不应该 看到的东西)。Spring Boot 通过其“白标签”错误页面提供了另一种实现相同功能的方式。
出于多种原因,扩展 SimpleMappingExceptionResolver
是相当常见的:
buildLogMessage
来覆盖默认日志消息。默认实现总是返回这个固定文本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;
}
}
此代码可在演示应用程序中找到,文件为 ExampleSimpleMappingExceptionResolver
您也可以通过相同的方式扩展 ExceptionHandlerExceptionResolver
并重写其 doResolveHandlerMethodException
方法。它的签名几乎相同(只是接受新的 HandlerMethod
而不是 Handler
)。
为了确保它被使用,还需要设置继承的 order 属性(例如在您新类的构造函数中)为一个小于 MAX_INT
的值,这样它会在默认的 ExceptionHandlerExceptionResolver 实例 之前 运行(创建自己的处理器实例比尝试修改/替换 Spring 创建的更容易)。更多信息请参阅演示应用程序中的 ExampleExceptionHandlerExceptionResolver。
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();
}
}
现在我们可以像这样从处理程序中返回一个实例作为 @ResponseBody
@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
实例上的方法。控制器通知的处理顺序是 未定义的。一个演示应用程序可在 github 找到。它使用 Spring Boot 和 Thymeleaf 构建了一个简单的 Web 应用程序。
该应用程序已经过两次修订(2014 年 10 月,2018 年 4 月),并且(希望)变得更好且更容易理解。基本原理保持不变。它使用 Spring Boot V2.0.1 和 Spring V5.0.5,但代码也适用于 Spring 3.x 和 4.x。
该演示正在 Cloud Foundry 上运行,地址是 http://mvc-exceptions-v2.cfapps.io/。
该应用程序引导用户通过 5 个演示页面,展示不同的异常处理技术
@ExceptionHandler
方法来处理自身异常的控制器SimpleMappingExceptionResolver
处理异常SimpleMappingExceptionResolver
应用程序中最重要的文件及其与每个演示的关系可以在项目的 README.md 中找到。
主页是 index.html,它
每个演示页面都包含多个链接,所有链接都故意抛出异常。每次您都需要使用浏览器的后退按钮返回演示页面。
得益于 Spring Boot,您可以将此演示作为一个 Java 应用程序运行(它运行一个嵌入式 Tomcat 容器)。要运行该应用程序,您可以使用以下任一命令(第二个命令得益于 Spring Boot maven 插件)
mvn exec:java
mvn spring-boot:run
随您选择。主页 URL 将是 http://localhost:8080。
此外,在演示应用程序中,我展示了如何创建一个“支持就绪”的错误页面,其中堆栈跟踪隐藏在 HTML 源代码中(作为注释)。理想情况下,支持人员应该从日志中获取此信息,但现实并不总是那么理想。无论如何,这个页面 确实 展示了底层错误处理方法 handleError
如何创建自己的 ModelAndView
以在错误页面中提供额外信息。请参阅
ExceptionHandlingController.handleError()
GlobalControllerExceptionHandler.handleError()
Spring Boot 允许以最少的配置来设置 Spring 项目。当它检测到类路径上的某些关键类和包时,Spring Boot 会自动创建合理的默认配置。例如,如果它检测到您正在使用 Servlet 环境,它会设置 Spring MVC,包含最常用的视图解析器、处理程序映射等。如果它检测到 JSP 和/或 Thymeleaf,它会设置这些视图技术。
Spring Boot 如何支持本文开头描述的默认错误处理?
/error
。BasicErrorController
来处理任何对 /error
的请求。该控制器将错误信息添加到内部 Model 中,并返回 error
作为逻辑视图名。View
对象提供默认错误页面(使其独立于您可能使用的任何视图解析系统)。BeanNameViewResolver
,以便可以将 /error
映射到同名的 View
。ErrorMvcAutoConfiguration
类,您将看到 defaultErrorView
作为名为 error
的 bean 返回。这是 BeanNameViewResolver
找到的 View bean。“白标签”错误页面故意做得非常简洁和朴素。您可以覆盖它
src/main/resources/templates/error.html
(此位置由 Spring Boot 属性 spring.thymeleaf.prefix
设置 - 对于其他支持的服务器端视图技术,如 JSP 或 Mustache,也存在类似的属性)。error
的 Bean。2.1 或者通过设置属性server.error.whitelabel.enabled
为 false
来禁用 Spring Boot 的“白标”错误页面。此时将使用您的容器默认错误页面。
按照惯例,Spring Boot 属性通常在 application.properties
或 application.yml
中设置。
如果您已经使用 SimpleMappingExceptionResolver
来设置默认错误视图怎么办?很简单,使用 setDefaultErrorView()
来定义与 Spring Boot 使用的视图相同的视图:error
。
请注意,在演示中,SimpleMappingExceptionResolver
的 defaultErrorView
属性特意设置为 defaultErrorPage
而不是 error
,这样您就可以看到何时是处理器生成错误页面以及何时是 Spring Boot 负责。通常两者都会设置为 error
。
在 Spring Framework 之外抛出的异常,例如来自 servlet Filter 的异常,也会由 Spring Boot 的备用错误页面报告。
为此,Spring Boot 必须为容器注册一个默认错误页面。在 Servlet 2 中,有一个 <error-page>
指令可以添加到 web.xml
中来完成此操作。遗憾的是,Servlet 3 不提供等效的 Java API。相反,Spring Boot 会执行以下操作:
捕获后续抛出的异常并处理。