Custom Error Message Handling for REST API – 为REST API定制错误信息处理

最后修改: 2016年 1月 20日

1. Overview

1.概述

In this tutorial, we’ll discuss how to implement a global error handler for a Spring REST API.

在本教程中,我们将讨论如何为Spring REST API实现一个全局错误处理器。

We will use the semantics of each exception to build out meaningful error messages for the client, with the clear goal of giving that client all the info to easily diagnose the problem.

我们将使用每个异常的语义来为客户建立有意义的错误信息,其明确的目标是给客户提供所有的信息来轻松诊断问题。

2. A Custom Error Message

2.一个自定义的错误信息

Let’s start by implementing a simple structure for sending errors over the wire — the ApiError:

让我们从实现一个简单的结构开始,通过电线发送错误 – 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);
    }
}

The information here should be straightforward:

这里的信息应该是直截了当的。

  • status – the HTTP status code
  • message – the error message associated with exception
  • error – List of constructed error messages

And of course, for the actual exception handling logic in Spring, we’ll use the @ControllerAdvice annotation:

当然,对于Spring中实际的异常处理逻辑,我们将使用@ControllerAdvice注释。

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

3. Handle Bad Request Exceptions

3.处理坏的请求异常

3.1. Handling the Exceptions

3.1.处理异常情况

Now let’s see how we can handle the most common client errors — basically scenarios of a client sending an invalid request to the API:

现在让我们看看我们如何处理最常见的客户端错误–基本上是客户端向API发送无效请求的情况。

  • BindException – This exception is thrown when fatal binding errors occur.
  • MethodArgumentNotValidException – This exception is thrown when an argument annotated with @Valid failed validation:

    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);
}

Note that we are overriding a base method out of the ResponseEntityExceptionHandler and providing our own custom implementation.

请注意,我们正在覆盖ResponseEntityExceptionHandler中的一个基础方法,并提供我们自己的自定义实现。

That’s not always going to be the case. Sometimes, we’re going to need to handle a custom exception that doesn’t have a default implementation in the base class, as we’ll get to see later on here.

这并不总是这样的。有时,我们需要处理一个基类中没有默认实现的自定义异常,正如我们在后面要看到的。

Next:

下一步。

  • MissingServletRequestPartException – This exception is thrown when the part of a multipart request is not found.

    MissingServletRequestPartException – 当一个多部分请求的部分没有找到时,会抛出这个异常。

  • MissingServletRequestParameterException – This exception is thrown when the request is missing a parameter:

    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 – This exception reports the result of constraint violations:

    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 – This exception is thrown when trying to set bean property with the wrong type.

    TypeMismatchException – 当试图以错误的类型设置Bean属性时,会抛出这个异常。

  • MethodArgumentTypeMismatchException – This exception is thrown when method argument is not the expected type:

    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

3.2.从客户端获取API

Let’s now have a look at a test that runs into a MethodArgumentTypeMismatchException.

现在让我们来看看一个遇到MethodArgumentTypeMismatchException的测试。

We’ll send a request with id as String instead of long:

我们将发送一个idString的请求,而不是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"));
}

And finally, considering this same request:

最后,考虑到这个相同的请求。

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

here’s what this kind of JSON error response will look like:

这种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. Handle NoHandlerFoundException

4.处理NoHandlerFoundException

Next, we can customize our servlet to throw this exception instead of sending a 404 response:

接下来,我们可以定制我们的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>

Then, once this happens, we can simply handle it just like any other exception:

然后,一旦发生这种情况,我们可以简单地像处理其他异常一样处理它。

@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());
}

Here is a simple test:

这里有一个简单的测试。

@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"));
}

Let’s have a look at the full request:

让我们来看看完整的请求。

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

and the error JSON response:

错误的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"
    ]
}

Next, we’ll look at another interesting exception.

接下来,我们将看一下另一个有趣的例外。

5. Handle HttpRequestMethodNotSupportedException

5.处理HttpRequestMethodNotSupportedException

The HttpRequestMethodNotSupportedException occurs when we send a requested with an unsupported HTTP method:

当我们用不支持的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());
}

Here is a simple test reproducing this exception:

下面是一个简单的测试,再现了这个异常。

@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"));
}

And here’s the full request:

而这里是完整的请求。

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

and the error JSON response:

错误的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. Handle HttpMediaTypeNotSupportedException

6.处理HttpMediaTypeNotSupportedException

Now let’s handle HttpMediaTypeNotSupportedException, which occurs when the client sends a request with unsupported media type:

现在让我们来处理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());
}

Here is a simple test running into this issue:

下面是一个遇到这个问题的简单测试。

@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"));
}

Finally, here’s a sample request:

最后,这里有一个请求样本。

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

and the error JSON response:

错误的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. Default Handler

7.默认处理程序

Lastly, we’re going to implement a fallback handler — a catch-all type of logic that deals with all other exceptions that don’t have specific handlers:

最后,我们要实现一个回避处理程序–一个万能的逻辑类型,处理所有其他没有特定处理程序的异常。

@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. Conclusion

8.结论

Building a proper, mature error handler for a Spring REST API is tough and definitely an iterative process. Hopefully, this tutorial will be a good starting point as well as a good anchor for helping API clients to quickly and easily diagnose errors and move past them.

为Spring REST API建立一个适当的、成熟的错误处理程序是很困难的,而且肯定是一个反复的过程。希望本教程能成为一个好的起点,也是帮助API客户快速、轻松地诊断错误并超越错误的好帮手。

The full implementation of this tutorial can be found in the GitHub project. This is an Eclipse-based project, so it should be easy to import and run as it is.

本教程的完整实现可以在GitHub项目中找到。这是一个基于Eclipse的项目,所以应该很容易导入并按原样运行。