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

栏目: 编程工具 · 发布时间: 5年前

内容简介:1. 概览本文将举例说明

1. 概览

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

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

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

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

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

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

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

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

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

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Go程序设计语言

Go程序设计语言

艾伦 A. A. 多诺万 / 李道兵、高博、庞向才、金鑫鑫、林齐斌 / 机械工业出版社 / 2017-5 / 79

本书由《C程序设计语言》的作者Kernighan和谷歌公司Go团队主管Alan Donovan联袂撰写,是学习Go语言程序设计的指南。本书共13章,主要内容包括:Go的基础知识、基本结构、基本数据类型、复合数据类型、函数、方法、接口、goroutine、通道、共享变量的并发性、包、go工具、测试、反射等。 本书适合作为计算机相关专业的教材,也可供Go语言爱好者阅读。一起来看看 《Go程序设计语言》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试