领先一步
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" https://: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。结果如下所示。

控制器建议允许您使用完全相同的异常处理技术,但将其应用于整个应用程序,而不仅仅是单个控制器。您可以将它们视为注解驱动的拦截器。
任何使用 @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 系统中抛出且未由控制器处理的任何异常。接口如下所示
public interface HandlerExceptionResolver {
ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex);
}
handler 指的是生成异常的控制器(请记住,@Controller 实例只是 Spring MVC 支持的一种处理程序。例如:HttpInvokerExporter 和 WebFlow 执行器也是处理程序类型)。
在幕后,MVC 默认创建了三个这样的解析器。正是这些解析器实现了上面讨论的行为
ExceptionHandlerExceptionResolver 将未捕获的异常与处理程序(控制器)和任何控制器建议上的相应 @ExceptionHandler 方法进行匹配。ResponseStatusExceptionResolver 查找由 @ResponseStatus 注解的未捕获异常(如第 1 节所述)DefaultHandlerExceptionResolver 将标准 Spring 异常转换为 HTTP 状态码(我上面没有提到这一点,因为它属于 Spring MVC 内部)。这些按照列出的顺序进行链接和处理——Spring 内部创建了一个专用 bean(HandlerExceptionResolverComposite)来执行此操作。
请注意,resolveException 的方法签名不包含 Model。这就是为什么 @ExceptionHandler 方法不能注入模型的原因。
如果您愿意,可以实现自己的 HandlerExceptionResolver 来设置自己的自定义异常处理系统。处理程序通常实现 Spring 的 Ordered 接口,这样您就可以定义处理程序的运行顺序。
Spring 长期以来提供了一个简单但方便的 HandlerExceptionResolver 实现,您很可能已经在您的应用程序中使用了它——SimpleMappingExceptionResolver。它提供了以下选项:
exception 属性的名称,以便可以在视图(例如 JSP)中使用它。默认情况下,此属性名为 exception。设置为 null 可禁用。请记住,从 @ExceptionHandler 方法返回的视图无法访问异常,但定义给 SimpleMappingExceptionResolver 的视图可以访问。(例如 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:javamvn spring-boot:run您自己选择。主页 URL 将是 https://:8080。
在演示应用程序中,我还展示了如何创建一个“支持就绪”的错误页面,其中堆栈跟踪隐藏在 HTML 源代码中(作为注释)。理想情况下,支持团队应该从日志中获取这些信息,但生活并不总是理想的。无论如何,此页面确实展示了底层错误处理方法 handleError 如何创建自己的 ModelAndView 以在错误页面中提供额外信息。请参阅
ExceptionHandlingController.handleError()GlobalControllerExceptionHandler.handleError()Spring Boot 允许以最少的配置设置 Spring 项目。当 Spring Boot 在类路径上检测到某些关键类和包时,它会自动创建合理的默认值。例如,如果它发现您正在使用 Servlet 环境,它会设置 Spring MVC,并提供最常用的视图解析器、处理程序映射等。如果它发现 JSP 和/或 Thymeleaf,它会设置这些视图技术。
Spring Boot 如何支持本文开头描述的默认错误处理?
/error。BasicErrorController 来处理对 /error 的任何请求。控制器将错误信息添加到内部模型并返回 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 或通过设置属性禁用 Spring Boot 的“白标签”错误页面server.error.whitelabel.enabled 为 false。此时将使用容器的默认错误页面。
按照惯例,Spring Boot 属性通常在 application.properties 或 application.yml 中设置。
如果您已经在使用 SimpleMappingExceptionResolver 来设置默认错误视图怎么办?很简单,使用 setDefaultErrorView() 来定义 Spring Boot 使用的相同视图:error。
请注意,在演示中,SimpleMappingExceptionResolver 的 defaultErrorView 属性特意设置为 defaultErrorPage 而不是 error,这样您就可以看到处理程序何时生成错误页面,以及何时由 Spring Boot 负责。通常,两者都会设置为 error。
在 Spring 框架之外抛出的异常,例如来自 servlet Filter 的异常,也由 Spring Boot 的回退错误页面报告。
为此,Spring Boot 必须为容器注册一个默认错误页面。在 Servlet 2 中,您可以将 <error-page> 指令添加到 web.xml 中来完成此操作。遗憾的是,Servlet 3 不提供等效的 Java API。相反,Spring Boot 执行以下操作:
捕获下游抛出的异常并处理它。