本文将地带你了解如何在
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
虽然它能正确设置响应的状态码,但有一个局限性,那就是它不能为响应体设置任何消息。对于 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 上。
本文介绍了在 Spring 应用中为 REST API 实现异常处理的几种方法。
Ref:
https://www.baeldung.com/exception-handling-for-rest-with-spring
Spring 异常 “No Multipart Boundary Was Found”
在 Spring 中实现 Bulk 和 Batch API
使用 Embedding 模型和向量数据库的 Spring AI RAG
在 Spring Boot GraalVM 原生镜像中使用 Thymeleaf 布局和 Fragment 表达式
在 JPA 中使用 CriteriaQuery 执行 COUNT 查询