如何在Spring Boot中优雅地处理异常并返回统一格式的响应

D
dashi1 2025-08-05T02:21:31+08:00
0 0 237

如何在Spring Boot中优雅地处理异常并返回统一格式的响应

在现代Web开发中,良好的异常处理机制是保证系统健壮性和用户体验的关键。尤其是在基于Spring Boot构建的RESTful API项目中,如果异常未被妥善处理,可能会导致前端收到不一致甚至无法解析的响应体,从而引发严重的用户体验问题。

本文将详细介绍如何通过Spring Boot提供的@ControllerAdvice注解配合@ExceptionHandler来实现全局异常处理,并设计一套统一的响应格式,使所有API接口无论是否发生异常都能返回结构清晰、语义明确的JSON数据。

一、为什么需要统一异常处理?

假设我们有一个简单的用户注册接口:

@PostMapping("/users")
public User createUser(@RequestBody User user) {
    if (user.getName() == null || user.getName().trim().isEmpty()) {
        throw new IllegalArgumentException("用户名不能为空");
    }
    return userService.save(user);
}

当客户端发送非法请求时(如空用户名),服务器会直接抛出异常,此时浏览器或前端框架可能收到一个HTTP状态码为500的HTML页面(Tomcat默认错误页),而不是有意义的JSON错误信息。这会导致:

  • 前端无法识别错误类型;
  • 用户看到的是混乱的堆栈信息;
  • 接口难以调试和监控。

因此,我们需要一个机制,在任何地方抛出异常时,都由一个中心化的逻辑来捕获,并转换成标准的响应格式。

二、核心解决方案:@ControllerAdvice + @ExceptionHandler

1. 创建全局异常处理器类

@ControllerAdvice
public class GlobalExceptionHandler {

    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    // 处理运行时异常(如IllegalArgumentException)
    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity<ApiResponse<String>> handleRuntimeException(RuntimeException ex) {
        logger.warn("Runtime exception occurred: {}", ex.getMessage(), ex);
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(ApiResponse.error("参数错误", ex.getMessage()));
    }

    // 处理业务异常(如UserNotFoundException)
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ApiResponse<String>> handleBusinessException(BusinessException ex) {
        logger.info("Business exception occurred: {}", ex.getMessage());
        return ResponseEntity.status(ex.getHttpStatus())
                .body(ApiResponse.error(ex.getMessage(), ex.getDetail()));
    }

    // 处理404 Not Found
    @ExceptionHandler(NoHandlerFoundException.class)
    public ResponseEntity<ApiResponse<String>> handle404(NoHandlerFoundException ex) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
                .body(ApiResponse.error("接口不存在", "请检查URL路径"));
    }

    // 处理其他未预期异常(兜底)
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse<String>> handleGeneralException(Exception ex) {
        logger.error("Unexpected error occurred", ex);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(ApiResponse.error("系统内部错误", "请联系管理员"));
    }
}

⚠️ 注意:@ControllerAdvice 是 Spring MVC 提供的全局异常处理机制,适用于 Controller 层的所有方法。

2. 定义统一响应模型 ApiResponse<T>

public class ApiResponse<T> {
    private int code;
    private String message;
    private T data;
    private long timestamp;

    public static <T> ApiResponse<T> success(T data) {
        ApiResponse<T> response = new ApiResponse<>();
        response.setCode(200);
        response.setMessage("成功");
        response.setData(data);
        response.setTimestamp(System.currentTimeMillis());
        return response;
    }

    public static <T> ApiResponse<T> error(String message, String detail) {
        ApiResponse<T> response = new ApiResponse<>();
        response.setCode(500);
        response.setMessage(message);
        response.setTimestamp(System.currentTimeMillis());
        return response;
    }

    // getter/setter 省略...
}

这个类可以支持任意类型的返回数据(如 User, List<Order> 等),同时包含状态码、消息、时间戳等元信息。

三、自定义业务异常(可选但推荐)

为了更细粒度地控制不同场景下的错误行为,建议创建自己的异常类:

public class BusinessException extends RuntimeException {
    private HttpStatus httpStatus;

    public BusinessException(String message, HttpStatus status) {
        super(message);
        this.httpStatus = status;
    }

    public HttpStatus getHttpStatus() {
        return httpStatus;
    }
}

然后在服务层使用:

@Service
public class UserService {
    public User save(User user) {
        if (user.getEmail() == null || !user.getEmail().contains("@")) {
            throw new BusinessException("邮箱格式不正确", HttpStatus.BAD_REQUEST);
        }
        return userRepository.save(user);
    }
}

这样就能让前端根据不同的HTTP状态码做出差异化处理(比如弹窗提示 vs 跳转登录页)。

四、集成Swagger文档(增强可读性)

如果你使用了Springdoc OpenAPI(即Swagger UI),记得也要对异常进行注解说明,避免文档缺失:

@ApiResponses({
    @ApiResponse(responseCode = "400", description = "参数校验失败"),
    @ApiResponse(responseCode = "500", description = "服务器内部错误")
})
@PostMapping("/users")
public ResponseEntity<ApiResponse<User>> createUser(@RequestBody User user) {
    // ...
}

五、进阶技巧:日志记录与追踪ID

对于生产环境,建议结合MDC(Mapped Diagnostic Context)添加请求唯一标识,方便排查问题:

@Component
public class RequestLoggingInterceptor implements HandlerInterceptor {
    private static final ThreadLocal<String> traceId = new ThreadLocal<>();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String id = UUID.randomUUID().toString();
        traceId.set(id);
        MDC.put("traceId", id);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        MDC.clear();
        traceId.remove();
    }
}

并在异常处理器中打印Trace ID:

logger.warn("Request failed with traceId: {}, error: {}", traceId.get(), ex.getMessage());

六、测试验证

你可以用Postman或curl模拟各种异常情况:

curl -X POST http://localhost:8080/users \
  -H "Content-Type: application/json" \
  -d '{"name": "", "email": "invalid"}'

预期返回:

{
  "code": 400,
  "message": "参数错误",
  "data": null,
  "timestamp": 1719000000000
}

总结

通过以上实践,我们可以做到:

✅ 所有异常都被集中捕获
✅ 返回格式统一且语义清晰
✅ 支持多种异常类型分类处理
✅ 易于扩展和维护(只需修改GlobalExceptionHandler)
✅ 符合RESTful最佳实践

这套方案已在多个中大型Spring Boot项目中稳定运行多年,是构建高质量API不可或缺的一环。

如果你正在搭建微服务架构或准备上线API网关,强烈建议将此模式纳入标准工程模板!

📌 小贴士:还可以进一步集成AOP切面来做日志埋点、性能统计等,形成完整的可观测体系。

相似文章

    评论 (0)