Error Handling for REST with Spring – 用Spring对REST进行错误处理

最后修改: 2013年 10月 28日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

This tutorial will illustrate how to implement Exception Handling with Spring for a REST API. We’ll also get a bit of historical overview and see which new options the different versions introduced.

本教程将说明如何使用Spring为REST API实现异常处理。我们还将了解一些历史概况,并查看不同版本引入的新选项。

Before Spring 3.2, the two main approaches to handling exceptions in a Spring MVC application were HandlerExceptionResolver or the @ExceptionHandler annotation. Both have some clear downsides.

在Spring 3.2之前,在Spring MVC应用程序中处理异常的两种主要方法是HandlerExceptionResolver@ExceptionHandler注解。两者都有一些明显的弊端。

Since 3.2, we’ve had the @ControllerAdvice annotation to address the limitations of the previous two solutions and to promote a unified exception handling throughout a whole application.

从3.2开始,我们有了@ControllerAdvice注解,以解决前两种解决方案的局限性,并促进整个应用程序的统一的异常处理。

Now Spring 5 introduces the ResponseStatusException class — a fast way for basic error handling in our REST APIs.

现在,Spring 5引入了ResponseStatusException class–这是一种在我们的REST API中进行基本错误处理的快速方法。

All of these do have one thing in common: They deal with the separation of concerns very well. The app can throw exceptions normally to indicate a failure of some kind, which will then be handled separately.

所有这些都有一个共同点:它们能很好地处理关注点分离。应用程序可以正常地抛出异常,以表示某种失败,然后将被单独处理。

Finally, we’ll see what Spring Boot brings to the table and how we can configure it to suit our needs.

最后,我们将看到Spring Boot带来了什么,以及我们如何配置它以满足我们的需求。

2. Solution 1: the Controller-Level @ExceptionHandler

2.解决方案1:控制器级别的@ExceptionHandler

The first solution works at the @Controller level. We will define a method to handle exceptions and annotate that with @ExceptionHandler:

第一个解决方案是在@Controller层工作。我们将定义一个方法来处理异常,并用@ExceptionHandler来注释。

public class FooController{
    
    //...
    @ExceptionHandler({ CustomException1.class, CustomException2.class })
    public void handleException() {
        //
    }
}

This approach has a major drawback: The @ExceptionHandler annotated method is only active for that particular Controller, not globally for the entire application. Of course, adding this to every controller makes it not well suited for a general exception handling mechanism.

这种方法有一个很大的缺点。@ExceptionHandler 注释的方法只对那个特定的控制器有效,而不是对整个应用程序有效。当然,在每个控制器中添加这个方法使得它不太适合作为一个通用的异常处理机制。

We can work around this limitation by having all Controllers extend a Base Controller class.

我们可以通过让所有控制器扩展一个基控制器类来解决这个限制。

However, this solution can be a problem for applications where, for whatever reason, that isn’t possible. For example, the Controllers may already extend from another base class, which may be in another jar or not directly modifiable, or may themselves not be directly modifiable.

然而,这个解决方案对于那些由于某种原因而无法实现的应用来说是个问题。例如,控制器可能已经扩展自另一个基类,而这个基类可能在另一个jar中,或者不能直接修改,或者本身不能直接修改。

Next, we’ll look at another way to solve the exception handling problem — one that is global and doesn’t include any changes to existing artifacts such as Controllers.

接下来,我们将看看另一种解决异常处理问题的方法–一种全局性的,不包括对现有工件(如控制器)的任何改变的方法。

3. Solution 2: the HandlerExceptionResolver

3、解决方案2:HandlerExceptionResolver

The second solution is to define an HandlerExceptionResolver. This will resolve any exception thrown by the application. It will also allow us to implement a uniform exception handling mechanism in our REST API.

第二个解决方案是定义一个HandlerExceptionResolver。这将解决应用程序抛出的任何异常。它还将允许我们在REST API中实现一个统一的异常处理机制

Before going for a custom resolver, let’s go over the existing implementations.

在使用自定义解析器之前,让我们看看现有的实现。

3.1. ExceptionHandlerExceptionResolver

3.1.ExceptionHandlerExceptionResolver

This resolver was introduced in Spring 3.1 and is enabled by default in the DispatcherServlet. This is actually the core component of how the @ExceptionHandler mechanism presented earlier works.

这个解析器是在Spring 3.1中引入的,在DispatcherServlet中默认启用。这实际上是前面介绍的@ExceptionHandler机制如何工作的核心组件。

3.2. DefaultHandlerExceptionResolver

3.2.DefaultHandlerExceptionResolver

This resolver was introduced in Spring 3.0, and it’s enabled by default in the DispatcherServlet.

这个解析器是在Spring 3.0中引入的,它在DispatcherServlet中被默认启用。

It’s used to resolve standard Spring exceptions to their corresponding HTTP Status Codes, namely Client error 4xx and Server error 5xx status codes. Here’s the full list of the Spring Exceptions it handles and how they map to status codes.

它用于将标准的 Spring 异常解析为其相应的HTTP 状态代码,即客户端错误4xx和服务器错误5xx状态代码。以下是其处理的Spring异常的完整列表以及它们如何映射到状态代码。

While it does set the Status Code of the Response properly, one limitation is that it doesn’t set anything to the body of the Response. And for a REST API — the Status Code is really not enough information to present to the Client — the response has to have a body as well, to allow the application to give additional information about the failure.

虽然它确实正确地设置了响应的状态代码,但有一个限制是它没有为响应的主体设置任何东西。而对于REST API来说,状态代码确实不足以向客户端展示信息–响应也必须有一个主体,以允许应用程序提供有关失败的额外信息。

This can be solved by configuring view resolution and rendering error content through ModelAndView, but the solution is clearly not optimal. That’s why Spring 3.2 introduced a better option that we’ll discuss in a later section.

这可以通过配置视图分辨率和通过ModelAndView渲染错误内容来解决,但该方案显然不是最佳方案。这就是为什么Spring 3.2引入了一个更好的选择,我们将在后面的章节中讨论。

3.3. ResponseStatusExceptionResolver

3.3.ResponseStatusExceptionResolver

This resolver was also introduced in Spring 3.0 and is enabled by default in the DispatcherServlet.

这个解析器也是在Spring 3.0中引入的,在DispatcherServlet中默认启用。

Its main responsibility is to use the @ResponseStatus annotation available on custom exceptions and to map these exceptions to HTTP status codes.

它的主要职责是使用@ResponseStatus注解,可用于自定义异常,并将这些异常映射到HTTP状态代码。

Such a custom exception may look like:

这样一个自定义的例外可能看起来像。

@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class MyResourceNotFoundException extends RuntimeException {
    public MyResourceNotFoundException() {
        super();
    }
    public MyResourceNotFoundException(String message, Throwable cause) {
        super(message, cause);
    }
    public MyResourceNotFoundException(String message) {
        super(message);
    }
    public MyResourceNotFoundException(Throwable cause) {
        super(cause);
    }
}

The same as the DefaultHandlerExceptionResolver, this resolver is limited in the way it deals with the body of the response — it does map the Status Code on the response, but the body is still null.

DefaultHandlerExceptionResolver相同,这个解析器在处理响应的主体方面受到限制–它确实映射了响应上的状态代码,但是主体仍然是null。

3.4. Custom HandlerExceptionResolver

3.4.自定义HandlerExceptionResolver

The combination of DefaultHandlerExceptionResolver and ResponseStatusExceptionResolver goes a long way toward providing a good error handling mechanism for a Spring RESTful Service. The downside is, as mentioned before, no control over the body of the response.

DefaultHandlerExceptionResolverResponseStatusExceptionResolver的组合在为Spring RESTful服务提供良好的错误处理机制方面发挥了很大作用。缺点是,如前所述,无法控制响应的主体

Ideally, we’d like to be able to output either JSON or XML, depending on what format the client has asked for (via the Accept header).

理想情况下,我们希望能够输出JSON或XML,这取决于客户要求的格式(通过Acceptheader)。

This alone justifies creating a new, custom exception resolver:

仅仅这一点就证明了创建一个新的、自定义的异常解析器的合理性。

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

One detail to notice here is that we have access to the request itself, so we can consider the value of the Accept header sent by the client.

这里需要注意的一个细节是,我们可以访问request本身,所以我们可以考虑客户端发送的Accept头的值。

For example, if the client asks for application/json, then, in the case of an error condition, we’d want to make sure we return a response body encoded with application/json.

例如,如果客户端要求application/json,那么,在出现错误的情况下,我们要确保返回一个用application/json编码的响应体。

The other important implementation detail is that we return a ModelAndView — this is the body of the response, and it will allow us to set whatever is necessary on it.

另一个重要的实现细节是,我们返回一个ModelAndView–这是响应的主体,它将允许我们对其进行任何必要的设置。

This approach is a consistent and easily configurable mechanism for the error handling of a Spring REST Service.

这种方法对于Spring REST服务的错误处理是一种一致的、易于配置的机制。

It does, however, have limitations: It’s interacting with the low-level HtttpServletResponse and fits into the old MVC model that uses ModelAndView, so there’s still room for improvement.

然而,它确实有局限性。它与低级别的HtttpServletResponse进行交互,并且适合使用ModelAndView的旧MVC模型,所以仍有改进的余地。

4. Solution 3: @ControllerAdvice

4.解决方案3:@ControllerAdvice

Spring 3.2 brings support for a global @ExceptionHandler with the @ControllerAdvice annotation.

Spring 3.2带来了对全局@ExceptionHandler @ControllerAdvice注释的支持。

This enables a mechanism that breaks away from the older MVC model and makes use of ResponseEntity along with the type safety and flexibility of @ExceptionHandler:

这实现了一种机制,打破了旧的MVC模型,并利用ResponseEntity以及@ExceptionHandler的类型安全和灵活性。

@ControllerAdvice
public class RestResponseEntityExceptionHandler 
  extends ResponseEntityExceptionHandler {

    @ExceptionHandler(value 
      = { IllegalArgumentException.class, IllegalStateException.class })
    protected ResponseEntity<Object> handleConflict(
      RuntimeException ex, WebRequest request) {
        String bodyOfResponse = "This should be application specific";
        return handleExceptionInternal(ex, bodyOfResponse, 
          new HttpHeaders(), HttpStatus.CONFLICT, request);
    }
}

The@ControllerAdvice annotation allows us to consolidate our multiple, scattered @ExceptionHandlers from before into a single, global error handling component.

@ControllerAdvice注解允许我们将之前的多个分散的@ExceptionHandlers整合成一个单一的、全局的错误处理组件。

The actual mechanism is extremely simple but also very flexible:

实际机制极其简单,但也非常灵活。

  • It gives us full control over the body of the response as well as the status code.
  • It provides mapping of several exceptions to the same method, to be handled together.
  • It makes good use of the newer RESTful ResposeEntity response.

One thing to keep in mind here is to match the exceptions declared with @ExceptionHandler to the exception used as the argument of the method.

这里需要记住的一点是,将用@ExceptionHandler声明的异常与作为方法参数的异常相匹配。

If these don’t match, the compiler will not complain — no reason it should — and Spring will not complain either.

如果这些不匹配,编译器不会抱怨–没有理由要抱怨–而Spring也不会抱怨。

However, when the exception is actually thrown at runtime, the exception resolving mechanism will fail with:

然而,当异常在运行时被实际抛出时,异常解析机制将以失败。

java.lang.IllegalStateException: No suitable resolver for argument [0] [type=...]
HandlerMethod details: ...

5. Solution 4: ResponseStatusException (Spring 5 and Above)

5.解决方案4:ResponseStatusException(Spring 5及以上)

Spring 5 introduced the ResponseStatusException class.

Spring 5引入了ResponseStatusException类。

We can create an instance of it providing an HttpStatus and optionally a reason and a cause:

我们可以创建一个实例,提供一个HttpStatus以及可选的reasoncause

@GetMapping(value = "/{id}")
public Foo findById(@PathVariable("id") Long id, HttpServletResponse response) {
    try {
        Foo resourceById = RestPreconditions.checkFound(service.findOne(id));

        eventPublisher.publishEvent(new SingleResourceRetrievedEvent(this, response));
        return resourceById;
     }
    catch (MyResourceNotFoundException exc) {
         throw new ResponseStatusException(
           HttpStatus.NOT_FOUND, "Foo Not Found", exc);
    }
}

What are the benefits of using ResponseStatusException?

使用ResponseStatusException的好处是什么?

  • Excellent for prototyping: We can implement a basic solution quite fast.
  • One type, multiple status codes: One exception type can lead to multiple different responses. This reduces tight coupling compared to the @ExceptionHandler.
  • We won’t have to create as many custom exception classes.
  • We have more control over exception handling since the exceptions can be created programmatically.

And what about the tradeoffs?

那么权衡利弊呢?

  • There’s no unified way of exception handling: It’s more difficult to enforce some application-wide conventions as opposed to @ControllerAdvice, which provides a global approach.
  • Code duplication: We may find ourselves replicating code in multiple controllers.

We should also note that it’s possible to combine different approaches within one application.

我们还应该注意到,在一个应用程序中结合不同的方法是可能的。

For example, we can implement a @ControllerAdvice globally but also ResponseStatusExceptions locally.

例如,我们可以在全球范围内实现一个@ControllerAdvice,但也可以在本地实现ResponseStatusExceptions。

However, we need to be careful: If the same exception can be handled in multiple ways, we may notice some surprising behavior. A possible convention is to handle one specific kind of exception always in one way.

然而,我们需要小心。如果同一个异常可以用多种方式处理,我们可能会注意到一些令人惊讶的行为。一个可能的惯例是,总是以一种方式处理一种特定的异常。

For more details and further examples, see our tutorial on ResponseStatusException.

更多的细节和进一步的例子,请参阅我们的关于ResponseStatusException的教程。

6. Handle the Access Denied in Spring Security

6.在Spring Security中处理拒绝访问的情况

The Access Denied occurs when an authenticated user tries to access resources that he doesn’t have enough authorities to access.

当一个经过认证的用户试图访问他没有足够权限访问的资源时,就会发生访问被拒绝。

6.1. REST and Method-Level Security

6.1.REST和方法级安全

Finally, let’s see how to handle Access Denied exception thrown by method-level security annotations – @PreAuthorize, @PostAuthorize, and @Secure.

最后,让我们看看如何处理由方法级安全注解抛出的访问拒绝异常–@PreAuthorize, @PostAuthorize, 和@Secure

Of course, we’ll use the global exception handling mechanism that we discussed earlier to handle the AccessDeniedException as well:

当然,我们也会使用前面讨论的全局异常处理机制来处理AccessDeniedException

@ControllerAdvice
public class RestResponseEntityExceptionHandler 
  extends ResponseEntityExceptionHandler {

    @ExceptionHandler({ AccessDeniedException.class })
    public ResponseEntity<Object> handleAccessDeniedException(
      Exception ex, WebRequest request) {
        return new ResponseEntity<Object>(
          "Access denied message here", new HttpHeaders(), HttpStatus.FORBIDDEN);
    }
    
    ...
}

7. Spring Boot Support

7.Spring Boot支持

Spring Boot provides an ErrorController implementation to handle errors in a sensible way.

Spring Boot提供了一个ErrorController 实现,以合理的方式处理错误。

In a nutshell, it serves a fallback error page for browsers (a.k.a. the Whitelabel Error Page) and a JSON response for RESTful, non-HTML requests:

简而言之,它为浏览器提供一个回退错误页面(又称白标错误页面),为RESTful、非HTML请求提供一个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"
}

As usual, Spring Boot allows configuring these features with properties:

像往常一样,Spring Boot允许用属性来配置这些功能。

  • server.error.whitelabel.enabled: can be used to disable the Whitelabel Error Page and rely on the servlet container to provide an HTML error message
  • server.error.include-stacktrace: with an always value; includes the stacktrace in both the HTML and the JSON default response
  • server.error.include-message: since version 2.3, Spring Boot hides the message field in the response to avoid leaking sensitive information; we can use this property with an always value to enable it

Apart from these properties, we can provide our own view-resolver mapping for /error, overriding the Whitelabel Page.

除了这些属性之外,我们可以为/错误提供我们自己的视图解析器映射,覆盖白标页。

We can also customize the attributes that we want to show in the response by including an ErrorAttributes bean in the context. We can extend the DefaultErrorAttributes class provided by Spring Boot to make things easier:

我们还可以通过在上下文中包含一个ErrorAttributesbean来定制我们想在响应中显示的属性。我们可以扩展Spring Boot提供的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;
    }
}

If we want to go further and define (or override) how the application will handle errors for a particular content type, we can register an ErrorController bean.

如果我们想进一步定义(或覆盖)应用程序将如何处理特定内容类型的错误,我们可以注册一个ErrorControllerbean。

Again, we can make use of the default BasicErrorController provided by Spring Boot to help us out.

同样,我们可以利用Spring Boot提供的默认BasicErrorController来帮助我们。

For example, imagine we want to customize how our application handles errors triggered in XML endpoints. All we have to do is define a public method using the @RequestMapping, and stating it produces application/xml media type:

例如,假设我们想自定义我们的应用程序如何处理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) {
        
    // ...

    }
}

Note: here we’re still relying on the server.error.* Boot properties we might have been defined in our project, which are bound to the ServerProperties bean.

注意:这里我们仍然依赖于我们可能在项目中定义的server.error.* Boot属性,这些属性被绑定到ServerPropertiesbean。

8. Conclusion

8.结论

This article discussed several ways to implement an exception handling mechanism for a REST API in Spring, starting with the older mechanism and continuing with the Spring 3.2 support and into 4.x and 5.x.

本文讨论了在Spring中为REST API实现异常处理机制的几种方法,从较早的机制开始,一直到Spring 3.2支持,再到4.x和5.x。

As always, the code presented in this article is available over on GitHub.

一如既往,本文介绍的代码可在GitHub上获得over

For the Spring Security-related code, you can check the spring-security-rest module.

关于Spring安全相关的代码,你可以查看spring-security-rest模块。