欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Spring的REST API异常处理

程序员文章站 2022-05-22 09:41:57
...

Spring的REST错误处理-异常处理

1、概述

	本文将阐述如何使用REST API的spring实现异常处理。我们还将获得一下历史概述,并查看不同版本引入了那些新选项。
	在spring3.2之前,在spring MVC应用程序中处理异常主要有两种方法:HandlerExceptionResolver或@ExceptionHandler注解,两者都有明显的缺点。从3.2开始,spring有了@ControllerAdvice批注,已解决前两个方案的局限性,并在整个应用中促进统一异常处理。现在Spring5引入了ResponseStatusException类:一种在REST API中进行基本错误处理的快速方法。
	所有这些确实有一个共同点-它们很好的处理了关注点分离。该应用程序通常可以引发异常以指示某种类型的异常-异常将随后单独处理。
	最后,我们将看到springboot带来的好处,以及如何配置它以满足我们的需求。

2、解决方案1-控制器级别@ExceptionHandler

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

public class TestController {
	//...
	@ExceptionHandler({CustomException1.class, CustomException2.class})
	public void handlerException() {
	    //...
	}
}

这种方法有一个主要缺点-带@ExceptionHandler注释的方法仅对特定持有的Controller有效,而对整个应用程序无效,当然,将其添加到每个控制器使其不适用常规异常处理机制。
我们可以让所有的Controller扩展Base Controller类来解决此限制,但是,对于无论出于何种原因都无法实现的应用程序来说,这可能是个问题。例如,控制器可能已经从另一个基类扩展了,该基类可能在另一个jar中,或者不能直接修改,或者它们本身也不能直接修改。
接下来,我们将讨论另一种解决异常处理问题的方法-一种全局的方法,不包括对现有工件(如Controller)的任何更改。

3、解决方案2-HandlerExceptionResolver

第二种解决方案是定义HandlerExceptionResolver-这将解决应用程序引发的任何异常。它还将使我们能够在REST API中实现统一的异常处理机制。
在使用自定义解析器之前,让我们看一下现有的实现。

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

3.2 DefaultHandlerExceptionResolver
该解析器在spring3.0中引入,默认情况下在DispatcherServlet中启用。它用于将标准Spring异常解析为其对应的HTTP状态代码,即客户端错误-4xx和服务器错误-5xx代码。这是它处理Spring Exception的完整列表,以及它们如何映射带状态代码
Spring的REST API异常处理
尽管它正确设置了响应的状态码,但一个限制是它没有对响应的主体设置任何内容。对于REST API,状态码实际上是不足以向客户端提供信息-响应也必须具有主体,以允许应用程序提供有关故障的其他信息。
可以通过配置视图分辨率并通过ModelAndView呈现错误内容来解决此问题,但是解决方案不是最佳的。这就是为什么Spring 3.2引入了一个更好的选项的原因,我们将在后面的部分中进行讨论。

3.3 ResponseStatusExceptionResolver
此解析器也在Spring 3.0中引入,并且默认情况下在DispatchServlet中启用。它的主要职责是使用自定义异常上可用的@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相同,此解析器在处理响应主体方面受到限制-它确实将状态码映射到响应上,但主体仍为null。

3.4 SimpleMappingExceptionResolver和AnnotationMethodHandlerExceptionResolver
该SimpleMappingExceptionResolver已经有相当长的一段时间-它是上了年纪的Spring MVC模型出来,是不是一个REST服务非常相关的。我们基本上使用它来映射异常类名称以查看名称。
该AnnotationMethodHandlerExceptionResolver在Spring3.0中引入处理过的异常@ExceptionHandler注释但已被弃用ExceptionHandlerExceptionResolver如Spring3.2。

3.5 自定义HandlerExceptionResolver
DefaultHandlerExceptionResolver和ResponseStatusExceptionResolver的组合在为Spring RESTful服务提供良好错误处理机制方面还有很长的路要走。如前所述,不利方面是无法控制响应主体。
理想情况下,我们希望能够输出JOSN或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();
    }
}

这里需要注意一个细节,我们可以访问请求本身,因此我们可以考虑客户端发送的Accept标头的值。
例如,如果客户端要求提供application/json,则在出现错误时,我们需要确保我们返回一个以application/json编码的响应正文。
另一个重要的实现细节是,我们返回 ModelAndView -这是响应的主体,它使我们可以设置所需的内容。
这种方法是用于Spring REST Service的错误处理的一致且易于配置的机制。但是,它确实有局限性:它与低级HtttpServletResponse进行交互,并且适合使用ModelAndView的旧MVC模型-因此仍有改进的空间。

4、解决方案aaa@qq.com

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(Spring5以及更高)

Spring5引入了ResponseStatusException类。我们可以创建一个提供HttpStatus是实例,还可以选择一个reason and a cause:

@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提供全局方法要困难得多。
  • 代码复制:我们可能会发现自己在多个控制器中复制代码
    我们还应该注意,可以在一个应用程序中组合不同的方法。
    例如,我们可以 全局实现@ControllerAdvice,还可以 局部实现 ResponseStatusException。但是,我们需要注意:如果可以通过多种方式处理相同的异常,我们可能会注意到一些令人惊讶的行为。一种可能的约定是始终以一种方式处理一种特定类型的异常。

6、处理Spring Security中拒绝的访问

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

6.1 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 ”。

6.2 自定义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 Configuration对其进行配置:

<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中,我们可以通过重定向或显示自定义错误消息来根据需要自定义响应。

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

7、Spring Boot支持

Spring Boo提供了一个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.enable: 可用于禁用Whitelable错误页面并依靠servlet容器提供HTML错误消息
  • Server.error.include-stacktrace: 具有始终值,它在HTML和JSON默认响应中都包含stacktrace
    除了这些属性,我们还可以为错误提供我们自己的视图解析器映射,以覆盖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) {
        
    // ...
 
    }
}

8、结论

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