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 的測試。
我們將
@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
{
"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 客户端快速、輕鬆地診斷和解決錯誤。