Spring MVC 中的異常處理

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

注意: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 類

控制器建議允許您使用完全相同的異常處理技術,但將其應用於整個應用程式,而不僅僅是單個控制器。您可以將它們視為註解驅動的攔截器。

任何使用 @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 的檢視可以訪問。

(例如 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://: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 的任何請求。控制器將錯誤資訊新增到內部模型並返回 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 或透過設定屬性停用 Spring Boot 的“白標籤”錯誤頁面

server.error.whitelabel.enabledfalse。此時將使用容器的預設錯誤頁面。

按照慣例,Spring Boot 屬性通常在 application.propertiesapplication.yml 中設定。

SimpleMappingExceptionResolver 整合

如果您已經在使用 SimpleMappingExceptionResolver 來設定預設錯誤檢視怎麼辦?很簡單,使用 setDefaultErrorView() 來定義 Spring Boot 使用的相同檢視:error

請注意,在演示中,SimpleMappingExceptionResolverdefaultErrorView 屬性特意設定為 defaultErrorPage 而不是 error,這樣您就可以看到處理程式何時生成錯誤頁面,以及何時由 Spring Boot 負責。通常,兩者都會設定為 error

容器級異常處理

在 Spring 框架之外丟擲的異常,例如來自 servlet Filter 的異常,也由 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 社群所有即將舉行的活動。

檢視所有