Spring REST 的异常处理机制

翻译自:
最近流行的前后端分离方法中,异常处理机制往往还是处于旧的MVC异常处理机制,很大的一个缺点就是没有给响应主体添加任何有效信息而返回给客户端,通常是要求客户端重定向到一个预先定义好的错误页面,这个不符合REST风格,此篇文章承前启后不仅概括了原有的MVC异常处理机制,也提出了新的REST异常处理机制。并且秉承了一贯的风格,每篇文章都配有可以实际应用的代码。这篇文章对我的帮助很大,所以也特别翻译过来,希望也能帮助到其他人。

文章共分为六个部分:

1. 概括

2. 通过controller的@ExceptionHandler实现异常处理

3. 通过HandlerExceptionResolve实现异常处理

4. 通过@ControllerAdvice实现异常处理(Spring 3.2及以上版本)

5. 关于spring security访问拒绝的处理

6. 小

1 概括

这篇文章将讲述在Spring REST 程序中如何进行异常处理。我们将讨论Spring 3.24.x版本以后所推荐的异常处理方法,同时也会对更低版本的异常处理做一些介绍。

Spring 3.2之前,主要有两种处理方法:HandlerExceptionResolver或者@ExceptionHandler。两种方法都存在明显的缺点。

3.2版本之后,我们有了新的注解@ControllerAdvice来解决前两种方法的不足。

所有这些都有一个共同点——它们很好地处理了关注的分离。应用程序可以正常地抛出异常来指示某种类型的失败,这些异常将被单独处理。


2 解决方法 1- Controller中的注解@ExceptionHandler

第一个方法我们会在Contoller中定义一个方法来处理异常,这个方法用@ExceptionHandler来注解

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

此方法有一个主要缺点——@ ExceptionHandler注释方法只对该特定控制器有效,而不是全局应用程序。当然,将它添加到每个控制器使得它不能很好地适用于一般的异常处理机制。              限制通常是通过让所有控制器扩展基本控制器类来实现的,然而,这对于应用程序来说可能是个问题,无论出于什么原因,控制器不能从这样的类扩展。例如,Controller可能已经扩展了其他的类,或者本身可能不能直接修改。

接下来,我们将研究解决异常处理问题的另一种方法——一个是全局的,不包括对现有构件(如Controllers)的任何更改。

3 解决方法 2 – 利用HandlerExceptionResolver

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

在进行自定义解析器之前,让我们先看看现有的实现方式。

   
   
   
   
   
   
   
   
   
 
 
   
   
   

3.1 ExceptionHandlerExceptionResolver

这个解析器是在Spring 3.1中引入的,并且在DispatcherServlet中默认启用。这实际上是前面介绍的@ExceptionHandler的核心组成部分。
3.2 DefaultHandlerExceptionResolver
这个解析器是在Spring 3中引入的,并且在DispatcherServlet中默认启用。它用于解决标准的Spring异常到其相应的HTTP状态代码,即客户端错误-4xx和服务器错误-5xx状态代码。下面是它处理的Spring异常的完整列表,以及如何将这些映射到状态代码。
HTTP状态代码
201 (Created).  
304 (Not Modified)  
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配置视图解析和渲染错误内容来解决,但是解决方案显然不是最优的——这就是为什么在Spring 3.2中可以使用更好的选项——我们将在本文后面的部分讨论。
3.3 ResponseStatusExceptionResolver

此解析器也在Spring3中引入,默认情况下在DispatcherServlet中启用。它的主要职责是使用在自定义异常上可用的@ ResponseStatus注释,并将这些异常映射到HTTP状态代码。             
这样的自定义异常看起来像:
@ResponseStatus(value = HttpStatus.NOT_FOUND)
publi 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 andAnnotationMethodHandlerExceptionResolver

SimpleMappingExceptionResolver已经存在了相当长的时间——它来自旧的Spring MVC模型,与REST服务不太相关。它用于映射异常类名称以视图的名称。             AnnotationMethodHandlerExceptionResolverSpring3中引入,通过@ ExceptionHandler注释来处理异常,但是Spring 3.2已被ExceptionHandlerExceptionResolver替代。

3.5 Custom HandlerExceptionResolver

DefaultHandlerExceptionResolverResponseStatusExceptionResolver解决程序的组合对于提供一个良好的错误处理机制对于Spring REST服务有很大的帮助。缺点是,正如前面提到的那样,没有控制响应主题内容。理想情况下,我们希望能够输出JSONXML,这取决于客户端请求的格式(通过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报头的值。例如,如果客户端请求应用程序/JSON,那么,在错误情况下,应用程序仍然应该返回用应用程序/JSON编码的响应体。另一个重要的实现细节是返回一个ModelAndView——这是响应的主体,它将允许应用程序设置它所需的任何内容。              这种方法是一个一致的和易于配置的Spring REST异常处理机制。但是它有局限性:它与低级HtttpServletResponse响应交互,它适合使用ModelAndView的旧MVC模型——因此仍有改进的余地。

4 新的解决方法3 利用@ControllerAdvice (Spring 3.2 及以上版本)

Spring 3.2为新的@ControllerAdvice建议注释提供了全局@ ExceptionHandler的支持。这使得异常处理机制摆脱了旧的MVC模型,并且使用了响应实体以及@ ExceptionHandler的类型安全性和灵活性。

新的注释允许从以前多个分散的@ ExceptionHandler合并成单个的全局错误处理组件。              实际机制非常简单,但也非常灵活:

l  它允许完全控制响应的主体以及状态代码。

l  它允许对同一方法中的几个异常进行映射,以便一起处理。          

l  它很好地利用了新的REST响应实体响应。

l  这里要记住的一件事是将声明的异常与@ ExceptionHandler相匹配,使用异常作为方法的参数。如果这些不匹配,编译器不会提示编译错误——没有理由,而且Spring也不会提示编译错误。

l  但是,当异常在运行时实际抛出时,异常解析机制将失败:

5 Spring Security中处理拒绝访问

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

5.1 MVC – Custom Error Page自定义错误页面

<http>
    <intercept-url pattern="/admin/*" access="hasAnyRole('ROLE_ADMIN')"/>   
    ... 
    <access-denied-handler error-page="/my-error-page" />
</http>
And the Java configuration:
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .antMatchers("/admin/*").hasAnyRole("ROLE_ADMIN")
        ...
        .and()
        .exceptionHandling().accessDeniedPage("/my-error-page");
}


当用户试图访问一个没有足够权限的资源时,它们将被重定向到“/My Error页面

5.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 配置来配置它:
<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中,我们可以通过重定向或显示自定义错误消息来自定义响应。

5.3 利用REST和方法的安全机制

最后,让我们看看如何处理方法级安全性@ PreAuthorize@ PostAuthorize@ Secure Access Denied

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

@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中实现RESTAPI的异常处理机制的方法,从旧的机制开始,并继续探讨Spring 3.2,以及44.1支持的新的机制。              

一如往常,本文中提供的代码(GITHUB)可以实际使用。这是一个基于Maven的项目,因此应该很容易导入和运行。



猜你喜欢

转载自blog.csdn.net/java_augur/article/details/80037033