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

1. 概览java

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

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

在3.2以后,咱们有了新的注解@ControllerAdvice来解决前两个解决方案的局限性。服务器

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

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

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

public class FooController{ui

//...编码

@ExceptionHandler({ CustomException1.class, CustomException2.class})url

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状态码。

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

@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头)。

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

@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的类型安全性和灵活性:

@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也不会提示。

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

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

  2. HandlerMethod details: ...

5. 处理Spring Security中拒绝访问

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

5.1. MVC – 自定义错误页

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

使用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“。

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引起的拒绝访问。

固然,咱们将使用以前讨论过的全局异常处理机制来处理新的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);

}

...

}

写在最后:

码字不易看到最后了,那就点个关注呗,只收藏不点关注的都是在耍流氓! 关注并私信我“架构”,免费送一些Java架构资料,先到先得!

相关文章
相关标签/搜索