Spring Boot应用如何全局捕获ConnectionException并存储故障请求?
实现结论
基于Spring Boot + PostgreSQL的技术栈,完全可以实现全局统一捕获数据库连接异常,同时完成故障请求的持久化存储,不存在框架层面的强制限制。
核心实现路径
1. 全局异常捕获的原理说明
Spring Boot自带的@RestControllerAdvice全局异常解析器,本身可以捕获所有沿MVC处理链路向上抛出的异常——包括Service层、DAO层抛出的数据库连接异常。这类异常通常会被Spring的JDBC、ORM模块统一包装为org.springframework.jdbc.CannotGetJdbcConnectionException,如果是PostgreSQL驱动层直接抛出的连接类错误,也会以org.postgresql.util.PSQLException的形式沿调用链向上传递,只要没有在业务代码中被提前try-catch吞掉,就能被全局处理器接收到。
2. 具体落地步骤
- 声明全局异常处理器,单独匹配两类需要捕获的异常类型:
- Spring包装层抛出的
CannotGetJdbcConnectionException - PostgreSQL驱动层抛出的
PSQLException,可额外匹配连接类错误码(比如08001连接拒绝、08006连接中断)做精准过滤,避免把表不存在、SQL语法错误这类非连接异常也归到同一分支处理
- Spring包装层抛出的
- 从当前请求上下文获取触发异常的原始请求信息:通过
RequestContextHolder拿到当前绑定的HttpServletRequest对象,提取请求URI、请求方法、客户端IP、请求头、请求参数、请求体内容(注意需要提前配置可重复读的请求包装过滤器,避免请求体流被读取后无法二次获取)、异常堆栈信息 - 实现文件持久化逻辑:把提取到的故障请求信息序列化为固定格式(比如结构化JSON),按日切分写入到指定的本地文件路径,写入时要做磁盘剩余空间校验、文件权限控制,避免无限制写满磁盘
3. 需要额外覆盖的边界场景
注意:仅靠
@RestControllerAdvice无法覆盖所有非Web链路的数据库连接异常
- 异步线程执行的数据库操作、@Scheduled定时任务、消息队列消费链路中触发的连接异常,因为不经过DispatcherServlet的请求管线,不会被Web层的全局异常处理器捕获,需要单独配置对应链路的异常监听逻辑:
- 给@Async异步任务实现
AsyncUncaughtExceptionHandler接口兜底 - 给MQ消费者、定时任务手动增加全局try-catch分支,复用相同的文件写入逻辑
- 给@Async异步任务实现
- 避免在异常处理器中引入循环依赖:把文件写入逻辑抽离为无状态的独立工具组件,不要依赖Web层的请求上下文Bean,防止应用启动阶段出现循环引用报错
- 持久化请求日志时要做敏感信息过滤,自动剔除Cookie中的认证凭证、请求体中的密码、密钥类字段,避免敏感数据泄露
4. 最小可用代码示例
@RestControllerAdvice public class GlobalDatabaseExceptionHandler { // 注入独立的文件写入组件 private final FaultRequestFileWriter faultRequestFileWriter; public GlobalDatabaseExceptionHandler(FaultRequestFileWriter faultRequestFileWriter) { this.faultRequestFileWriter = faultRequestFileWriter; } // 匹配数据库连接类异常 @ExceptionHandler({ CannotGetJdbcConnectionException.class, PSQLException.class }) public ResponseEntity<CommonErrorResponse> handleConnectionFault(Exception ex) { // 获取当前请求上下文 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); // 组装故障请求日志 FaultRequestLog logEntry = FaultRequestLog.builder() .requestUri(request.getRequestURI()) .httpMethod(request.getMethod()) .clientIp(request.getRemoteAddr()) .occurTime(LocalDateTime.now()) .rootException(ex.getCause().getMessage()) .build(); // 写入文件系统 faultRequestFileWriter.write(logEntry); // 返回统一的503服务不可用响应 return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE) .body(new CommonErrorResponse("数据库暂时不可用,请稍后重试")); } }
内容的提问来源于stack exchange,提问作者Bhavesh Shah
相关产品推荐
相关产品推荐

