【巨人的肩膀】Spring Rest 调用的异常处理

概述

本文将说明如何对使用 REST API 的 Spring 实现异常处理

  • Spring在不同版本中对异常处理的支持
  1. Spring 3.2 之前,在 Spring MVC 应用程序中处理异常的两种主要方法是 HandlerExceptionResolver 或 @ExceptionHandler 注解。两者都有明显的缺点
  2. Spring 3.2 开始,增加了 @ControllerAdvice 注解,以解决前两个解决方案的局限性,并在整个应用程序中促进统一的异常处理
  3. Spring 5 引入了 ResponseStatusException 类-一种在 REST API 中进行基本错误处理的快速方法

所有这些异常处理有一个共同点:它们很好地处理了关注点分离。即在问题关注点抛出异常,然后独立地处理异常

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

第一个解决方案适用于 @Controller 级别。 我们将定义一个处理异常的方法,并使用 @ExceptionHandler 对其进行注释:

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

}

这种方法有一个很大的缺点:只有在 Controller 添加 @ExceptionHandler 注解的方法才会生效,而不是全局定义

我们可以通过让所有 Controller 扩展 Base Controller 类来解决此限制。但是,对于无论出于何种原因都无法实现的应用程序,此解决方案可能会成为问题。例如,controller 可能已经从另一个基类扩展了,该基类可能在另一个 jar 中,或者不能直接修改,或者它们本身也不能直接修改

接下来,我们将讨论另一种解决异常处理问题的方法-一种全局的方法,不包括对现有工件(如Controllers)的任何更改

解决方案2:HandlerExceptionResolver

第二种解决方案是定义 HandlerExceptionResolver。这将解决应用程序引发的所有异常。它还将使我们能够在 REST API 中实现统一的异常处理机制

在设定自定义解析器之前,让我们看一下现有的实现

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

  • DefaultHandlerExceptionResolver
    该解析器在 Spring 3.0 中引入,默认情况下在 DispatcherServlet 中启用。它用于将标准 Spring 异常解析为其对应的 HTTP 状态代码,即客户端错误 4xx 和服务器错误 5xx 状态代码。这是它处理的 Spring Exception 的完整列表,以及它们如何映射到状态代码

ExceptionHTTP Status Code
BindException400 (Bad Request)
ConversionNotSupportedException500 (Internal Server Error)
HttpMediaTypeNotAcceptableException406 (Not Acceptable)
HttpMediaTypeNotSupportedException415 (Unsupported Media Type)
HttpMessageNotReadableException400 (Bad Request)
HttpMessageNotWritableException500 (Internal Server Error)
HttpRequestMethodNotSupportedException405 (Method Not Allowed)
MethodArgumentNotValidException400 (Bad Request)
MissingServletRequestParameterException400 (Bad Request)
MissingServletRequestPartException400 (Bad Request)
NoSuchRequestHandlingMethodException404 (Not Found)
TypeMismatchException400 (Bad Request)

虽然它确实正确设置了响应的状态码,但是一个限制是它没有对响应的主体设置任何内容。对于 REST API,状态码实际上是不足以向客户端提供的信息,响应也必须具有主体,以允许应用程序提供有关故障的其他信息。可以通过配置视图解析并通过 ModelAndView 呈现错误内容来解决此问题,但是解决方案显然不是最佳的。这就是为什么 Spring 3.2 引入了一个更好的选项的原因,我们将在后面的部分中进行讨论

  • ResponseStatusExceptionResolver
    此解析器也在 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 在处理 response body 方面受到限制-它确实将 Status Code 映射到 response 上,但 body 仍为 null

  • SimpleMappingExceptionResolver 和 AnnotationMethodHandlerExceptionResolver
    SimpleMappingExceptionResolver 已经停止使用相当长的一段时间。它来自较早的 Spring MVC 模型,与 REST 服务无关。我们基本上使用它来映射异常类名称以查看名称
    AnnotationMethodHandlerExceptionResolver 在 Spring 3.0 中引入处理过的异常 @ExceptionHandler 注释但已被替代为 ExceptionHandlerExceptionResolver 在 Spring 3.2 中

  • 自定义HandlerExceptionResolver
    DefaultHandlerExceptionResolver 和 ResponseStatusExceptionResolver 的组合在为 Spring RESTful 服务提供良好的错误处理机制方面有很长的路要走
    如前所述,不利方面是:no control over the body of the response

理想情况下,我们希望能够输出 JSON 或 XML,具体取决于客户端要求的格式(通过 Accept 标头),仅此一项就证明创建一个新的自定义异常解析器是合理的:

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

这里需要注意的细节是:

  1. 我们可以访问请求本身,因此我们可以考虑客户端发送的 Accept 标头的值。例如,如果客户端要求输入 application/json,那么在出现错误情况的情况下,我们要确保我们返回一个以 application/json 编码的响应正文
  2. 我们返回 ModelAndView-这是响应的主体,它将使我们能够设置所需的内容

这种方法是用于 Spring REST 服务的错误处理的一致且易于配置的机制,但是,它确实有局限性:它与低级 HtttpServletResponse 进行交互并适合使用 ModelAndView 的旧 MVC 模型,因此仍有改进的空间

解决方案3:@ControllerAdvice

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 一样定义单例或全局的异常处理组件

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

  1. 它使我们可以完全控制 response body 以及 status code
  2. 它提供了几种异常到同一方法的映射,使之可以统一处理
  3. 它充分利用了最新的 RESTful ResposeEntity 响应
  4. 这里要记住的一件事是将 @ExceptionHandler 声明的异常与用作方法参数的异常进行匹配

如果这些不匹配,编译器将不会 complain(我也不知道这个词该怎么翻译,本意是抱怨,但抱怨肯定不合适),但是,当实际在运行时引发异常时,异常解析机制将因以下原因而失败:

    java.lang.IllegalStateException: No suitable resolver for argument [0] [type=...]
    HandlerMethod details: ...
批注
1. @Controller+@ExceptionHandler 优先级最高,其次是 @ControllerAdvice+@ExceptionHandler,最后才是 HandlerExceptionResolver,说明假设三种方式并存的情况 优先级越高的越先选择,而且被一个捕获处理了就不去执行其他的
2. 可以捕捉异常后做自定义处理(例如钉钉告警等),然后继续抛出

解决方案4:ResponseStatusException(Spring 5及更高版本)

Spring 5 引入了 ResponseStatusException 类。我们可以创建它的一个实例,提供 HttpStatus 以及可选的原因和原因:

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

    1. 出色的原型制作:我们可以很快实现基本解决方案
    2. 一种类型,多种状态代码:一种异常类型可以导致多种不同的响应。与 @ExceptionHandler 相比,这减少了紧密耦合
    3. 我们将不必创建那么多的自定义异常类
    4. 由于可以通过编程方式创建异常,因此我们对异常处理有了更多的控制
  • 不足

    1. 没有统一的异常处理方法:与提供全局方法的 @ControllerAdvice 相比,强制实施某些应用程序范围的约定更加困难
    2. 代码复制:我们可能会发现自己在多个控制器中复制代码
  • 额外注意

    1. 我们可以在一个应用程序中组合不同的方法。我们可以全局实现 @ControllerAdvice,也可以局部实现 ResponseStatusException
    2. 如果可以以多种方式处理相同的异常,我们可能会注意到一些令人惊讶的行为。一种可能的约定是始终以一种方式处理一种特定类型的异常

有关更多详细信息和更多示例,请参见我们的 ResponseStatusException教程
我全粘过来了…

  • ResponseStatusException教程
    • ResponseStatus
      • 在深入研究 ResponseStatusException 之前,让我们快速看一下 @ResponseStatus 注解。该注解是在 Spring 3 中引入的,用于将 HTTP 状态代码应用于 HTTP 响应,我们可以使用 @ResponseStatus 注解来设置 HTTP 响应中的状态和原因
            @ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "Actor Not Found")
            public class ActorNotFoundException extends Exception {
                // ...
            }
        
      • 如果在处理 HTTP 请求时抛出此异常,则响应将包括此批注中指定的 HTTP 状态
      • @ResponseStatus 方法的一个缺点是,它与异常会产生紧密耦合。在我们的示例中,所有类型为 ActorNotFoundException 的异常都会在响应中生成相同的错误消息和状态代码
    • ResponseStatusException
      • ResponseStatusException 是 @ResponseStatus 的编程替代方案,并且是用于将状态代码应用于 HTTP 响应的异常的基类。它是 RuntimeException,因此不需要在方法签名中显式添加
      • Spring 提供了 3 个构造函数来生成 ResponseStatusException:
            ResponseStatusException(HttpStatus status)
            ResponseStatusException(HttpStatus status, java.lang.String reason)
            ResponseStatusException(
                // an HTTP status set to HTTP response
                HttpStatus status, 
                // a message explaining the exception set to HTTP response
                java.lang.String reason, 
                // a Throwable cause of the ResponseStatusException
                java.lang.Throwable cause
            )
        
      • 注意:在 Spring 中,HandlerExceptionResolver 拦截并处理任何引发的但未由 Controller 处理的异常,这些处理程序之一,ResponseStatusExceptionResolver,查找 @ResponseStatus 注释的任何 ResponseStatusException 或未捕获的异常,然后提取 HTTP 状态代码和原因,并将其包括在 HTTP 响应中
      • ResponseStatusException 好处
        • 首先,可以分别处理相同类型的异常,并且可以在响应中设置不同的状态代码,从而减少紧密耦合
        • 其次,它避免了创建不必要的额外异常类
        • 最后,由于可以以编程方式创建异常,因此它提供了对异常处理的更多控制
    • 例子
      • 产生 ResponseStatusException
        • 现在,让我们来看一个生成 ResponseStatusException 的示例:
              @GetMapping("/actor/{id}")
              public String getActorName(@PathVariable("id") int id) {
                  try {
                      return actorService.getActor(id);
                  } catch (ActorNotFoundException ex) {
                      throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Actor Not Found", ex);
                  }
              }
          
        • Spring Boot 提供了默认的 /error 映射,返回带有 HTTP 状态和异常消息的 JSON 响应
              $ curl -i -s -X GET http://localhost:8080/actor/8
              HTTP/1.1 404
              Content-Type: application/json;charset=UTF-8
              Transfer-Encoding: chunked
              Date: Sun, 28 Jan 2018 19:48:10 GMT
              
              {
                  "timestamp": "2018-01-28T19:48:10.471+0000",
                  "status": 404,
                  "error": "Not Found",
                  "message": "Actor Not Found",
                  "path": "/actor/8"
              }
          
      • 不同的状态码–相同的异常类型
        • 现在,让我们看看在引发相同类型的异常时如何将不同的状态代码设置为 HTTP 响应:
              @PutMapping("/actor/{id}/{name}")
              public String updateActorName(@PathVariable("id") int id, @PathVariable("name") String name) {
                  try {
                      return actorService.updateActor(id, name);
                  } catch (ActorNotFoundException ex) {
                      throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Provide correct Actor Id", ex);
                  }
              }
          
        • 响应的外观如下:
              $ curl -i -s -X PUT http://localhost:8080/actor/8/BradPitt
              HTTP/1.1 400
              ...
              {
                  "timestamp": "2018-02-01T04:28:32.917+0000",
                  "status": 400,
                  "error": "Bad Request",
                  "message": "Provide correct Actor Id",
                  "path": "/actor/8/BradPitt"
              }
          
          

处理 Spring Security 中拒绝的访问

当通过身份验证的用户尝试访问他没有足够权限访问的资源时,将发生“访问被拒绝”

  • MVC-自定义错误页面
    首先,让我们看一下该解决方案的 MVC 风格,并了解如何为 Access Denied 自定义错误页面

XML 配置:

    <http>
        <intercept-url pattern="/admin/*" access="hasAnyRole('ROLE_ADMIN')"/>   
        ... 
        <access-denied-handler error-page="/my-error-page" />
    </http>

Java 配置:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/admin/*").hasAnyRole("ROLE_ADMIN")
            ...
            .and()
            .exceptionHandling().accessDeniedPage("/my-error-page");
    }

当用户尝试在没有足够权限的情况下访问资源时,他们将被重定向到“/my-error-page”

  • 自定义 AccessDeniedHandler
    接下来,让我们看看如何编写自定义 AccessDeniedHandler:
    @Component
    public class CustomAccessDeniedHandler implements AccessDeniedHandler {
    
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException ex) throws IOException, ServletException {
            response.sendRedirect("/my-error-page");
        }
    }

现在,我们使用 XML 配置对其进行配置:

    <http>
        <intercept-url pattern="/admin/*" access="hasAnyRole('ROLE_ADMIN')"/> 
        ...
        <access-denied-handler ref="customAccessDeniedHandler" />
    </http>

或者使用 Java 配置:

    @Autowired
    private CustomAccessDeniedHandler accessDeniedHandler;
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/admin/*").hasAnyRole("ROLE_ADMIN")
            ...
            .and()
            .exceptionHandling().accessDeniedHandler(accessDeniedHandler)
    }

请注意,如何在 CustomAccessDeniedHandler 中通过重定向或显示自定义错误消息来自定义响应

  • REST和方法级安全性
    最后,让我们看看如何处理方法级安全性 @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);
        }
        
        ...
    }

Spring Boot支持

Spring Boot提供了一个 ErrorController 实现来以明智的方式处理错误

简而言之,它为浏览器提供一个后备错误页面(又称Whitelabel错误页面),并为 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:
        # 可用于禁用Whitelabel错误页面并依靠servlet容器提供HTML错误消息
        enabled:
        # 具有始终 值;在HTML和JSON默认响应中都包含stacktrace
        include-stacktrace:

除了这些属性,我们还可以为 /error 提供我们自己的 view-resolver 映射,以覆盖 Whitelabel 页面

我们还可以通过在上下文中包含 ErrorAttributes 的 bean 来定制要在响应中显示的属性 。我们可以扩展 Spring Boot 提供的 DefaultErrorAttributes 类以使事情变得更容易:

    @Component
    public class MyCustomErrorAttributes extends DefaultErrorAttributes {
    
        @Override
        public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
            Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, includeStackTrace);
            errorAttributes.put("locale", webRequest.getLocale().toString());
            errorAttributes.remove("error");
    
            //...
    
            return errorAttributes;
        }
    }

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

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

例如,假设我们要自定义应用程序如何处理 XML 端点中触发的错误。我们要做的就是使用 @RequestMapping 定义一个公共方法 ,并声明它产生 application/xml媒体类型:

    @Component
    public class MyErrorController extends BasicErrorController {
    
        public MyErrorController(ErrorAttributes errorAttributes) {
            super(errorAttributes, new ErrorProperties());
        }
    
        @RequestMapping(produces = MediaType.APPLICATION_XML_VALUE)
        public ResponseEntity<Map<String, Object>> xmlError(HttpServletRequest request) {
            
        // ...
    
        }
    }

结论

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

与往常一样,本文提供的代码可从GitHub上获得

对于与 Spring Security 相关的代码,您可以检查spring-security-rest模块

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值