51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

在 Spring 应用中为 REST API 实现异常处理

1、概览 {#1概览}

本文将地带你了解如何在 Spring 中为 REST API 实现异常处理。

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

自 3.2 以来,可以使用 @ControllerAdvice 注解来解决前两种解决方案的局限性,并促进整个应用中统一的异常处理。

Spring 5 引入了 ResponseStatusException 类,一种在 REST API 中进行基本错误处理的快速方法。

所有这些都有一个共同点:它们都很好地处理了关注点的分离。应用通常可以抛出异常来表示某种失败,然后再单独进行处理。

2、解决方案 1:Controller 级的 @ExceptionHandler {#2解决方案-1controller-级的-exceptionhandler}

第一种解决方案适用于 @Controller 层面。定义一个处理异常的方法,并用 @ExceptionHandler 进行注解:

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

这种方法有一个很大的缺点:@ExceptionHandler 注解方法仅对特定 Controller 有效,而不是对整个应用全局有效。当然,可以将其添加到每个 Controller 中,但这并不适合作为通用的异常处理机制。

也可以通过让所有 Controller 都继承一个 Base Controller 类来绕过这一限制。

然而,对于某些原因无法实现上述方法的应用来说,这种解决方案可能会成为一个问题。例如,Controller 可能已经从另一个 Base 类继承而来,而该 Base 类可能在另一个 Jar 中或不可直接修改,或者 Controller 本身不可直接修改。

3、解决方案 2:HandlerExceptionResolver {#3解决方案-2handlerexceptionresolver}

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

在使用自定义解析器之前,先来了解一下现有的实现。

3.1、ExceptionHandlerExceptionResolver {#31exceptionhandlerexceptionresolver}

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

3.2、DefaultHandlerExceptionResolver {#32defaulthandlerexceptionresolver}

该 Resolver 在 Spring 3.0 中引入,默认在 DispatcherServlet 中启用。

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

| Exception | HTTP Status Code | |-------------------------------------------|------------------------------| | BindException | 400 (Bad Request) | | ConversionNotSupportedException | 500 (Internal Server Error) | | HttpMediaTypeNotAcceptableException | 406 (Not Acceptable) | | HttpMediaTypeNotSupportedException | 415 (Unsupported Media Type) | | HttpMessageNotReadableException | 400 (Bad Request) | | HttpMessageNotWritableException | 500 (Internal Server Error) | | HttpRequestMethodNotSupportedException | 405 (Method Not Allowed) | | MethodArgumentNotValidException | 400 (Bad Request) | | MissingServletRequestParameterException | 400 (Bad Request) | | MissingServletRequestPartException | 400 (Bad Request) | | NoSuchRequestHandlingMethodException | 404 (Not Found) | | TypeMismatchException | 400 (Bad Request) |

虽然它能正确设置响应的状态码,但有一个局限性,那就是它不能为响应体设置任何消息。对于 REST API 来说,状态代码确实不足以向客户端提供足够的信息,因此响应还必须有一个正文,以便应用提供有关故障的其他信息。

这虽然可以通过配置视图解析器和通过 ModelAndView 渲染错误内容来解决,但该解决方案显然不是最佳的。

3.3、ResponseStatusExceptionResolver {#33responsestatusexceptionresolver}

该 Resolver 也在 Spring 3.0 中引入,并在 DispatcherServlet 中默认启用。

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

这样的自定义异常可能如下所示:

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

DefaultHandlerExceptionResolver 相同,该 Resolver 处理响应体的方式受到限制 - 它确实会在响应上映射状态码,但 Body (响应体)仍为 null

3.4、自定义 HandlerExceptionResolver {#34自定义-handlerexceptionresolver}

DefaultHandlerExceptionResolverResponseStatusExceptionResolver 的组合可为 Spring RESTful 服务提供良好的错误处理机制。缺点是,如前所述,无法控制响应体。

理想情况下,我们希望能够响应 JSONXML ,具体取决于客户期望的格式(通过 Accept Header)。

创建一个新的自定义异常解析器。

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

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

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

另一个重要的实现细节是返回了一个 ModelAndView 对象,这是响应 Body,它允许我们对其进行必要的设置。

这种方法为 Spring REST 服务的错误处理提供了一致且易于配置的机制。

然而,它也有一些限制:它与底层的 HttpServletResponse 进行交互,并适用于使用 ModelAndView 的旧 MVC 模型,因此仍有改进的空间。

4、解决方案 3:@ControllerAdvice {#4解决方案-3controlleradvice}

Spring 3.2 为带有 @ControllerAdvice 注解的全局 @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);
    }
}

@ControllerAdvice 注解允许将之前多个分散的 @ExceptionHandler 整合为一个单一的全局错误处理组件。

实际机制非常简单,但也非常灵活:

  • 它能够完全控制响应体和状态码。
  • 它将多个异常映射到同一个方法中,以便一起处理。
  • 它充分利用了较新的 RESTful ResposeEntity 响应。

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

如果它们不匹配,编译不会异常,Spring 启动也不会异常。但是,当异常在运行时实际抛出时,异常解析机制将失败,并显示如下错误消息:

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

5、解决方案 4:ResponseStatusException(Spring 5 及其以上) {#5解决方案-4responsestatusexceptionspring-5-及其以上}

Spring 5 引入了 ResponseStatusException 类。

我们可以创建一个实例,提供 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);
    }
}

使用 ResponseStatusException 有什么好处?

  • 非常适合原型开发:可以快速实现一个基本的解决方案。
  • 一种类型,多种状态代码: 一种异常类型可导致多种不同的响应。与 @ExceptionHandler 相比,这减少了紧密耦合。
  • 不必创建那么多自定义异常类
  • 由于可以通过编程式创建异常,因此对异常处理的控制能力更强。

弊端呢?

  • 没有统一的异常处理方式:相比之下,@ControllerAdvice 提供了一种全局的方法,更难以强制执行一些应用范围的约定。
  • 代码重复:可能会在多个 Controller 中重复编写代码。

注意,在一个应用中可以结合不同的方法。

例如,可以在全局范围内实现 @ControllerAdvice,但也可以在本地范围内实现 ResponseStatusException

不过,需要小心谨慎: 如果可以用多种方式处理同一异常,可能会发现一些意外的行为。一种可能的约定俗成的做法是,总是以一种方式处理一种特定的异常

6、处理 Spring Security 中的 "Access Denied"(拒绝访问) {#6处理-spring-security-中的-access-denied拒绝访问}

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

6.1、REST 和方法级 Security {#61rest-和方法级-security}

最后,来看看如何处理方法级 Security 注解 @PreAuthorize@PostAuthorize@Secure 抛出的 "Access Denied" 异常。

当然,还是使用之前讨介绍的全局异常处理机制来处理 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 中的支持 {#7spring-boot-中的支持}

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

简而言之,它为浏览器提供一个基础的错误页面(又称 "Whitelabel Error Page"),并为 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"
}

Spring Boot 允许使用属性配置这些功能:

  • server.error.whitelabel.enabled:可用于禁用 "Whitelabel Error Page",并依靠 servlet 容器提供 HTML 错误消息。
  • server.error.include-stacktrace:设置为 always 时,在 HTML 和 JSON 默认响应中包含栈跟踪信息。
  • server.error.include-message:自 2.3 版本起,Spring Boot 隐藏响应中的 message 字段,以避免泄露敏感信息;可以使用该属性并将其设置为 always 来启用该功能。

除了这些属性外,还可以为 /error 提供自己的视图解析映射,覆盖 "Whitelabel Page"。

还可以通过在 Context 中包含一个 ErrorAttributes Bean 来自定义要在响应中显示的属性。可以继承 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;
    }
}

如果想进一步定义(或重写)应用如何处理特定内容类型的错误,可以注册 ErrorController Bean。

同样,可以继承 Spring Boot 提供的默认 BasicErrorController 来帮助我们处理异常。

例如,假设想自定义应用如何处理 XML 端点中触发的错误。所要做的就是使用 @RequestMapping 定义一个 public 方法,并指定它生成的是 application/xml 媒体类型(Media Type):

@Component
public class MyErrorController extends BasicErrorController {

    public MyErrorController(
      ErrorAttributes errorAttributes, ServerProperties serverProperties) {
        super(errorAttributes, serverProperties.getError());
    }

    @RequestMapping(produces = MediaType.APPLICATION_XML_VALUE) // application/xml
    public ResponseEntity<Map<String, Object>> xmlError(HttpServletRequest request) {
        
    // ...

    }
}

注意:这里仍然依赖于可能在 application.properties 中定义的 server.error. 属性,这些属性绑定在 ServerProperties Bean 上。

8、总结 {#8总结}

本文介绍了在 Spring 应用中为 REST API 实现异常处理的几种方法。


Ref:https://www.baeldung.com/exception-handling-for-rest-with-spring

赞(3)
未经允许不得转载:工具盒子 » 在 Spring 应用中为 REST API 实现异常处理