領先一步
VMware 提供培訓和認證,以加速您的進展。
了解更多注意: 2018 年 4 月修訂
Spring MVC 提供了幾種互補的例外處理方法,但我在教授 Spring MVC 時,經常發現我的學生對這些方法感到困惑或不熟悉。
今天我將向您展示各種可用的選項。我們的目標是在 Controller 方法中盡可能不要明確處理例外。它們是一種橫切關注點,最好在專用的程式碼中單獨處理。
有三個選項:按例外、按 Controller 或全域。
可在 http://github.com/paulc4/mvc-exceptions 找到一個展示此處討論要點的示範應用程式。 有關詳細資訊,請參閱下面的範例應用程式。
注意: 示範應用程式已更新 (2018 年 4 月) 以使用 Spring Boot 2.0.1,並且 (希望) 更容易使用和理解。 我還修復了一些損壞的連結 (感謝您的意見回饋,很抱歉花了一些時間)。
Spring Boot 允許使用最少的配置來設置 Spring 專案,如果您的應用程式不到幾年,您很可能正在使用它。
Spring MVC 沒有提供開箱即用的預設 (fallback) 錯誤頁面。 設置預設錯誤頁面最常見的方法一直是 SimpleMappingExceptionResolver
(實際上自 Spring V1 起)。 我們稍後將討論這個問題。
但是 Spring Boot 確實 提供了 fallback 錯誤處理頁面。
在啟動時,Spring Boot 嘗試尋找 /error
的映射。 按照慣例,以 /error
結尾的 URL 映射到相同名稱的邏輯檢視:error
。 在示範應用程式中,此檢視又映射到 error.html
Thymeleaf 模板。(如果使用 JSP,它將根據您的 InternalResourceViewResolver
的設置映射到 error.jsp
)。 實際映射將取決於您或 Spring Boot 設置的 ViewResolver
(如果有的話)。
如果找不到 /error
的檢視解析器映射,Spring Boot 會定義自己的 fallback 錯誤頁面 - 所謂的 "Whitelabel Error Page" (僅包含 HTTP 狀態資訊和任何錯誤詳細資訊 (例如來自未捕獲的例外的訊息) 的最小頁面)。 在範例應用程式中,如果您將 error.html
模板重新命名為 error2.html
,然後重新啟動,您會看到它被使用。
如果您發出 RESTful 請求 (HTTP 請求已指定 HTML 以外的所需回應類型),Spring Boot 會傳回 JSON 格式的相同錯誤資訊,該資訊會放入 "Whitelabel" 錯誤頁面中。
$> 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>
指令 (儘管實作方式非常不同)。 從 Servlet Filter 等 Spring MVC 框架外部擲出的例外,仍然會由 Spring Boot fallback 錯誤頁面報告。 範例應用程式也顯示了此範例。
更深入地討論 Spring Boot 錯誤處理可以在本文末尾找到。
本文的其餘部分適用於無論您是否使用帶有或不帶有 Spring Boot 的 Spring。.
沒有耐心的 REST 開發人員可能會選擇直接跳到關於自定義 REST 錯誤回應的章節。 但是,他們應該閱讀整篇文章,因為其中大部分內容同樣適用於所有 Web 應用程式,無論是否為 REST。
通常,處理 Web 請求時擲出的任何未處理的例外都會導致伺服器傳回 HTTP 500 回應。 但是,您自己編寫的任何例外都可以使用 @ResponseStatus
註釋進行註釋 (該註釋支援 HTTP 規範定義的所有 HTTP 狀態碼)。 當從 Controller 方法中擲出已註釋的例外,並且未在其他地方處理時,它將自動導致傳回具有指定狀態碼的適當 HTTP 回應。
例如,這是遺失訂單的例外。
@ResponseStatus(value=HttpStatus.NOT_FOUND, reason="No such Order") // 404
public class OrderNotFoundException extends RuntimeException {
// ...
}
這是使用它的 Controller 方法
@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
) 方法新增到任何 Controller,以專門處理同一 Controller 中請求處理 (@RequestMapping
) 方法擲出的例外。 此類方法可以
@ResponseStatus
註釋的例外 (通常是您未編寫的預定義例外)以下 Controller 示範了這三個選項
@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 允許您使用完全相同的例外處理技術,但將它們應用於整個應用程式,而不僅僅是個別的 Controller。 您可以將它們視為註釋驅動的攔截器。
任何使用 @ControllerAdvice
註釋的類別都會變成 Controller Advice,並且支援三種類型的方法
@ExceptionHandler
註釋的例外處理方法。@ModelAttribute
。 請注意,這些屬性無法用於例外處理檢視。
@InitBinder
.
我們僅會研究例外處理 - 在 線上手冊中搜尋有關 @ControllerAdvice
方法的更多資訊。
您在上面看到的任何例外處理常式都可以在 Controller Advice 類別上定義 - 但現在它們適用於從任何 Controller 擲出的例外。 這是一個簡單的範例
@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
的應用程式上下文中宣告的 Spring bean,只要實作了 HandlerExceptionResolver
,就會被用來攔截和處理 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
方法中的原因。
如果願意,您可以實作自己的 HandlerExceptionResolver
來設定您自己的自訂例外處理系統。處理器通常實作 Spring 的 Ordered
介面,因此您可以定義處理器的執行順序。
Spring 長期以來提供了一個簡單但方便的 HandlerExceptionResolver
實作,您可能會發現它已經在您的應用程式中使用 - SimpleMappingExceptionResolver
。 它提供了以下選項:
exception
屬性的名稱,以便可以在檢視中使用。(例如 JSP)。預設情況下,此屬性名為 exception
。 設為 null
以停用。請記住,從 @ExceptionHandler
方法傳回的檢視無法存取例外,但定義給 SimpleMappingExceptionResolver
的檢視可以存取例外。
以下是使用 Java Configuration 的典型配置
@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 Configuration
<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://127.0.0.1:8080。
在示範應用程式中,我也展示了如何建立「準備好支援」的錯誤頁面,並將堆疊追蹤隱藏在 HTML 原始碼中(作為註解)。 理想情況下,支援團隊應該從日誌中取得此資訊,但生活並不總是那麼理想。 無論如何,此頁面 *確實* 顯示了底層錯誤處理方法 handleError
如何建立自己的 ModelAndView
,以在錯誤頁面中提供額外資訊。 參見
ExceptionHandlingController.handleError()
:githubGlobalControllerExceptionHandler.handleError()
:githubSpring Boot 允許使用最少的配置來設定 Spring 專案。 當 Spring Boot 在類別路徑上偵測到某些關鍵類別和套件時,它會自動建立合理的預設值。 例如,如果它看到您正在使用 Servlet 環境,它將使用最常用的檢視解析器、處理常式對應等來設定 Spring MVC。 如果它看到 JSP 和/或 Thymeleaf,它將設定這些檢視技術。
Spring Boot 如何支援本文開頭描述的預設錯誤處理?
/error
。BasicErrorController
來處理任何對 /error
的請求。 該控制器會將錯誤資訊新增到內部模型,並傳回 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
屬性刻意設定為非 error
,而是 defaultErrorPage
,以便您可以區分是處理常式產生錯誤頁面,還是 Spring Boot 負責。 通常,兩者都會設定為 error
。
在 Spring Framework 之外拋出的例外,例如來自 servlet Filter 的例外,也會由 Spring Boot 的後備錯誤頁面報告。
為此,Spring Boot 必須為容器註冊預設錯誤頁面。 在 Servlet 2 中,有一個 <error-page>
指令,您可以將其新增到您的 web.xml
中來執行此操作。 可惜的是,Servlet 3 沒有提供等效的 Java API。 相反,Spring Boot 執行以下操作:
捕獲在後續階段引發的例外並處理它。