知識庫 / Spring RSS 訂閱

REST API 自定義錯誤消息處理

REST,Spring
HongKong
7
03:56 AM · Dec 06 ,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. 處理無效請求異常

3.1. 處理異常

現在讓我們看看如何處理最常見的客户端錯誤——即客户端向 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());
}
<ul>
 <li id="ConstraintViolationException">
  <p><em>ConstraintViolationException</em> – This exception reports the result of constraint violations:</p>
 </li>
</ul>
@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());
}
<ul>
 <li id="TypeMismatchException">
  <p><em>TypeMismatchException</em> – 當嘗試使用錯誤的類型設置 Bean 屬性時,會拋出此異常。</p>
 </li>
 <li id="MethodArgumentTypeMismatchException">
  <p><em>MethodArgumentTypeMismatchException</em> – 當方法參數類型與預期類型不匹配時,會拋出此異常:</p>
 </li>
</ul>
@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. 從客户端消費 API

讓我們現在來看一個運行到 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響應:

<servlet>
    <servlet-name>api</servlet-name>
    <servlet-class>
      org.springframework.web.servlet.DispatcherServlet</servlet-class>        
    <init-param>
        <param-name>throwExceptionIfNoHandlerFound</param-name>
        <param-value>true</param-value>
    </init-param>
</servlet>

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

@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

當使用不支持的 HTTP 方法發送請求時,會發生 HttpRequestMethodNotSupportedException 異常。

@Override
protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(
  HttpRequestMethodNotSupportedException ex, 
  HttpHeaders headers, 
  HttpStatus status, 
  WebRequest request) {
    StringBuilder builder = new StringBuilder();
    builder.append(ex.getMethod());
    builder.append(
      " method is not supported for this request. Supported methods are ");
    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(" media type is not supported. Supported media types are ");
    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(), "error occurred");
    return new ResponseEntity<Object>(
      apiError, new HttpHeaders(), apiError.getStatus());
}

8. 結論

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

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

發佈 評論

Some HTML is okay.