Spring MVC 中的异常处理

工程 | Paul Chapman | 2013 年 11 月 01 日 | ...

注意: 2018 年 4 月修订

Spring MVC 提供了几种相辅相成的异常处理方法,但在教授 Spring MVC 时,我经常发现我的学生对此感到困惑或不适应。

今天我将向您展示各种可用选项。我们的目标是尽可能 在 Controller 方法中显式处理异常。它们是一种跨领域关注点,最好在专门的代码中单独处理。

有三种选项:按异常、按控制器或全局。

演示本文讨论要点的应用程序可在 http://github.com/paulc4/mvc-exceptions 找到。有关详细信息,请参阅下方的示例应用程序

注意: *演示应用程序已进行改进和更新(2018 年 4 月),使用 Spring Boot 2.0.1,并且(希望)更容易使用和理解。我还修复了一些损坏的链接(感谢反馈,抱歉花了一些时间)。*

Spring Boot

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 还是其他类型。

使用 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 响应。

基于控制器的异常处理

使用 @ExceptionHandler

您可以向任何控制器添加额外的(@ExceptionHandler)方法,以专门处理同一控制器中请求处理(@RequestMapping)方法抛出的异常。此类方法可以

  1. 处理没有 @ResponseStatus 注解的异常(通常是您未编写的预定义异常)
  2. 将用户重定向到专门的错误视图
  3. 构建完全自定义的错误响应

以下控制器展示了这三个选项

@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 相关对象,例如 HttpServletRequestHttpServletResponseHttpSession 和/或 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。结果如下所示。

Example of an error page with a hidden exception for support

全局异常处理

使用 @ControllerAdvice 类

控制器通知(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;
  }
}

深入探讨

HandlerExceptionResolver

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 接口,这样您就可以定义处理程序的运行顺序。

SimpleMappingExceptionResolver

Spring 长期以来提供了一个简单但方便的 HandlerExceptionResolver 实现,您很可能已经在您的应用程序中使用了它——即 SimpleMappingExceptionResolver。它提供了以下选项:

  • 将异常类名映射到视图名——只需指定类名,无需包名。
  • 为任何未在其他地方处理的异常指定默认(备用)错误页面
  • 记录一条消息(此功能默认未启用)。
  • 设置要添加到 Model 中的 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

出于多种原因,扩展 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

您也可以通过相同的方式扩展 ExceptionHandlerExceptionResolver 并重写其 doResolveHandlerMethodException 方法。它的签名几乎相同(只是接受新的 HandlerMethod 而不是 Handler)。

为了确保它被使用,还需要设置继承的 order 属性(例如在您新类的构造函数中)为一个小于 MAX_INT 的值,这样它会在默认的 ExceptionHandlerExceptionResolver 实例 之前 运行(创建自己的处理器实例比尝试修改/替换 Spring 创建的更容易)。更多信息请参阅演示应用程序中的 ExampleExceptionHandlerExceptionResolver

错误与 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();
    }
}

现在我们可以像这样从处理程序中返回一个实例作为 @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 方法添加到您的控制器中。
  • 警告: 在同一个应用程序中混合使用过多的这些选项时要小心。如果同一个异常可以通过多种方式处理,您可能无法获得期望的行为。Controller 上的 @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 个演示页面,展示不同的异常处理技术

  1. 一个带有 @ExceptionHandler 方法来处理自身异常的控制器
  2. 一个抛出异常由全局 ControllerAdvice 处理的控制器
  3. 使用 SimpleMappingExceptionResolver 处理异常
  4. 与演示 3 相同,但为作比较而禁用了 SimpleMappingExceptionResolver
  5. 展示了 Spring Boot 如何生成其错误页面

应用程序中最重要的文件及其与每个演示的关系可以在项目的 README.md 中找到。

主页是 index.html,它

  • 链接到每个演示页面
  • 链接(页面底部)指向 Spring Boot 端点,供对 Spring Boot 感兴趣的人参考。

每个演示页面都包含多个链接,所有链接都故意抛出异常。每次您都需要使用浏览器的后退按钮返回演示页面。

得益于 Spring Boot,您可以将此演示作为一个 Java 应用程序运行(它运行一个嵌入式 Tomcat 容器)。要运行该应用程序,您可以使用以下任一命令(第二个命令得益于 Spring Boot maven 插件)

  • mvn exec:java
  • mvn spring-boot:run

随您选择。主页 URL 将是 http://localhost:8080

错误页面内容

此外,在演示应用程序中,我展示了如何创建一个“支持就绪”的错误页面,其中堆栈跟踪隐藏在 HTML 源代码中(作为注释)。理想情况下,支持人员应该从日志中获取此信息,但现实并不总是那么理想。无论如何,这个页面 确实 展示了底层错误处理方法 handleError 如何创建自己的 ModelAndView 以在错误页面中提供额外信息。请参阅

  • github 上的 ExceptionHandlingController.handleError()
  • github 上的 GlobalControllerExceptionHandler.handleError()

Spring Boot 和错误处理

Spring Boot 允许以最少的配置来设置 Spring 项目。当它检测到类路径上的某些关键类和包时,Spring Boot 会自动创建合理的默认配置。例如,如果它检测到您正在使用 Servlet 环境,它会设置 Spring MVC,包含最常用的视图解析器、处理程序映射等。如果它检测到 JSP 和/或 Thymeleaf,它会设置这些视图技术。

备用错误页面

Spring Boot 如何支持本文开头描述的默认错误处理?

  1. 发生任何未处理的错误时,Spring Boot 会内部转发到 /error
  2. Boot 设置了一个 BasicErrorController 来处理任何对 /error 的请求。该控制器将错误信息添加到内部 Model 中,并返回 error 作为逻辑视图名。
  3. 如果配置了任何视图解析器,它们将尝试使用相应的错误视图。
  4. 否则,将使用专门的 View 对象提供默认错误页面(使其独立于您可能使用的任何视图解析系统)。
  5. Spring Boot 设置了一个 BeanNameViewResolver,以便可以将 /error 映射到同名的 View
  6. 如果您查看 Boot 的 ErrorMvcAutoConfiguration 类,您将看到 defaultErrorView 作为名为 error 的 bean 返回。这是 BeanNameViewResolver 找到的 View bean。

“白标签”错误页面故意做得非常简洁和朴素。您可以覆盖它

  1. 通过定义一个错误模板 - 在我们的演示中,我们使用 Thymeleaf,因此错误模板位于 src/main/resources/templates/error.html(此位置由 Spring Boot 属性 spring.thymeleaf.prefix 设置 - 对于其他支持的服务器端视图技术,如 JSP 或 Mustache,也存在类似的属性)。
  2. 如果您使用服务器端渲染,则可以:2.1 将您自己的错误视图定义为一个名为 error 的 Bean。2.1 或者通过设置属性

server.error.whitelabel.enabledfalse 来禁用 Spring Boot 的“白标”错误页面。此时将使用您的容器默认错误页面。

按照惯例,Spring Boot 属性通常在 application.propertiesapplication.yml 中设置。

SimpleMappingExceptionResolver 集成

如果您已经使用 SimpleMappingExceptionResolver 来设置默认错误视图怎么办?很简单,使用 setDefaultErrorView() 来定义与 Spring Boot 使用的视图相同的视图:error

请注意,在演示中,SimpleMappingExceptionResolverdefaultErrorView 属性特意设置为 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 会执行以下操作:

  • 对于带有嵌入式容器的 Jar 应用,它使用特定于容器的 API 注册默认错误页面。
  • 对于作为传统 WAR 文件部署的 Spring Boot 应用,使用 Servlet Filter 来

捕获后续抛出的异常并处理。

获取 Spring 时事通讯

订阅 Spring 时事通讯,保持联系

订阅

超越自我

VMware 提供培训和认证,助您加速进步。

了解更多

获得支持

Tanzu Spring 通过一个简单的订阅,提供对 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举行的活动

查看 Spring 社区中所有即将举行的活动。

查看全部