1. 概述
本教程將演示如何使用 Spring 實現 REST API 的異常處理。我們將學習到這其中存在多種可能性。所有這些都具有一個共同點:它們都非常有效地處理了 分層分離 的概念。應用程序可以正常地拋出異常以指示某種失敗,然後這些異常會被單獨處理。">
" to annotate methods that Spring automatically invokes when the given exception occurs. We can specify the exception either with the annotation or by declaring it as a method parameter, which allows us to read out details from the exception object to handle it correctly. The method itself is handled as a Controller method, so:
- ". Content Negotiation is allowed here .
- " object. Spring will set the " header automatically to ““”.
- .
@ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(CustomException1.class) public void handleException1() {}
problem details object:
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler
public ProblemDetail handleException2(CustomException2 ex) {
// ...
}
@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) { // ... }
@RestController public class FooController { //... @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(CustomException1.class) public void handleException() { // ... } }
. But there’s another approach that fits better in the sense of composition over inheritance.
contains code that is shared between multiple controllers. It’s a special kind of Spring component. Especially for REST APIs, where each method’s return value should be rendered into the response body, there’s a .
@RestControllerAdvice public class MyGlobalExceptionHandler { @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(CustomException1.class) public void handleException() { // ... } }
) that we could inherit from to use common pre-defined functionality like " generation. We could also inherit methods for handling typical MVC exceptions:
@ControllerAdvice
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)
}
}" annotation to the class since all methods return a ", so we’ve used the vanilla " annotation here.
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. 現有的實現
已經啓用的現有實現駐留在 DispatcherServlet 中:
- ExceptionHandlerExceptionResolver
ExceptionHandler 機制早期呈現的核心組件。 - ResponseStatusExceptionResolver
@ResponseStatus - DefaultHandlerExceptionResolver
4xx 和服務器錯誤 5xx 狀態碼。 此處列出了它處理的 Spring 異常及其映射到狀態碼的方式。 雖然它正確地設置了響應的狀態碼,但一個 限制是它沒有設置任何內容到響應的 body - DefaultHandlerExceptionResolver
5.2. 自定義 HandlerExceptionResolver
DefaultHandlerExceptionResolver 和 ResponseStatusExceptionResolver 的組合在為 Spring RESTful 服務提供良好的錯誤處理機制方面大有裨益。 然而,正如之前提到的,我們無法控制響應的 body
理想情況下,我們希望能夠輸出 JSON 或 XML,具體取決於客户端要求的格式(通過 Accept header)。
這本身就充分正當創建一個新的、自定義的異常解析器:
@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 header 的值。
例如,如果客户端要求 application/json,那麼在出現錯誤條件時,我們應該確保返回一個用 application/json 編碼的響應 body。
另一個重要的實現細節是,我們返回一個 ModelAndView — 這是響應的 body,並且它將允許我們設置其中的任何內容。
這種方法是為 Spring RESTful 服務錯誤處理的,並且是一種一致且易於配置的機制。
它,然而,存在一些限制:它與低級別的 HtttpServletResponse 交互,並且融入了使用 ModelAndView 的舊 MVC 模型。
6. 進一步説明
6.1. 處理現有異常
存在一些我們經常需要處理的異常,例如在典型的 REST 實現中:- AccessDeniedException 發生在經過身份驗證的用户嘗試訪問他沒有權限訪問的資源時。例如,當我們使用方法級別安全註解,如@PreAuthorize、@PostAuthorize 和@Secure時,可能會發生這種情況。
- ValidationException 和ConstraintViolationException 發生在 Bean Validation 使用時。
- PersistenceException 和DataAccessException 發生在 Spring Data JPA 使用時。
@RestControllerAdvice
public class MyGlobalExceptionHandler {
@ResponseStatus(value = HttpStatus.FORBIDDEN)
@ExceptionHandler( AccessDeniedException.class )
public void handleAccessDeniedException() {
// ...
}
}
6.2. Spring Boot 支持
Spring Boot 提供了一個ErrorController 實現,用於以合理的方式處理錯誤。簡而言之,它為瀏覽器提供了一個備用錯誤頁面(又稱 Whitelabel Error Page)以及為非 HTML 請求(RESTful 請求)提供 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 Error Page,並依賴 Servlet 容器提供 HTML 錯誤消息
- server.error.include-stacktrace,如果設置為always,則在 HTML 和 JSON 默認響應中包含堆棧跟蹤
- server.error.include-message,自版本 2.3 起,Spring Boot 會隱藏message 字段以避免泄露敏感信息;我們可以通過將always 設置為true 來啓用它
我們還可以通過在上下文中包含ErrorAttributes 豆來定製響應中要顯示屬性:我們可以擴展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 豆。
再次,我們可以利用 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) {
// ...
}
}
注意:在這裏,我們仍然依賴於我們可能在項目中定義的server.error.* Spring Boot 屬性,這些屬性與ServerProperties 豆綁定。
7. 結論
在本文中,我們討論了在 Spring 中實現 REST API 異常處理機制的幾種方法,並根據其使用場景進行了比較。
應注意的是,在單個應用程序中可以組合使用不同的方法。例如,我們可以全局實現一個@ControllerAdvice,同時也可以本地實現ResponseStatusException。