自定義 REST API 錯誤消息處理

REST,Spring
Remote
1
03:46 PM · Dec 01 ,2025

1. 概述

在本教程中,我們將討論如何為 Spring REST API 實現全局錯誤處理程序。

我們將利用每個異常的語義來構建有意義的錯誤消息,以便向客户端提供清晰的信息,從而幫助客户端輕鬆診斷問題。

2. 自定義錯誤消息

讓我們首先實現一個簡單的結構,用於通過網絡發送錯誤——ApiError:

public class ApiError {

    private HttpStatus status;
    private String message;
    private List<String> errors;

    public ApiError(HttpStatus status, String message, List<String> errors) {
        super();
        this.status = status;
        this.message = message;
        this.errors = errors;
    }

    public ApiError(HttpStatus status, String message, String error) {
        super();
        this.status = status;
        this.message = message;
        errors = Arrays.asList(error);
    }
}

這裏的信息應該很簡單:

  • status – HTTP 狀態碼
  • message – 異常關聯的錯誤消息
  • error – 構造的錯誤消息列表

當然,在 Spring 的實際異常處理邏輯中,我們將使用 @ControllerAdvice 註解:

@ControllerAdvice
public class CustomRestExceptionHandler extends ResponseEntityExceptionHandler {
    ...
}

3. Handle Bad Request Exceptions

3.1. Handling the Exceptions

現在讓我們看看如何處理最常見的客户端錯誤——基本上是客户端將無效請求發送到 API 的情況:

  • BindException – 此異常在發生致命綁定錯誤時拋出。
  • MethodArgumentNotValidException – 此異常在用 @Valid 註解標記的參數失敗時拋出:

@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
  MethodArgumentNotValidException ex, 
  HttpHeaders headers, 
  HttpStatus status, 
  WebRequest request) {
    List<String> errors = new ArrayList<String>();
    for (FieldError error : ex.getBindingResult().getFieldErrors()) {
        errors.add(error.getField() + ": " + error.getDefaultMessage());
    }
    for (ObjectError error : ex.getBindingResult().getGlobalErrors()) {
        errors.add(error.getObjectName() + ": " + error.getDefaultMessage());
    }
    
    ApiError apiError = 
      new ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), errors);
    return handleExceptionInternal(
      ex, apiError, headers, apiError.getStatus(), request);
}

注意,我們正在從 ResponseEntityExceptionHandler 類中覆蓋一個基本方法,並提供自己的自定義實現。

這並不總是如此。 有時,我們需要處理沒有基本類中默認實現的自定義異常,就像稍後會看到的那樣。

接下來:

  • MissingServletRequestPartException – 此異常在未找到多部分請求的一部分時拋出。
  • MissingServletRequestParameterException – 此異常在請求缺少參數時拋出:

@Override
protected ResponseEntity<Object> handleMissingServletRequestParameter(
  MissingServletRequestParameterException ex, HttpHeaders headers, 
  HttpStatus status, WebRequest request) {
    String error = ex.getParameterName() + " parameter is missing";
    
    ApiError apiError = 
      new ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), error);
    return new ResponseEntity<Object>(
      apiError, new HttpHeaders(), apiError.getStatus());
}
  • ConstraintViolationException – 此異常報告約束違反的結果:
  • @ExceptionHandler({ ConstraintViolationException.class })
    public ResponseEntity<Object> handleConstraintViolation(
      ConstraintViolationException ex, WebRequest request) {
        List<String> errors = new ArrayList<String>();
        for (ConstraintViolation<?> violation : ex.getConstraintViolations()) {
            errors.add(violation.getRootBeanClass().getName() + " " + 
              violation.getPropertyPath() + ": " + violation.getMessage());
        }
    
        ApiError apiError = 
          new ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), errors);
        return new ResponseEntity<Object>(
          apiError, new HttpHeaders(), apiError.getStatus());
    }
    • TypeMismatchException – 此異常在嘗試用錯誤的類型設置 bean 屬性時拋出。
    • MethodArgumentTypeMismatchException – 此異常在方法參數不是預期類型時拋出:

    • @ExceptionHandler({ MethodArgumentTypeMismatchException.class })
      public ResponseEntity<Object> handleMethodArgumentTypeMismatch(
        MethodArgumentTypeMismatchException ex, WebRequest request) {
          String error = 
            ex.getName() + " should be of type " + ex.getRequiredType().getName();
      
          ApiError apiError = 
            new ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), error);
          return new ResponseEntity<Object>(
            apiError, new HttpHeaders(), apiError.getStatus());
      }

      3.2. Consuming the API From the Client

      現在讓我們看看一個運行到 MethodArgumentTypeMismatchException 的測試。

      我們將 發送一個請求,其中 id 作為 String 而不是 long:

      @Test
      public void whenMethodArgumentMismatch_thenBadRequest() {
          Response response = givenAuth().get(URL_PREFIX + "/api/foos/ccc");
          ApiError error = response.as(ApiError.class);
      
          assertEquals(HttpStatus.BAD_REQUEST, error.getStatus());
          assertEquals(1, error.getErrors().size());
          assertTrue(error.getErrors().get(0).contains("should be of type"));
      }

      並且最後,考慮同一個請求:

      Request method:	GET
      Request path:	http://localhost:8080/spring-security-rest/api/foos/ccc
      

      這裏是這種 JSON 錯誤響應的外觀:

      {
          "status": "BAD_REQUEST",
          "message": 
            "Failed to convert value of type [java.lang.String] 
             to required type [java.lang.Long]; nested exception 
             is java.lang.NumberFormatException: For input string: \"ccc\"",
          "errors": [
              "id should be of type java.lang.Long"
          ]
      }

      4. 處理 NoHandlerFoundException

      接下來,我們可以自定義servlet來拋出此異常,而不是發送404響應:

      
          
              api
              
                org.springframework.web.servlet.DispatcherServlet
              
              
                  throwExceptionIfNoHandlerFound
                  true
              
          
      

      然後,一旦發生這種情況,我們可以像處理任何其他異常一樣簡單地處理它:

      @Override
      protected ResponseEntity<Object> handleNoHandlerFoundException(
        NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
          String error = "No handler found for " + ex.getHttpMethod() + " " + ex.getRequestURL();
      
          ApiError apiError = new ApiError(HttpStatus.NOT_FOUND, ex.getLocalizedMessage(), error);
          return new ResponseEntity<Object>(apiError, new HttpHeaders(), apiError.getStatus());
      }
      

      這是一個簡單的測試:

      @Test
      public void whenNoHandlerForHttpRequest_thenNotFound() {
          Response response = givenAuth().delete(URL_PREFIX + "/api/xx");
          ApiError error = response.as(ApiError.class);
      
          assertEquals(HttpStatus.NOT_FOUND, error.getStatus());
          assertEquals(1, error.getErrors().size());
          assertTrue(error.getErrors().get(0).contains("No handler found"));
      }
      

      讓我們查看完整的請求:

      Request method:	DELETE
      Request path:	http://localhost:8080/spring-security-rest/api/xx

      以及 錯誤 JSON 響應:

      {
          "status":"NOT_FOUND",
          "message":"No handler found for DELETE /spring-security-rest/api/xx",
          "errors":[
              "No handler found for DELETE /spring-security-rest/api/xx"
          ]
      }
      

      接下來,我們將查看另一個有趣的異常。

      5. 處理 HttpRequestMethodNotSupportedException

      HttpRequestMethodNotSupportedException 發生在向請求發送不支持的 HTTP 方法時:

      @Override
      protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(
        HttpRequestMethodNotSupportedException ex, 
        HttpHeaders headers, 
        HttpStatus status, 
        WebRequest request) {
          StringBuilder builder = new StringBuilder();
          builder.append(ex.getMethod());
          builder.append(
            " 方法不支持此請求。 支持的方法是 ");
          ex.getSupportedHttpMethods().forEach(t -> builder.append(t + " "));
      
          ApiError apiError = new ApiError(HttpStatus.METHOD_NOT_ALLOWED, 
            ex.getLocalizedMessage(), builder.toString());
          return new ResponseEntity<Object>(
            apiError, new HttpHeaders(), apiError.getStatus());
      }

      這是一個簡單的測試,可以重現此異常:

      @Test
      public void whenHttpRequestMethodNotSupported_thenMethodNotAllowed() {
          Response response = givenAuth().delete(URL_PREFIX + "/api/foos/1");
          ApiError error = response.as(ApiError.class);
      
          assertEquals(HttpStatus.METHOD_NOT_ALLOWED, error.getStatus());
          assertEquals(1, error.getErrors().size());
          assertTrue(error.getErrors().get(0).contains("Supported methods are"));
      }

      下面是完整的請求:

      Request method:	DELETE
      Request path:	http://localhost:8080/spring-security-rest/api/foos/1

      以及 錯誤 JSON 響應:

      {
          "status":"METHOD_NOT_ALLOWED",
          "message":"Request method 'DELETE' not supported",
          "errors":[
              "DELETE method is not supported for this request. Supported methods are GET "
          ]
      }

      6. 處理 HttpMediaTypeNotSupportedException

      現在我們來處理 HttpMediaTypeNotSupportedException,該異常發生在客户端發送帶有不支持的媒體類型的請求時:

      @Override
      protected ResponseEntity<Object> handleHttpMediaTypeNotSupported(
        HttpMediaTypeNotSupportedException ex, 
        HttpHeaders headers, 
        HttpStatus status, 
        WebRequest request) {
          StringBuilder builder = new StringBuilder();
          builder.append(ex.getContentType());
          builder.append(" 媒體類型不受支持。 支持的媒體類型包括 ");
          ex.getSupportedMediaTypes().forEach(t -> builder.append(t + ", "));
      
          ApiError apiError = new ApiError(HttpStatus.UNSUPPORTED_MEDIA_TYPE, 
            ex.getLocalizedMessage(), builder.substring(0, builder.length() - 2));
          return new ResponseEntity<Object>(
            apiError, new HttpHeaders(), apiError.getStatus());
      }

      以下是一個遇到此問題時的簡單測試:

      @Test
      public void whenSendInvalidHttpMediaType_thenUnsupportedMediaType() {
          Response response = givenAuth().body("").post(URL_PREFIX + "/api/foos");
          ApiError error = response.as(ApiError.class);
      
          assertEquals(HttpStatus.UNSUPPORTED_MEDIA_TYPE, error.getStatus());
          assertEquals(1, error.getErrors().size());
          assertTrue(error.getErrors().get(0).contains("media type is not supported"));
      }

      最後,這是一個示例請求:

      Request method:	POST
      Request path:	http://localhost:8080/spring-security-
      Headers:	Content-Type=text/plain; charset=ISO-8859-1

      以及 錯誤 JSON 響應:

      {
          "status":"UNSUPPORTED_MEDIA_TYPE",
          "message":"Content type 'text/plain;charset=ISO-8859-1' not supported",
          "errors":["text/plain;charset=ISO-8859-1 media type is not supported. 
             Supported media types are text/xml 
             application/x-www-form-urlencoded 
             application/*+xml 
             application/json;charset=UTF-8 
             application/*+json;charset=UTF-8 "]
          }
      

      7. 默認處理程序

      最後,我們將實現一個回退處理程序——一種兜底邏輯,用於處理所有沒有特定處理程序的其他異常:

      @ExceptionHandler({ Exception.class })
      public ResponseEntity<Object> handleAll(Exception ex, WebRequest request) {
          ApiError apiError = new ApiError(
            HttpStatus.INTERNAL_SERVER_ERROR, ex.getLocalizedMessage(), "錯誤發生");
          return new ResponseEntity<Object>(
            apiError, new HttpHeaders(), apiError.getStatus());
      }

      8. 結論

      構建成熟的 Spring REST API 錯誤處理程序具有挑戰性,並且是一個迭代的過程。希望本教程能作為一個好的起點,並幫助 API 客户端快速、輕鬆地診斷和解決錯誤。

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

發佈 評論

Some HTML is okay.