REST API錯誤處理與Spring

REST,Spring
Remote
1
07:06 PM · Dec 01 ,2025

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 中:

  • ExceptionHandlerExceptionResolverExceptionHandler 機制早期呈現的核心組件。
  • ResponseStatusExceptionResolver@ResponseStatus
  • DefaultHandlerExceptionResolver4xx 和服務器錯誤 5xx 狀態碼。 此處列出了它處理的 Spring 異常及其映射到狀態碼的方式。 雖然它正確地設置了響應的狀態碼,但一個 限制是它沒有設置任何內容到響應的 body

5.2. 自定義 HandlerExceptionResolver

DefaultHandlerExceptionResolverResponseStatusExceptionResolver 的組合在為 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時,可能會發生這種情況。
  • ValidationExceptionConstraintViolationException 發生在 Bean Validation 使用時。
  • PersistenceExceptionDataAccessException 發生在 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)以及為非 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 來啓用它
除了這些屬性之外,我們可以在 /error 中提供我們的 view-resolver 映射,從而覆蓋 Whitelabel Page。

我們還可以通過在上下文中包含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

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.