译:如何使用Spring优雅地处理REST异常

2023-03-07 16:17:35 浏览数 (2)

原文链接:https://www.baeldung.com/exception-handling-for-rest-with-spring

作者: Eugen Paraschiv

译者: helloworldtang

目录

  • 1. 概览
  • 2. 使用控制器作用域的注解 @ExceptionHandler
  • 3. 使用 HandlerExceptionResolver
  • 4. 使用新注解 @ControllerAdvice (Spring 3.2及以上版本)
  • 5. 处理Spring Security中的拒绝访问
  • 6. 总结

1. 概览

本文将举例说明如何使用Spring来实现REST API的异常处理。我们将同时考虑Spring 3.2和4.x推荐的解决方案,同时也会考虑以前的解决方案。

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

在3.2之后,我们有了新的注解@ControllerAdvice来解决前两个解决方案的局限性。

所有这些都有一个共同点——它们很好地处理了关注点分离。应用程序可以像往常一样抛出异常以表示某种类型的故障——这些异常将被单独处理。

2. 解决方案 1 – 控制器作用域的注解 @ExceptionHandler

第一个解决方案是在@Controller作用域有效——我们将定义一个处理异常的方法,并给这个方法添加@ExceptionHandler注解:

代码语言:javascript复制
public class FooController{
    //...
    @ExceptionHandler({ CustomException1.class, CustomException2.class})
    public void handleException() {
        //
    }
}

这种方法有 一个很大的缺陷 ———添加了@ExceptionHandler注解的方法只针对特定的控制器,而不是全局的整个应用程序。当然,在每个控制器中都添加@ExceptionHandler 注解的办法使它无法很好的适应常规的异常处理机制。

@ExceptionHandler在作用域方面的缺陷通常是通过让所有控制器都扩展一个控制器基类的方式来解决——然而,对于应用程序来说,这可能是一个问题,因为不管出于什么原因,总有一些控制器不能从这个基控制器扩展。例如,这些控制器可能不能直接修改,或者一些控制器可能已经从别的基类扩展,而这个基类可能在另一个jar中或者不能直接修改。

接下来,我们将讨论另一种解决异常处理问题的方法——一种全局的、不包括对现有组件的任何更改。

3. 解决方案 2 – HandlerExceptionResolver

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

在使用自定义解析器之前,让我们回顾一下现有的异常解析器。

3.1. ExceptionHandlerExceptionResolver

这个解析器在Spring 3.1中引入,并且在 DispatcherServlet中是默认启用的。它实际上是前面介绍的@ExceptionHandler机制的核心组成部分。

3.2. DefaultHandlerExceptionResolver

DefaultHandlerExceptionResolver是在Spring 3.0中引入的,并且在DispatcherServlet中是默认启用的。它用于将Spring中的标准异常解析为对应的HTTP状态码,即客户端错误——4xx和服务器错误——5xx状态码。这是Spring异常的完整列表,以及这些异常对应的HTTP状态码。

虽然它确实正确地设置了响应的状态码,但有一个缺陷是它不会改变响应体。对于REST API来说,状态码实际上并没有足够的信息显示给客户端——响应也必须有一个响应体,以便服务器能够提供更多关于故障的信息。

这个缺陷可以通过ModelAndView配置视图解析和渲染错误内容来解决,但是这个解决方案很显然不是最理想的——这就是为什么在Spring 3.2中提供了更好的选择——我们将在本文的后半部分讨论这个问题。

3.3. ResponseStatusExceptionResolver

这个解析器也是在Spring 3.0中引入,并且在DispatcherServlet中是默认启用的。它的主要职责是根据自定义异常上配置的注解@ResponseStatus,将这些自定义异常映射到设定的HTTP状态码。

通过这个方式创建的一个自定义异常可能看起来是这样的:

代码语言:javascript复制
@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException() {
        super();
    }
    public ResourceNotFoundException(String message, Throwable cause) {
        super(message, cause);
    }
    public ResourceNotFoundException(String message) {
        super(message);
    }
    public ResourceNotFoundException(Throwable cause) {
        super(cause);
    }
}

与DefaultHandlerExceptionResolver一样,这个解析器在处理响应体方面是有缺陷的——它确实重新设定了响应的状态码,但是响应体仍然是空的。

3.4. SimpleMappingExceptionResolver和 AnnotationMethodHandlerExceptionResolver

SimpleMappingExceptionResolver 已经存在了相当长一段时间——它来自于较早的Spring MVC模型,与REST服务不太相关。它被用来映射异常类名到视图名。

在Spring 3.0中引入了AnnotationMethodHandlerExceptionResolver,通过注解@ExceptionHandler来处理异常,但是在Spring 3.2时已经被ExceptionHandlerExceptionResolver 废弃。

3.5. 自定义HandlerExceptionResolver

在为Spring RESTful 服务提供良好的错误处理机制方面,DefaultHandlerExceptionResolver和ResponseStatusExceptionResolver组合还有很长的路要走。缺陷是——正如前面提到的——无法控制响应体。

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

这就足以创建一个新的、自定义的异常解析器

代码语言:javascript复制
@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服务的异常处理来说,这种方法是一种一致且易于配置的机制。但是它有一些限制:它与低层的HtttpServletResponse交互,它适合使用ModelAndView的旧MVC模型——所以仍然有改进的空间。

4. 新的解决方案 3 – 使用新的注解 @ControllerAdvice (Spring 3.2及以上版本)

Spring 3.2使用新的注解@ControllerAdvice为全局的@ExceptionHandler提供支持。这就形成了一种脱离旧MVC模型的机制,使用ResponseEntity以及注解@ExceptionHandler的类型安全性和灵活性:

代码语言:javascript复制
@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合并到一个单一的、全局的错误处理组件中

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

  • 它允许对响应体和HTTP状态码进行完全控制
  • 它允许将几个异常映射到相同的方法,以便一起处理
  • 它充分利用了新的REST风格的 ResposeEntity响应

这里要特别注意一个细节,@ExceptionHandler声明的异常类要与其修饰方法的参数类型相匹配。如果这两个地方不匹配,编译器将不会提示——它没有理由去提示,Spring也不会提示。

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

代码语言:javascript复制
java.lang.IllegalStateException: No suitable resolver for argument [0] [type=...]
HandlerMethod details: ...

5. 处理Spring Security中拒绝访问

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

5.1. MVC – 自定义错误页

首先,让我们看一下MVC风格的解决方案,看看如何定制一个拒绝访问的错误页面:

使用XML配置

代码语言:javascript复制
<http>
    <intercept-url pattern="/admin/*" access="hasAnyRole('ROLE_ADMIN')"/>   
    ... 
    <access-denied-handler error-page="/my-error-page" />
</http>

使用Java配置

代码语言:javascript复制
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .antMatchers("/admin/*").hasAnyRole("ROLE_ADMIN")
        ...
        .and()
        .exceptionHandling().accessDeniedPage("/my-error-page");
}

当用户试图访问资源但没有足够的权限时,它们将被重定向到“/my-error-page“。

5.2. 自定义AccessDeniedHandler

接下来,让我们看看如何编写自定义AccessDeniedHandler:

代码语言:javascript复制
@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配置进行配置:

代码语言:javascript复制
<http>
    <intercept-url pattern="/admin/*" access="hasAnyRole('ROLE_ADMIN')"/> 
    ...
    <access-denied-handler ref="customAccessDeniedHandler" />
</http>

或者使用Java配置

代码语言:javascript复制
@Autowired
private CustomAccessDeniedHandler accessDeniedHandler;

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

请注意,在我们的 CustomAccessDeniedHandler中,我们可以通过重定向或显示一条自定义错误信息的方式来定制响应。

5.3. REST和方法级的安全性

最后,让我们看看如何处理方法级的安全性注解@PreAuthorize、@PostAuthorize和@Secure引发的拒绝访问。

当然,我们将使用之前讨论过的全局异常处理机制来处理新的AccessDeniedException:

代码语言:javascript复制
@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);
    }
    ...
}

6. 总结

本教程讨论了在Spring中实现REST API异常处理机制的几种方法,从旧的机制开始,然后是Spring 3.2中的,最后是4.x和5.x中的。

与往常一样,本文中提供的代码可以在Github上找到。这是一个基于Maven的项目,所以应该很容易导入和运行。

0 人点赞