1. 概述

本文将系统介绍 如何在 Spring REST API 中实现异常处理机制。我们将探讨多种实现方案,它们共同遵循 关注点分离 的核心原则:应用层可以正常抛出异常来表示业务失败,而异常处理则由专门机制统一管理。

2. @ExceptionHandler

通过 @ExceptionHandler 注解,Spring 会在指定异常发生时自动调用标记方法。我们可以通过注解参数或方法参数声明异常类型,后者允许从异常对象提取详细信息进行处理。该方法的特性与控制器方法类似:

  • 可返回渲染到响应体的对象或完整的 ResponseEntity,支持内容协商(Spring 6.2+)
  • 可返回 ProblemDetail 对象,Spring 会自动设置 Content-Type: application/problem+json
  • 可通过 @ResponseStatus 指定状态码

最简单的 400 状态码异常处理器:

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

也可将异常声明为方法参数,例如构建符合 RFC-9457 的错误详情对象:

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler
public ProblemDetail handleException2(CustomException2 ex) {
    // ...
}

Spring 6.2 支持为不同内容类型编写独立处理器:

@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) {
    // ...
}

还能处理多种异常类型,使用共享父类作为方法参数:

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler({ 
    CustomException4.class, 
    CustomException5.class
})
public ResponseEntity<CustomExceptionObject> handleException45(Exception ex) {
    // ...
}

2.1. 局部异常处理(控制器级别)

将处理器直接放在控制器类中:

@RestController
public class FooController {
    //...

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

✅ 适用于需要控制器专属异常处理的场景
❌ 缺点:无法跨控制器复用,除非通过基类继承(违反组合优于继承原则)

2.2. 全局异常处理

@ControllerAdvice 封装多控制器共享逻辑,是特殊的 Spring 组件。REST API 中推荐使用 @RestControllerAdvice

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

⚠️ 可继承 ResponseEntityExceptionHandler 基类获取预置功能(如 ProblemDetails 生成):

@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) {
        // ... (自定义逻辑,可调用父类方法)
    }
}

注:因所有方法返回 ResponseEntity,此处使用 @ControllerAdvice 而非 @RestControllerAdvice

3. 直接在异常上使用注解

直接为自定义异常添加 @ResponseStatus 注解:

@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class MyResourceNotFoundException extends RuntimeException {
    // ...
}

⚠️ 与 DefaultHandlerExceptionResolver 类似,无法控制响应体(仅设置状态码,响应体为 null)
❌ 局限:只能用于自定义异常(无法注解已编译类)
✅ 最佳实践:仅用于边界层特定异常

异常通常继承 RuntimeException,避免不必要的 throws 声明

4. ResponseStatusException

控制器可直接抛出 ResponseStatusException,支持传入状态码、原因和原始异常:

@GetMapping(value = "/{id}")
public Foo findById(@PathVariable("id") Long id) {
    try {
        // ...
     }
    catch (MyResourceNotFoundException ex) {
         throw new ResponseStatusException(
           HttpStatus.NOT_FOUND, "Foo Not Found", ex);
    }
}

✅ 优势:

  • 快速原型开发
  • 松耦合:单异常类型可映射多种状态码(对比 @ExceptionHandler
  • 减少自定义异常类数量
  • 程序化控制异常处理

❌ 权衡:

  • 缺乏统一处理机制(对比 @ControllerAdvice 的全局性)
  • 代码重复风险
  • 分层架构中仅限控制器层抛出,需包装底层异常

5. HandlerExceptionResolver

自定义 HandlerExceptionResolver 可实现 统一异常处理机制,处理应用抛出的所有异常。

5.1. 现有实现

DispatcherServlet 默认启用三种实现:

  • ExceptionHandlerExceptionResolver@ExceptionHandler 的核心组件
  • ResponseStatusExceptionResolver@ResponseStatus 的核心组件
  • DefaultHandlerExceptionResolver:将标准 Spring 异常映射为 HTTP 状态码(4xx/5xx),完整映射表
    ⚠️ 仅设置状态码,响应体为 null

5.2. 自定义 HandlerExceptionResolver

为解决响应体控制问题(如根据 Accept 头返回 JSON/XML),需自定义解析器:

@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
  • 通过 ModelAndView 控制响应体

❌ 局限:

  • 依赖底层 HttpServletResponse
  • 耦合旧 MVC 模型(ModelAndView

6. 补充说明

6.1. 处理现有异常

常见 REST 异常处理场景:

  • AccessDeniedException:认证用户访问无权限资源(如 @PreAuthorize 触发)
  • ValidationException/ConstraintViolationExceptionBean Validation 触发
  • PersistenceException/DataAccessExceptionSpring Data JPA 触发

使用全局异常处理器处理 AccessDeniedException

@RestControllerAdvice
public class MyGlobalExceptionHandler {
    @ResponseStatus(value = HttpStatus.FORBIDDEN)
    @ExceptionHandler( AccessDeniedException.class )
    public void handleAccessDeniedException() {
        // ...
    }
}

6.2. Spring Boot 支持

Spring Boot 提供 ErrorController 实现智能处理错误

  • 浏览器请求:返回 Whitelabel 错误页
  • REST 请求:返回 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"
}

核心配置属性:

  • server.error.whitelabel.enabled:禁用 Whitelabel 页面
  • server.error.include-stacktrace:控制堆栈跟踪显示
  • server.error.include-message:控制消息字段显示(2.3+ 默认隐藏)

自定义响应属性(扩展 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;
    }
}

自定义特定内容类型错误处理(扩展 BasicErrorController):

@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) {
        // ...
    }
}

7. 总结

本文系统对比了 Spring REST API 异常处理的多种方案及其适用场景。实际开发中可灵活组合使用,例如:

  • 全局使用 @ControllerAdvice
  • 局部使用 ResponseStatusException

选择方案时需权衡:

  • 统一性 vs 灵活性
  • 代码复用 vs 快速实现
  • 分层架构约束

根据项目规模和团队规范选择最适合的异常处理策略。