超越自我
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。結果如下所示。
控制器通知(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;
}
}
在 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
介面,這樣您就可以定義處理程式的執行順序。
Spring 長期以來提供了一個簡單但方便的 HandlerExceptionResolver
實現,您很可能已經在您的應用程式中使用了它——即 SimpleMappingExceptionResolver
。它提供了以下選項:
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
是相當常見的:
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:java
mvn 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
的請求。該控制器將錯誤資訊新增到內部 Model 中,並返回 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 或者透過設定屬性server.error.whitelabel.enabled
為 false
來停用 Spring Boot 的“白標”錯誤頁面。此時將使用您的容器預設錯誤頁面。
按照慣例,Spring Boot 屬性通常在 application.properties
或 application.yml
中設定。
如果您已經使用 SimpleMappingExceptionResolver
來設定預設錯誤檢視怎麼辦?很簡單,使用 setDefaultErrorView()
來定義與 Spring Boot 使用的檢視相同的檢視:error
。
請注意,在演示中,SimpleMappingExceptionResolver
的 defaultErrorView
屬性特意設定為 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 會執行以下操作:
捕獲後續丟擲的異常並處理。