本文将地带你了解如何在 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 查询
  •