Spring的REST API异常处理
Spring的REST错误处理-异常处理
1、概述
本文将阐述如何使用REST API的spring实现异常处理。我们还将获得一下历史概述,并查看不同版本引入了那些新选项。
在spring3.2之前,在spring MVC应用程序中处理异常主要有两种方法:HandlerExceptionResolver或@ExceptionHandler注解,两者都有明显的缺点。从3.2开始,spring有了@ControllerAdvice批注,已解决前两个方案的局限性,并在整个应用中促进统一异常处理。现在Spring5引入了ResponseStatusException类:一种在REST API中进行基本错误处理的快速方法。
所有这些确实有一个共同点-它们很好的处理了关注点分离。该应用程序通常可以引发异常以指示某种类型的异常-异常将随后单独处理。
最后,我们将看到springboot带来的好处,以及如何配置它以满足我们的需求。
2、解决方案1-控制器级别@ExceptionHandler
第一个解决方案在Controller级别起作用-我们将定义一个异常处理的方法,并使用@ExceptionHandler对其进行注解:
public class TestController {
//...
@ExceptionHandler({CustomException1.class, CustomException2.class})
public void handlerException() {
//...
}
}
这种方法有一个主要缺点-带@ExceptionHandler注释的方法仅对特定持有的Controller有效,而对整个应用程序无效,当然,将其添加到每个控制器使其不适用常规异常处理机制。
我们可以让所有的Controller扩展Base Controller类来解决此限制,但是,对于无论出于何种原因都无法实现的应用程序来说,这可能是个问题。例如,控制器可能已经从另一个基类扩展了,该基类可能在另一个jar中,或者不能直接修改,或者它们本身也不能直接修改。
接下来,我们将讨论另一种解决异常处理问题的方法-一种全局的方法,不包括对现有工件(如Controller)的任何更改。
3、解决方案2-HandlerExceptionResolver
第二种解决方案是定义HandlerExceptionResolver-这将解决应用程序引发的任何异常。它还将使我们能够在REST API中实现统一的异常处理机制。
在使用自定义解析器之前,让我们看一下现有的实现。
3.1 ExceptionHandlerEXceptionResolver
该解析器在sring3.1中引入,默认情况下在DispatcherServlet中启用。这实际上是前面介绍的@ExceptionHandler机制如何工作的核心组件。
3.2 DefaultHandlerExceptionResolver
该解析器在spring3.0中引入,默认情况下在DispatcherServlet中启用。它用于将标准Spring异常解析为其对应的HTTP状态代码,即客户端错误-4xx和服务器错误-5xx代码。这是它处理Spring Exception的完整列表,以及它们如何映射带状态代码
尽管它正确设置了响应的状态码,但一个限制是它没有对响应的主体设置任何内容。对于REST API,状态码实际上是不足以向客户端提供信息-响应也必须具有主体,以允许应用程序提供有关故障的其他信息。
可以通过配置视图分辨率并通过ModelAndView呈现错误内容来解决此问题,但是解决方案不是最佳的。这就是为什么Spring 3.2引入了一个更好的选项的原因,我们将在后面的部分中进行讨论。
3.3 ResponseStatusExceptionResolver
此解析器也在Spring 3.0中引入,并且默认情况下在DispatchServlet中启用。它的主要职责是使用自定义异常上可用的@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相同,此解析器在处理响应主体方面受到限制-它确实将状态码映射到响应上,但主体仍为null。
3.4 SimpleMappingExceptionResolver和AnnotationMethodHandlerExceptionResolver
该SimpleMappingExceptionResolver已经有相当长的一段时间-它是上了年纪的Spring MVC模型出来,是不是一个REST服务非常相关的。我们基本上使用它来映射异常类名称以查看名称。
该AnnotationMethodHandlerExceptionResolver在Spring3.0中引入处理过的异常@ExceptionHandler注释但已被弃用ExceptionHandlerExceptionResolver如Spring3.2。
3.5 自定义HandlerExceptionResolver
DefaultHandlerExceptionResolver和ResponseStatusExceptionResolver的组合在为Spring RESTful服务提供良好错误处理机制方面还有很长的路要走。如前所述,不利方面是无法控制响应主体。
理想情况下,我们希望能够输出JOSN或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 Service的错误处理的一致且易于配置的机制。但是,它确实有局限性:它与低级HtttpServletResponse进行交互,并且适合使用ModelAndView的旧MVC模型-因此仍有改进的空间。
4、解决方案aaa@qq.com
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期从之前一个单一的,到全球性的错误处理组件。
实际的机制非常简单,但也非常灵活。它给我们:
- 完全控制响应的主体以及状态码
- 将多个异常映射到同一方法,以一起处理,并且 它充分利用了更新的RESTful ResposeEntity响应
这里要记住的一件事是将@ExceptionHandler声明的异常与用作方法参数的异常进行匹配。如果这些不匹配,则编译器将不会报错-没有理由,并且Spring也不会抛出异常。
但是,当实际在运行时引发异常时,异常解决机制将因以下原因而失败:
java.lang.IllegalStateException: No suitable resolver for argument [0] [type=...]
HandlerMethod details: ...
5、解决方案4-ResponseStatusException(Spring5以及更高)
Spring5引入了ResponseStatusException类。我们可以创建一个提供HttpStatus是实例,还可以选择一个reason and a cause:
@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);
}
}
使用ResponseStatusException有什么好处?
- 出色的原型制作:我们可以很快实现基本解决方案
- 一种类型,多种状态代码:一种异常类型可以导致多种不同的响应。与@ExceptionHandler相比,这减少了紧密耦合
- 我们将不必创建那么多的自定义异常类
- 由于可以通过编程方式创建异常,因此可以更好地控制异常处理
那权衡又如何呢? - 没有统一的异常处理方式:实施一些应用程序范围的约定比@ControllerAdvice提供全局方法要困难得多。
- 代码复制:我们可能会发现自己在多个控制器中复制代码
我们还应该注意,可以在一个应用程序中组合不同的方法。
例如,我们可以 全局实现@ControllerAdvice,还可以 局部实现 ResponseStatusException。但是,我们需要注意:如果可以通过多种方式处理相同的异常,我们可能会注意到一些令人惊讶的行为。一种可能的约定是始终以一种方式处理一种特定类型的异常。
6、处理Spring Security中拒绝的访问
当经过身份验证的用户尝试访问他没有足够权限访问的资源时,将发生“访问被拒绝”。
6.1 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 ”。
6.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 Configuration对其进行配置:
<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中,我们可以通过重定向或显示自定义错误消息来根据需要自定义响应。
6.3 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);
}
...
}
7、Spring Boot支持
Spring Boo提供了一个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.enable: 可用于禁用Whitelable错误页面并依靠servlet容器提供HTML错误消息
- Server.error.include-stacktrace: 具有始终值,它在HTML和JSON默认响应中都包含stacktrace
除了这些属性,我们还可以为错误提供我们自己的视图解析器映射,以覆盖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) {
// ...
}
}
8、结论
本教程讨论了几种在Spring中为REST API实现异常处理机制的方法,从较旧的机制开始,一直到Spring 3.2支持,一直延伸到4.x和5.x。
下一篇: Spring Boot 全局异常处理