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" 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 還是其他型別。

使用 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 將是 https://: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 社群中所有即將舉行的活動。

檢視全部