首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Spring 异常处理@ExceptionHandler

Spring 异常处理@ExceptionHandler

作者头像
崔认知
发布2025-08-04 10:42:11
发布2025-08-04 10:42:11
18700
代码可运行
举报
文章被收录于专栏:nobodynobody
运行总次数:0
代码可运行

翻译:https://www.baeldung.com/exception-handling-for-rest-with-spring

1. 概览

本教程将展示如何为 REST API 实现 Spring 异常处理。我们将了解到有多种实现方式。它们都有一个共同点:都能很好地处理关注点分离。应用可以正常抛出异常以表明某种失败,异常将被单独处理。

2. @ExceptionHandler

我们可以使用 @ExceptionHandler 来注解方法,当发生指定异常时,Spring 将自动调用这些方法。我们可以通过注解指定异常,也可以将其声明为方法参数,这使我们能够从异常对象中读取详细信息以正确处理异常。该方法本身作为控制器方法被处理,所以:

  • 它可以返回一个被渲染到响应体中的对象,或者是一个完整的 ResponseEntity 。自 Spring 6.2 起,这里允许进行内容协商。
  • 它可以返回一个 ProblemDetail 对象。Spring 将自动把 ContentType 头设置为 “application/problem+json“。
  • 我们可以使用 @ResponseStatus 指定返回码。

一个返回 400 状态码的最简单的异常处理器可能是这样的:

代码语言:javascript
代码运行次数:0
运行
复制
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(CustomException1.class)
public void handleException1() { }

我们也可以将处理的异常声明为方法参数,例如,读取异常详细信息并创建一个符合 RFC-9457 标准的问题详细信息对象:

代码语言:javascript
代码运行次数:0
运行
复制
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler
public ProblemDetail handleException2(CustomException2 ex) {
    // ...
}

自 Spring 6.2 起,我们可以针对不同的内容类型编写不同的异常处理器:

代码语言:javascript
代码运行次数:0
运行
复制
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler( produces = MediaType.APPLICATION_JSON_VALUE )
public CustomExceptionObject handleException3Json(CustomException3 ex) {
    // ...
}

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler( produces = MediaType.TEXT_PLAIN_VALUE )
public String handleException3Text(CustomException3 ex) {
    // ...
}

而且,我们还可以针对不同类型的异常编写异常处理器。如果处理方法需要详细信息,我们使用所有异常类型的共享超类作为方法参数:

代码语言:javascript
代码运行次数:0
运行
复制
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler({
    CustomException4.class,
    CustomException5.class
})
public ResponseEntity<CustomExceptionObject> handleException45(Exception ex) {
    // ...
}

2.1. 本地异常处理(控制器级别)

我们可以将此类处理器方法放置在控制器类中:

代码语言:javascript
代码运行次数:0
运行
复制
@RestController
public class FooController {
    //...

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(CustomException1.class)
    public void handleException() {
        // ...
    }
}

我们可以在需要控制器特定异常处理时使用这种方法。但它的缺点是除非我们将它放在基类中并使用继承,否则无法在多个控制器中使用它。但还有另一种更适合组合而非继承的方法。

2.2. 全局异常处理

@ControllerAdvice 包含多个控制器共享的代码。它是一种特殊的 Spring 组件。对于 REST API 而言,每个方法的返回值都应被渲染到响应体中,因此有一个 @RestControllerAdvice

因此,为了处理应用中所有控制器的特定异常,我们可以编写一个简单的类:

代码语言:javascript
代码运行次数:0
运行
复制
@RestControllerAdvice
public class MyGlobalExceptionHandler {
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(CustomException1.class)
    public void handleException() {
        // ...
    }
}

我们应该知道,还有一个基类(ResponseEntityExceptionHandler),我们可以通过继承它来使用常见的预定义功能,如 ProblemDetails 生成。我们还可以继承用于处理典型 MVC 异常的方法:

代码语言:javascript
代码运行次数:0
运行
复制
@ControllerAdvice
public class MyCustomResponseEntityExceptionHandler
  extends ResponseEntityExceptionHandler {

    @ExceptionHandler({
        IllegalArgumentException.class,
        IllegalStateException.class
    })
    ResponseEntity<Object> handleConflict(RuntimeException ex, WebRequest request) {
        String bodyOfResponse = "This should be application specific";
        return super.handleExceptionInternal(ex, bodyOfResponse,
          new HttpHeaders(), HttpStatus.CONFLICT, request);
    }

    @Override
    protected ResponseEntity<Object> handleHttpMediaTypeNotAcceptable(
      HttpMediaTypeNotAcceptableException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request {
        // ... (customization, maybe invoking the overridden method)
    }
}

在上述示例中,请注意,由于所有方法都返回一个 ResponseEntity ,所以我们使用了普通的 @ControllerAdvice 注解,而没有在类上添加 @RestControllerAdvice 注解。

3. 直接注解异常

另一种简单的方法是直接用 @ResponseStatus 注解我们的自定义异常:

代码语言:javascript
代码运行次数:0
运行
复制
@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class MyResourceNotFoundException extends RuntimeException {
    // ...
}

与 DefaultHandlerExceptionResolver 一样,此解析器在处理响应体方面存在限制 —— 它会将状态码映射到响应上,但响应体仍然是 null 。我们只能用它来处理我们的自定义异常,因为我们无法注解已经编译好的现有类。而且,在分层架构中,我们只应将此方法用于边界特定的异常

顺便说一下,我们应该注意到,在这种情况下,异常通常是从 RuntimeException 派生而来的,因为我们这里不需要编译器检查。否则,这将导致我们代码中出现不必要的 throws 声明。

4. ResponseStatusException

控制器还可以抛出一个 ResponseStatusException 。我们可以通过提供一个 HttpStatus 来创建它的实例,并可选地提供一个原因和一个原因实例:

代码语言:javascript
代码运行次数:0
运行
复制
@GetMapping(value = "/{id}")
public Foo findById(@PathVariable("id") Long id) {
    try {
        // ...
     }
    catch (MyResourceNotFoundException ex) {
         throw new ResponseStatusException(
           HttpStatus.NOT_FOUND, "Foo Not Found", ex);
    }
}

使用 ResponseStatusException 有什么好处呢?

  • 非常适合原型制作:我们可以相当快速地实现一个基本解决方案。
  • 一种异常类型,多种状态码:一种异常类型可以导致多种不同的响应。这与 @ExceptionHandler 相比,减少了紧密耦合
  • 我们不需要创建那么多自定义异常类。
  • 我们对异常处理有更多的控制权,因为异常可以被程序化地创建。

那权衡又是什么呢?

  • 没有统一的异常处理方式:与 @ControllerAdvice(它提供全局方法)相比,更难强制实施一些应用范围的约定。
  • 代码重复:我们可能会在多个控制器中复制代码。
  • 在分层架构中,我们只应在控制器中抛出这些异常。如代码示例所示,我们可能需要对底层抛出的异常进行包装。

5. HandlerExceptionResolver

另一种解决方案是定义一个自定义的 HandlerExceptionResolver 。它将解析应用抛出的任何异常。它还将允许我们在 REST API 中实现一个统一的异常处理机制

5.1. 现有实现

在 DispatcherServlet 中默认启用了几种现有实现:

  • ExceptionHandlerExceptionResolver 实际上是前面所述的 @ExceptionHandler 机制工作的核心组件。
  • ResponseStatusExceptionResolver 实际上是前面所述的 @ResponseStatus 机制工作的核心组件。
  • DefaultHandlerExceptionResolver 用于将标准 Spring 异常解析为它们对应 HTTP 状态码,即客户端错误 4xx 和服务器错误 5xx 状态码。以下是它处理的 Spring 异常及其映射到状态码的完整列表。虽然它确实正确设置了响应的状态码,但一个限制是它没有设置响应体的任何内容

5.2. 自定义 HandlerExceptionResolver

DefaultHandlerExceptionResolver 和 ResponseStatusExceptionResolver 的组合为我们提供了一个良好的 Spring RESTful 服务的错误处理机制。缺点是,如之前所述,我们对响应体没有控制权。

理想情况下,我们希望能够根据客户端请求的格式(通过 Accept 头)输出 JSON 或 XML。

这本身就证明了创建一个新的自定义异常解析器是合理的:

代码语言:javascript
代码运行次数:0
运行
复制
@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 模型。

6. 其他注意事项

6.1. 处理现有异常

在典型的 REST 实现中,我们经常要处理以下几种异常:

  • AccessDeniedException 发生在经过身份验证的用户试图访问他没有足够权限访问的资源时。例如,这可能发生在我们使用方法级别安全注解如 @PreAuthorize、@PostAuthorize 和 @Secure 时。
  • ValidationException 和 ConstraintViolationException 发生在我们使用 Bean Validation 时。
  • PersistenceException 和 DataAccessException 发生在我们使用 Spring Data JPA 时。

当然,我们将使用前面讨论的全局异常处理机制来处理 AccessDeniedException:

代码语言:javascript
代码运行次数:0
运行
复制
@RestControllerAdvice
public class MyGlobalExceptionHandler {
    @ResponseStatus(value = HttpStatus.FORBIDDEN)
    @ExceptionHandler( AccessDeniedException.class )
    public void handleAccessDeniedException() {
        // ...
    }
}

6.2. Spring Boot 支持

Spring Boot 提供了一个 ErrorController 实现,以一种合理的方式处理错误。

简而言之,它为浏览器提供了一个回退错误页面(即 Whitelabel 错误页面),并为 RESTful、非 HTML 请求提供了一个 JSON 响应:

代码语言:javascript
代码运行次数:0
运行
复制
{
    "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 错误页面,并依赖 servlet 容器提供 HTML 错误消息
  • server.error.include-stacktrace :当值为 always 时,它在 HTML 和 JSON 默认响应中都包含堆栈跟踪
  • server.error.include-message :自 2.3 版本起,Spring Boot 为了防止泄露敏感信息,在响应中隐藏了 message 字段;我们可以将此属性设置为 always 来启用它

除了这些属性,我们还可以提供自己的视图解析器映射,用于 /error,以覆盖 Whitelabel 页面。

我们还可以通过在上下文中包含一个 ErrorAttributes bean 来定制我们希望在响应中显示的属性。我们可以扩展 Spring Boot 提供的 DefaultErrorAttributes 类来简化操作:

代码语言:javascript
代码运行次数:0
运行
复制
@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 的公共方法,并声明它生成 application/xml 媒体类型:

代码语言:javascript
代码运行次数:0
运行
复制
@Component
public class MyErrorController extends BasicErrorController {
    public MyErrorController(
      ErrorAttributes errorAttributes, ServerProperties serverProperties) {
        super(errorAttributes, serverProperties.getError());
    }

    @RequestMapping(produces = MediaType.APPLICATION_XML_VALUE)
    public ResponseEntity<Map<String, Object>> xmlError(HttpServletRequest request) {
        // ...
    }
}

注意:这里我们仍然依赖我们在项目中可能已经定义的绑定到 ServerProperties bean 的 server.error.* Spring Boot 属性。

7. 结论

在本文中,我们讨论了几种为 Spring REST API 实现异常处理机制的方法。我们根据其使用场景对它们进行了比较。

我们应该注意到,在一个应用中可以结合使用不同的方法。例如,我们可以实现一个@ControllerAdvice 进行全局处理,同时在本地使用 ResponseStatusException 进行处理。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-08-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 认知科技技术团队 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 概览
  • 2. @ExceptionHandler
    • 2.1. 本地异常处理(控制器级别)
    • 2.2. 全局异常处理
  • 3. 直接注解异常
  • 4. ResponseStatusException
  • 5. HandlerExceptionResolver
    • 5.1. 现有实现
    • 5.2. 自定义 HandlerExceptionResolver
  • 6. 其他注意事项
    • 6.1. 处理现有异常
    • 6.2. Spring Boot 支持
  • 7. 结论
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档