概述
本文将说明如何对使用 REST API 的 Spring 实现异常处理
- Spring在不同版本中对异常处理的支持
- Spring 3.2 之前,在 Spring MVC 应用程序中处理异常的两种主要方法是 HandlerExceptionResolver 或 @ExceptionHandler 注解。两者都有明显的缺点
- Spring 3.2 开始,增加了 @ControllerAdvice 注解,以解决前两个解决方案的局限性,并在整个应用程序中促进统一的异常处理
- Spring 5 引入了 ResponseStatusException 类-一种在 REST API 中进行基本错误处理的快速方法
所有这些异常处理有一个共同点:它们很好地处理了关注点分离。即在问题关注点抛出异常,然后独立地处理异常
解决方案1:控制器级别的@ExceptionHandler
第一个解决方案适用于 @Controller 级别。 我们将定义一个处理异常的方法,并使用 @ExceptionHandler 对其进行注释:
public class FooController{
//...
@ExceptionHandler({ CustomException1.class, CustomException2.class })
public void handleException() {
//
}
}
这种方法有一个很大的缺点:只有在 Controller 添加 @ExceptionHandler 注解的方法才会生效,而不是全局定义
我们可以通过让所有 Controller 扩展 Base Controller 类来解决此限制。但是,对于无论出于何种原因都无法实现的应用程序,此解决方案可能会成为问题。例如,controller 可能已经从另一个基类扩展了,该基类可能在另一个 jar 中,或者不能直接修改,或者它们本身也不能直接修改
接下来,我们将讨论另一种解决异常处理问题的方法-一种全局的方法,不包括对现有工件(如Controllers)的任何更改
解决方案2:HandlerExceptionResolver
第二种解决方案是定义 HandlerExceptionResolver。这将解决应用程序引发的所有异常。它还将使我们能够在 REST API 中实现统一的异常处理机制
在设定自定义解析器之前,让我们看一下现有的实现
-
ExceptionHandlerExceptionResolver
该解析器是在 Spring 3.1 中引入的,默认情况下已在 DispatcherServlet 中启用。这实际上是前面介绍的 @ExceptionHandler 机制如何工作的核心组件 -
DefaultHandlerExceptionResolver
该解析器在 Spring 3.0 中引入,默认情况下在 DispatcherServlet 中启用。它用于将标准 Spring 异常解析为其对应的 HTTP 状态代码,即客户端错误 4xx 和服务器错误 5xx 状态代码。这是它处理的 Spring Exception 的完整列表,以及它们如何映射到状态代码
Exception | HTTP Status Code |
---|---|
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 引入了一个更好的选项的原因,我们将在后面的部分中进行讨论
- ResponseStatusExceptionResolver
此解析器也在 Spring 3.0 中引入,并且默认情况下在 DispatcherServlet 中启用。它的主要职责是使用自定义异常上可用的 @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 相同,此 resolver 在处理 response body 方面受到限制-它确实将 Status Code 映射到 response 上,但 body 仍为 null
-
SimpleMappingExceptionResolver 和 AnnotationMethodHandlerExceptionResolver
SimpleMappingExceptionResolver 已经停止使用相当长的一段时间。它来自较早的 Spring MVC 模型,与 REST 服务无关。我们基本上使用它来映射异常类名称以查看名称
AnnotationMethodHandlerExceptionResolver 在 Spring 3.0 中引入处理过的异常 @ExceptionHandler 注释但已被替代为 ExceptionHandlerExceptionResolver 在 Spring 3.2 中 -
自定义HandlerExceptionResolver
DefaultHandlerExceptionResolver 和 ResponseStatusExceptionResolver 的组合在为 Spring RESTful 服务提供良好的错误处理机制方面有很长的路要走
如前所述,不利方面是:no control over the body of the response
理想情况下,我们希望能够输出 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 模型,因此仍有改进的空间
解决方案3:@ControllerAdvice
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 一样定义单例或全局的异常处理组件
实际的机制非常简单,但也非常灵活:
- 它使我们可以完全控制 response body 以及 status code
- 它提供了几种异常到同一方法的映射,使之可以统一处理
- 它充分利用了最新的 RESTful ResposeEntity 响应
- 这里要记住的一件事是将 @ExceptionHandler 声明的异常与用作方法参数的异常进行匹配
如果这些不匹配,编译器将不会 complain(我也不知道这个词该怎么翻译,本意是抱怨,但抱怨肯定不合适),但是,当实际在运行时引发异常时,异常解析机制将因以下原因而失败:
java.lang.IllegalStateException: No suitable resolver for argument [0] [type=...]
HandlerMethod details: ...
批注
1. @Controller+@ExceptionHandler 优先级最高,其次是 @ControllerAdvice+@ExceptionHandler,最后才是 HandlerExceptionResolver,说明假设三种方式并存的情况 优先级越高的越先选择,而且被一个捕获处理了就不去执行其他的
2. 可以捕捉异常后做自定义处理(例如钉钉告警等),然后继续抛出
解决方案4:ResponseStatusException(Spring 5及更高版本)
Spring 5 引入了 ResponseStatusException 类。我们可以创建它的一个实例,提供 HttpStatus 以及可选的原因和原因:
@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);
}
}
-
优势
- 出色的原型制作:我们可以很快实现基本解决方案
- 一种类型,多种状态代码:一种异常类型可以导致多种不同的响应。与 @ExceptionHandler 相比,这减少了紧密耦合
- 我们将不必创建那么多的自定义异常类
- 由于可以通过编程方式创建异常,因此我们对异常处理有了更多的控制
-
不足
- 没有统一的异常处理方法:与提供全局方法的 @ControllerAdvice 相比,强制实施某些应用程序范围的约定更加困难
- 代码复制:我们可能会发现自己在多个控制器中复制代码
-
额外注意
- 我们可以在一个应用程序中组合不同的方法。我们可以全局实现 @ControllerAdvice,也可以局部实现 ResponseStatusException
- 如果可以以多种方式处理相同的异常,我们可能会注意到一些令人惊讶的行为。一种可能的约定是始终以一种方式处理一种特定类型的异常
有关更多详细信息和更多示例,请参见我们的 ResponseStatusException教程
我全粘过来了…
- ResponseStatusException教程
- ResponseStatus
- 在深入研究 ResponseStatusException 之前,让我们快速看一下 @ResponseStatus 注解。该注解是在 Spring 3 中引入的,用于将 HTTP 状态代码应用于 HTTP 响应,我们可以使用 @ResponseStatus 注解来设置 HTTP 响应中的状态和原因
@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "Actor Not Found") public class ActorNotFoundException extends Exception { // ... }
- 如果在处理 HTTP 请求时抛出此异常,则响应将包括此批注中指定的 HTTP 状态
- @ResponseStatus 方法的一个缺点是,它与异常会产生紧密耦合。在我们的示例中,所有类型为 ActorNotFoundException 的异常都会在响应中生成相同的错误消息和状态代码
- 在深入研究 ResponseStatusException 之前,让我们快速看一下 @ResponseStatus 注解。该注解是在 Spring 3 中引入的,用于将 HTTP 状态代码应用于 HTTP 响应,我们可以使用 @ResponseStatus 注解来设置 HTTP 响应中的状态和原因
- ResponseStatusException
- ResponseStatusException 是 @ResponseStatus 的编程替代方案,并且是用于将状态代码应用于 HTTP 响应的异常的基类。它是 RuntimeException,因此不需要在方法签名中显式添加
- Spring 提供了 3 个构造函数来生成 ResponseStatusException:
ResponseStatusException(HttpStatus status) ResponseStatusException(HttpStatus status, java.lang.String reason) ResponseStatusException( // an HTTP status set to HTTP response HttpStatus status, // a message explaining the exception set to HTTP response java.lang.String reason, // a Throwable cause of the ResponseStatusException java.lang.Throwable cause )
- 注意:在 Spring 中,HandlerExceptionResolver 拦截并处理任何引发的但未由 Controller 处理的异常,这些处理程序之一,ResponseStatusExceptionResolver,查找 @ResponseStatus 注释的任何 ResponseStatusException 或未捕获的异常,然后提取 HTTP 状态代码和原因,并将其包括在 HTTP 响应中
- ResponseStatusException 好处
- 首先,可以分别处理相同类型的异常,并且可以在响应中设置不同的状态代码,从而减少紧密耦合
- 其次,它避免了创建不必要的额外异常类
- 最后,由于可以以编程方式创建异常,因此它提供了对异常处理的更多控制
- 例子
- 产生 ResponseStatusException
- 现在,让我们来看一个生成 ResponseStatusException 的示例:
@GetMapping("/actor/{id}") public String getActorName(@PathVariable("id") int id) { try { return actorService.getActor(id); } catch (ActorNotFoundException ex) { throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Actor Not Found", ex); } }
- Spring Boot 提供了默认的 /error 映射,返回带有 HTTP 状态和异常消息的 JSON 响应
$ curl -i -s -X GET http://localhost:8080/actor/8 HTTP/1.1 404 Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Sun, 28 Jan 2018 19:48:10 GMT { "timestamp": "2018-01-28T19:48:10.471+0000", "status": 404, "error": "Not Found", "message": "Actor Not Found", "path": "/actor/8" }
- 现在,让我们来看一个生成 ResponseStatusException 的示例:
- 不同的状态码–相同的异常类型
- 现在,让我们看看在引发相同类型的异常时如何将不同的状态代码设置为 HTTP 响应:
@PutMapping("/actor/{id}/{name}") public String updateActorName(@PathVariable("id") int id, @PathVariable("name") String name) { try { return actorService.updateActor(id, name); } catch (ActorNotFoundException ex) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Provide correct Actor Id", ex); } }
- 响应的外观如下:
$ curl -i -s -X PUT http://localhost:8080/actor/8/BradPitt HTTP/1.1 400 ... { "timestamp": "2018-02-01T04:28:32.917+0000", "status": 400, "error": "Bad Request", "message": "Provide correct Actor Id", "path": "/actor/8/BradPitt" }
- 现在,让我们看看在引发相同类型的异常时如何将不同的状态代码设置为 HTTP 响应:
- 产生 ResponseStatusException
- ResponseStatus
处理 Spring Security 中拒绝的访问
当通过身份验证的用户尝试访问他没有足够权限访问的资源时,将发生“访问被拒绝”
- 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”
- 自定义 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 中通过重定向或显示自定义错误消息来自定义响应
- 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);
}
...
}
Spring Boot支持
Spring Boot提供了一个 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:
# 可用于禁用Whitelabel错误页面并依靠servlet容器提供HTML错误消息
enabled:
# 具有始终 值;在HTML和JSON默认响应中都包含stacktrace
include-stacktrace:
除了这些属性,我们还可以为 /error 提供我们自己的 view-resolver 映射,以覆盖 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) {
// ...
}
}
结论
本文讨论了几种在 Spring 中为 REST API 实现异常处理机制的方法,从较旧的机制开始,一直到 Spring 3.2 支持,一直延伸到 4.x 和 5.x
与往常一样,本文提供的代码可从GitHub上获得
对于与 Spring Security 相关的代码,您可以检查spring-security-rest模块