Spring MVC中的异常处理

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

注意:2018年4月修订

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

今天,我将向您展示可用的各种选项。我们的目标是尽可能不要在控制器方法中显式处理异常。它们是跨领域问题,最好在专用代码中单独处理。

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

可在 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" https://127.0.0.1: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 过滤器)仍然由 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 类

控制器建议允许您使用完全相同的异常处理技术,但将其应用于整个应用程序,而不仅仅是单个控制器。您可以将它们视为注释驱动的拦截器。

任何用@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系统中引发的任何未由控制器处理的异常。接口如下所示

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

SimpleMappingExceptionResolver

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

  • 将异常类名映射到视图名——只需指定类名,无需包。
  • 为任何其他地方未处理的异常指定默认(后备)错误页面。
  • 记录消息(默认情况下未启用)。
  • 设置要添加到模型中的exception属性的名称,以便可以在视图内使用它

(例如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方法添加到您的控制器。
  • 警告:小心不要在同一个应用程序中混合使用太多这些选项。如果同一个异常可以用多种方式处理,您可能无法获得想要的行为。控制器的@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将为https://127.0.0.1:8080

错误页面内容

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

  • ExceptionHandlingController.handleError(),位于github
  • GlobalControllerExceptionHandler.handleError(),位于github

Spring Boot和错误处理

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

回退错误页面

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

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

“Whitelabel”错误页面故意设计得很简陋。您可以覆盖它:

  1. 通过定义一个错误模板——在我们的演示中,我们使用Thymeleaf,因此错误模板位于src/main/resources/templates/error.html(此位置由Spring Boot属性spring.thymeleaf.prefix设置——其他支持的服务器端视图技术(如JSP或Mustache)也存在类似的属性)。
  2. 如果您没有使用服务器端渲染:1. 定义您自己的错误视图,作为名为error的bean。2. 或通过设置属性禁用Spring Boot的“Whitelabel”错误页面:

server.error.whitelabel.enabled设置为false。您的容器将使用默认错误页面。

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

SimpleMappingExceptionResolver集成

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

请注意,在演示中,SimpleMappingExceptionResolverdefaultErrorView属性故意设置为defaultErrorPage而不是error,这样您就可以看到处理程序何时生成错误页面以及Spring Boot何时负责。通常情况下,两者都应设置为error

容器范围异常处理

在Spring框架外部抛出的异常(例如,来自servlet过滤器的异常)也会由Spring Boot的回退错误页面报告。

为此,Spring Boot必须为容器注册一个默认错误页面。在Servlet 2中,有一个<error-page>指令,您可以将其添加到您的web.xml中来执行此操作。遗憾的是,Servlet 3没有提供等效的Java API。相反,Spring Boot执行以下操作:

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

捕获进一步发生的异常并进行处理。

获取Spring新闻通讯

与Spring新闻通讯保持联系

订阅

领先一步

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

了解更多

获取支持

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

了解更多

即将举行的活动

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

查看全部