在 Spring 应用中为 REST API 实现异常处理
1、概览
本文将地带你了解如何在 Spring 中为 REST API 实现异常处理。
在 Spring 3.2 之前,在 Spring MVC 应用中处理异常的两种主要方法是 HandlerExceptionResolver
或 @ExceptionHandler
注解。这两种方法都有一些明显的缺点。
自 3.2 以来,可以使用 @ControllerAdvice
注解来解决前两种解决方案的局限性,并促进整个应用中统一的异常处理。
Spring 5 引入了 ResponseStatusException
类,一种在 REST API 中进行基本错误处理的快速方法。
所有这些都有一个共同点:它们都很好地处理了关注点的分离。应用通常可以抛出异常来表示某种失败,然后再单独进行处理。
2、解决方案 1:Controller 级的 @ExceptionHandler
第一种解决方案适用于 @Controller
层面。定义一个处理异常的方法,并用 @ExceptionHandler
进行注解:
public class FooController{
//...
@ExceptionHandler({ CustomException1.class, CustomException2.class })
public void handleException() {
//
}
}
这种方法有一个很大的缺点:@ExceptionHandler
注解方法仅对特定 Controller 有效,而不是对整个应用全局有效。当然,可以将其添加到每个 Controller 中,但这并不适合作为通用的异常处理机制。
也可以通过让所有 Controller 都继承一个 Base Controller 类来绕过这一限制。
然而,对于某些原因无法实现上述方法的应用来说,这种解决方案可能会成为一个问题。例如,Controller 可能已经从另一个 Base 类继承而来,而该 Base 类可能在另一个 Jar 中或不可直接修改,或者 Controller 本身不可直接修改。
3、解决方案 2:HandlerExceptionResolver
第二种解决方案是定义一个 HandlerExceptionResolver
。这将解析应用抛出的任何异常。它还允许在 REST API 中实现统一的异常处理机制。
在使用自定义解析器之前,先来了解一下现有的实现。
3.1、ExceptionHandlerExceptionResolver
该 Resolver 在 Spring 3.1 中引入,并在 DispatcherServlet
中默认启用。这实际上是前面介绍的 @ExceptionHandler
机制如何工作的核心组件。
3.2、DefaultHandlerExceptionResolver
该 Resolver 在 Spring 3.0 中引入,默认在 DispatcherServlet
中启用。
它用于将标准 Spring 异常解析为相应的 HTTP 状态码,即客户端错误 4xx 和服务器错误 5xx 状态码。以下是它所处理的 Spring 异常的完整列表,以及它映射到的 HTTP 状态码。
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
渲染错误内容来解决,但该解决方案显然不是最佳的。
3.3、ResponseStatusExceptionResolver
该 Resolver 也在 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 处理响应体的方式受到限制 - 它确实会在响应上映射状态码,但 Body (响应体)仍为 null
。
3.4、自定义 HandlerExceptionResolver
DefaultHandlerExceptionResolver
和 ResponseStatusExceptionResolver
的组合可为 Spring RESTful 服务提供良好的错误处理机制。缺点是,如前所述,无法控制响应体。
理想情况下,我们希望能够响应 JSON 或 XML,具体取决于客户期望的格式(通过 Accept
Header)。
创建一个新的自定义异常解析器。
@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();
}
}
这里需要注意的一个细节是,可以访问 request
本身,因此可以考虑客户端发送的 Accept
Header 的值。
例如,如果客户端要求使用 application/json
,那么在出现错误的情况下,就需要确保返回一个用 application/json
编码的响应体。
另一个重要的实现细节是返回了一个 ModelAndView
对象,这是响应 Body,它允许我们对其进行必要的设置。
这种方法为 Spring REST 服务的错误处理提供了一致且易于配置的机制。
然而,它也有一些限制:它与底层的 HttpServletResponse
进行交互,并适用于使用 ModelAndView
的旧 MVC 模型,因此仍有改进的空间。
4、解决方案 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
整合为一个单一的全局错误处理组件。
实际机制非常简单,但也非常灵活:
- 它能够完全控制响应体和状态码。
- 它将多个异常映射到同一个方法中,以便一起处理。
- 它充分利用了较新的 RESTful
ResposeEntity
响应。
这里要注意的一点是,用 @ExceptionHandler
声明的异常要与作为方法参数的异常相匹配。
如果它们不匹配,编译不会异常,Spring 启动也不会异常。但是,当异常在运行时实际抛出时,异常解析机制将失败,并显示如下错误消息:
java.lang.IllegalStateException: No suitable resolver for argument [0] [type=...]
HandlerMethod details: ...
5、解决方案 4:ResponseStatusException(Spring 5 及其以上)
Spring 5 引入了 ResponseStatusException
类。
我们可以创建一个实例,提供 HttpStatus
以及可选的 reason
和 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
提供了一种全局的方法,更难以强制执行一些应用范围的约定。 - 代码重复:可能会在多个 Controller 中重复编写代码。
注意,在一个应用中可以结合不同的方法。
例如,可以在全局范围内实现 @ControllerAdvice
,但也可以在本地范围内实现 ResponseStatusException
。
不过,需要小心谨慎: 如果可以用多种方式处理同一异常,可能会发现一些意外的行为。一种可能的约定俗成的做法是,总是以一种方式处理一种特定的异常。
6、处理 Spring Security 中的 “Access Denied”(拒绝访问)
当经过身份认证的用户试图访问他没有足够权限访问的资源时,就会发生拒绝访问的情况。
6.1、REST 和方法级 Security
最后,来看看如何处理方法级 Security 注解 @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 Boot 提供了一个 ErrorController
实现,以合理的方式处理错误。
简而言之,它为浏览器提供一个基础的错误页面(又称 “Whitelabel Error Page”),并为 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.enabled
:可用于禁用 “Whitelabel Error Page”,并依靠 servlet 容器提供 HTML 错误消息。server.error.include-stacktrace
:设置为always
时,在 HTML 和 JSON 默认响应中包含栈跟踪信息。server.error.include-message
:自 2.3 版本起,Spring Boot 隐藏响应中的message
字段,以避免泄露敏感信息;可以使用该属性并将其设置为always
来启用该功能。
除了这些属性外,还可以为 /error
提供自己的视图解析映射,覆盖 “Whitelabel Page”。
还可以通过在 Context 中包含一个 ErrorAttributes
Bean 来自定义要在响应中显示的属性。可以继承 Spring Boot 提供的 DefaultErrorAttributes
类来简化操作:
@Component
public class MyCustomErrorAttributes extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(
WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> errorAttributes =
super.getErrorAttributes(webRequest, options);
errorAttributes.put("locale", webRequest.getLocale()
.toString());
errorAttributes.remove("error");
//...
return errorAttributes;
}
}
如果想进一步定义(或重写)应用如何处理特定内容类型的错误,可以注册 ErrorController
Bean。
同样,可以继承 Spring Boot 提供的默认 BasicErrorController
来帮助我们处理异常。
例如,假设想自定义应用如何处理 XML 端点中触发的错误。所要做的就是使用 @RequestMapping
定义一个 public
方法,并指定它生成的是 application/xml
媒体类型(Media Type):
@Component
public class MyErrorController extends BasicErrorController {
public MyErrorController(
ErrorAttributes errorAttributes, ServerProperties serverProperties) {
super(errorAttributes, serverProperties.getError());
}
@RequestMapping(produces = MediaType.APPLICATION_XML_VALUE) // application/xml
public ResponseEntity<Map<String, Object>> xmlError(HttpServletRequest request) {
// ...
}
}
注意:这里仍然依赖于可能在 application.properties
中定义的 server.error.
属性,这些属性绑定在 ServerProperties
Bean 上。
8、总结
本文介绍了在 Spring 应用中为 REST API 实现异常处理的几种方法。
Ref:https://www.baeldung.com/exception-handling-for-rest-with-spring