1. 概述
本教程將演示如何使用 Spring 實現 REST API 的異常處理。我們將學習到這方面存在多種可能性。所有這些方法都具有一個共同點:它們非常有效地處理了 分離關注點。應用程序可以正常地拋出異常以指示某種失敗,然後這些異常會被單獨處理。
2.
我們可以使用 來註解那些 Spring 自動調用的方法,當給定異常發生時。我們可以通過註解或將其作為方法參數聲明異常,從而從異常對象中讀取信息以正確處理它。方法本身作為 Controller 方法進行處理,因此:
- 它可以返回渲染到響應體中的對象,或完整的 ResponseEntity。這裏允許內容協商 自 Spring 6.2 起。
- 它可以返回 ProblemDetail 對象。Spring 會自動設置 Content-Type 標題為 “application/problem+json“。
- 我們可以使用 @ResponseStatus 指定返回值代碼。
返回 400 狀態碼的最簡單異常處理程序可能是:
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(CustomException1.class)
public void handleException1() { }我們還可以將處理的異常聲明為方法參數,例如,用於讀取異常詳情並創建符合 RFC-9457 規範 的問題詳情對象:
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler
public ProblemDetail handleException2(CustomException2 ex) {
// ...
}自 Spring 6.2 版本起,我們可以為不同內容類型編寫不同的異常處理器:
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler( produces = MediaType.APPLICATION_JSON_VALUE )
public CustomExceptionObject handleException3Json(CustomException3 ex) {
// ...
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler( produces = MediaType.TEXT_PLAIN_VALUE )
public String handleException3Text(CustomException3 ex) {
// ...
}我們還可以為不同類型的異常編寫異常處理程序。如果處理程序方法中需要詳細信息,我們使用所有異常類型的公共父類作為方法參數:
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler({
CustomException4.class,
CustomException5.class
})
public ResponseEntity<CustomExceptionObject> handleException45(Exception ex) {
// ...
}2.1. 本地異常處理(控制器級別)
我們可以將這些處理方法放在控制器類中:
@RestController
public class FooController {
//...
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(CustomException1.class)
public void handleException() {
// ...
}
}我們可以在需要控制器特定異常處理時使用這種方法。但是,這種方法的一個缺點是 除非將其放入基類並使用繼承,否則我們無法在多個控制器中使用它。
2.2 全局異常處理
<em @ControllerAdvice</em> 包含在多個控制器之間共享的代碼。它是一種特殊的 Spring 組件。尤其是在 REST API 中,每個方法的返回值都應渲染到響應體中,因此有 <em @RestControllerAdvice</em>。
因此,為了為應用程序的所有控制器處理特定異常,我們可以編寫一個簡單的類:
@RestControllerAdvice
public class MyGlobalExceptionHandler {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(CustomException1.class)
public void handleException() {
// ...
}
}我們應該知道,還有一個基礎類 (ResponseEntityExceptionHandler),我們可以從中繼承,從而使用預定義的常用功能,例如 ProblemDetails 的生成。 此外,我們還可以繼承方法來處理典型的 MVC 異常:
@ControllerAdvice
public class MyCustomResponseEntityExceptionHandler
extends ResponseEntityExceptionHandler {
@ExceptionHandler({
IllegalArgumentException.class,
IllegalStateException.class
})
ResponseEntity<Object> handleConflict(RuntimeException ex, WebRequest request) {
String bodyOfResponse = "This should be application specific";
return super.handleExceptionInternal(ex, bodyOfResponse,
new HttpHeaders(), HttpStatus.CONFLICT, request);
}
@Override
protected ResponseEntity<Object> handleHttpMediaTypeNotAcceptable(
HttpMediaTypeNotAcceptableException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request {
// ... (customization, maybe invoking the overridden method)
}
}請注意,在上述示例中,由於所有方法都返回 ResponseEntity,因此無需為類添加 @RestControllerAdvice 註解,我們此處使用了標準的 @ControllerAdvice 註解。
3. 直接註解異常
另一種簡單的方法是直接使用 @ResponseStatus 註解我們的自定義異常:
@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class MyResourceNotFoundException extends RuntimeException {
// ...
}與 DefaultHandlerExceptionResolver 類似,此解析器在處理響應體方面受到限制——它確實會映射響應上的狀態碼,但響應體仍然是 null。我們只能將其用於我們的自定義異常,因為我們無法對已編譯的現有類進行註解。並且,在分層架構中,我們應該僅將此方法用於邊界特定異常。
順便説一下,我們應該注意的是,在此上下文中,異常通常繼承自 RuntimeException ,因為我們此處不需要編譯器檢查。否則,這將在我們的代碼中導致不必要的 throws 聲明。
4. ResponseStatusException
控制器還可以拋出 ResponseStatusException。我們可以通過提供一個 HttpStatus,以及可選的 reason 和 cause 來創建它的實例:
@GetMapping(value = "/{id}")
public Foo findById(@PathVariable("id") Long id) {
try {
// ...
}
catch (MyResourceNotFoundException ex) {
throw new ResponseStatusException(
HttpStatus.NOT_FOUND, "Foo Not Found", ex);
}
}使用 ResponseStatusException 的優勢是什麼?
- 對於原型設計非常出色:我們可以快速實現一個基本解決方案。
- 單類型,多狀態碼:一種異常類型可以導致多種不同的響應。 這與 @ExceptionHandler 相比,減少了緊耦合。
- 我們不必創建這麼多自定義異常類。
- 由於異常可以程序化創建,因此我們對異常處理擁有 更大的控制權。
還有哪些權衡點?
- 沒有統一的異常處理方式:與 @ControllerAdvice 相比,難以強制執行一些應用程序範圍內的約定,後者提供了一種全局方法。
- 代碼重複:我們可能會在多個控制器中複製代碼。
- 在分層架構中,我們應該只在控制器中拋出這些異常。 如代碼示例所示,我們可能需要為來自底層層的異常進行異常包裝。
有關更多詳細信息和進一步示例,請參閲我們關於 ResponseStatusException 的教程。
5. HandlerExceptionResolver
另一個解決方案是定義一個自定義的 HandlerExceptionResolver。這將解決應用程序拋出的任何異常。它還將允許我們在 REST API 中實現一個 統一異常處理機制。
5.1. 已有實現
已啓用的實現位於<em lang="en">DispatcherServlet</em中:
<em lang="en">ExceptionHandlerExceptionResolver</em>是@<em lang="en">ExceptionHandler</em>機制前面所介紹的核心組件。<em lang="en">ResponseStatusExceptionResolver</em>是@<em lang="en">ResponseStatus</em>機制前面所介紹的核心組件。<em lang="en">DefaultHandlerExceptionResolver</em>用於將標準 Spring 異常映射到其相應的 HTTP 狀態碼,即客户端錯誤狀態碼4xx和服務器錯誤狀態碼5xx。 完整的異常列表及映射關係。 它正確設置了響應的狀態碼,但有一個限制是它不會設置響應體的內容。
5.2. 自定義 <em>HandlerExceptionResolver</em>
<em>DefaultHandlerExceptionResolver</em> 和 <em>ResponseStatusExceptionResolver</em> 的組合對於提供一個良好的錯誤處理機制,尤其適用於 Spring RESTful 服務。 然而,正如之前提到的,我們無法控制響應體的內容。
理想情況下,我們希望能夠輸出 JSON 或 XML,具體取決於客户端要求的格式(通過 `Accept 標頭)。
這本身就 justifica 創建一個 <em>自定義異常解析器</em>:
@Component
public class RestResponseStatusExceptionResolver extends AbstractHandlerExceptionResolver {
@Override
protected ModelAndView doResolveException(
HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
try {
if (ex instanceof IllegalArgumentException) {
return handleIllegalArgument(
(IllegalArgumentException) ex, response, handler);
}
// ...
} catch (Exception handlerException) {
logger.warn("Handling of [" + ex.getClass().getName() + "]
resulted in Exception", handlerException);
}
return null;
}
private ModelAndView handleIllegalArgument(
IllegalArgumentException ex, HttpServletResponse response) throws IOException {
response.sendError(HttpServletResponse.SC_CONFLICT);
String accept = request.getHeader(HttpHeaders.ACCEPT);
// ...
return new ModelAndView();
}
}需要注意的是,我們能夠訪問到實際的request,因此可以考慮客户端發送的Accept請求頭的值。
例如,如果客户端請求application/json,那麼在發生錯誤條件時,我們應該確保返回包含application/json編碼的響應體。
另一個重要的實現細節是,我們返回一個ModelAndView——這是響應的體,並且它將允許我們設置其中的必要內容。
這種方法是 Spring REST Service 錯誤處理的始終如一且易於配置的機制。
然而,它也有一些侷限性:它與低級別的 HtttpServletResponse 交互,並且符合舊的 MVC 模型,該模型使用 ModelAndView。
6. 補充説明
以下是一些補充説明,以幫助您更好地理解和應用該技術。
6.1. 處理現有異常
以下是我們在典型 REST 實現中經常處理的幾種異常:
- AccessDeniedException 發生在經過身份驗證的用户嘗試訪問其沒有足夠權限訪問的資源時。例如,當我們使用方法級別安全註解(如 @PreAuthorize、@PostAuthorize 和 @Secure)時,可能會發生這種情況。
- ValidationException 和 ConstraintViolationException 發生在 Bean Validation 中。
- PersistenceException 和 DataAccessException 發生在 Spring Data JPA 中。
當然,我們也會使用我們之前討論的全局異常處理機制來處理 AccessDeniedException:
@RestControllerAdvice
public class MyGlobalExceptionHandler {
@ResponseStatus(value = HttpStatus.FORBIDDEN)
@ExceptionHandler( AccessDeniedException.class )
public void handleAccessDeniedException() {
// ...
}
}6.2. Spring Boot 支持
Spring Boot 提供一個 ErrorController 實現,用於以合理的方式處理錯誤。
簡單來説,它作為瀏覽器的備用錯誤頁面(又稱 Whitelabel Error Page)以及對 RESTful、非 HTML 請求的 JSON 響應。
{
"timestamp": "2019-01-17T16:12:45.977+0000",
"status": 500,
"error": "Internal Server Error",
"message": "Error processing the request!",
"path": "/my-endpoint-with-exceptions"
}如往常一樣,Spring Boot 允許通過屬性配置這些功能:
- server.error.whitelabel.enabled: 可用於禁用 Whitelabel 錯誤頁面,並依賴 Servlet 容器提供 HTML 錯誤消息。
- server.error.include-stacktrace: 當值為 always 時,會在 HTML 和 JSON 默認響應中包含堆棧跟蹤。
- server.error.include-message: 自 2.3 版本起,Spring Boot 會隱藏響應中的 message 字段,以避免泄露敏感信息;我們可以使用值為 always 的屬性來啓用它。
除了這些屬性之外,我們可以為 /error 路徑提供自定義的視圖解析器映射,從而覆蓋 Whitelabel 頁面。
此外,我們還可以通過在上下文中包含一個 ErrorAttributes Bean 來自定義響應中要顯示的屬性。 我們可以通過擴展 Spring Boot 提供的 DefaultErrorAttributes 類來簡化操作:
@Component
public class MyCustomErrorAttributes extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(
WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, options);
errorAttributes.put("locale", webRequest.getLocale().toString());
errorAttributes.remove("error");
//...
return errorAttributes;
}
}如果我們想要進一步定義(或覆蓋)應用程序如何處理特定內容類型的錯誤,我們可以註冊一個ErrorController Bean。
再次強調,我們可以利用 Spring Boot 提供的默認BasicErrorController 來幫助我們。
例如,假設我們想要自定義應用程序如何處理在 XML 端點觸發的錯誤。 唯一需要做的就是定義一個使用@RequestMapping註解的公共方法,並聲明它產生application/xml媒體類型:
@Component
public class MyErrorController extends BasicErrorController {
public MyErrorController(
ErrorAttributes errorAttributes, ServerProperties serverProperties) {
super(errorAttributes, serverProperties.getError());
}
@RequestMapping(produces = MediaType.APPLICATION_XML_VALUE)
public ResponseEntity<Map<String, Object>> xmlError(HttpServletRequest request) {
// ...
}
}注意:在此,我們仍然依賴於項目中定義的,與 ServerProperties Bean 綁定的 server.error.* Spring Boot 屬性。
7. 結論
本文討論了在 Spring 中實現 REST API 異常處理機制的幾種方法,並根據其使用場景進行了比較。
需要注意的是,在單個應用程序中可以組合使用不同的方法。例如,我們可以全局地實現 @ControllerAdvice,同時也可以在本地使用 ResponseStatusException。