
本文介绍如何通过 AspectJ 织入 JDBC 底层接口(Statement 和 PreparedStatement),实现跨框架(JDBC、Spring JDBC、Hibernate、Spring Data JPA)的 SQL 全链路监控,无需依赖具体 ORM 实现。
本文介绍如何通过 aspectj 织入 jdbc 底层接口(`statement` 和 `preparedstatement`),实现跨框架(jdbc、spring jdbc、hibernate、spring data jpa)的 sql 全链路监控,无需依赖具体 orm 实现。
要真正实现“拦截所有 SQL 查询”,关键在于面向 JDBC 规范本身进行切面织入,而非依赖上层框架(如 JdbcTemplate 或 Repository 接口)的方法签名。因为无论 Spring Data JPA 还是 MyBatis,最终都会通过 java.sql.Statement 或其子接口(尤其是 PreparedStatement)执行 SQL——这是 JDBC 驱动层的统一出口。
✅ 正确的切入点设计
原始方案仅切 target(java.sql.Statement) 并捕获 args(sql),存在两大缺陷:
- PreparedStatement 的 execute()/executeQuery() 等方法不接收 SQL 字符串作为参数(SQL 已在 prepareStatement(String sql) 时绑定),因此 args(sql) 匹配失败;
- PreparedStatement.toString() 默认不返回原始 SQL(多数驱动返回内部对象描述),需调用 toString() 的增强实现或反射获取实际 SQL(见下文注意事项)。
改进后的切点应覆盖两类核心执行行为:
@Pointcut("target(java.sql.Statement)")
public void statement() {}
@Pointcut("target(java.sql.PreparedStatement)")
public void preparedStatement() {}
// 拦截 Statement.execute* 方法(如 executeQuery("SELECT ..."))
@AfterReturning(
pointcut = "!within(AnalyzerAspect) && statement() && args(sql)",
returning = "sql"
)
public void onStatementExecution(JoinPoint jp, String sql) {
processSql(sql);
}
// 拦截 PreparedStatement.execute* 方法(无 SQL 参数,需从目标对象提取)
@AfterReturning(
pointcut = "!within(AnalyzerAspect) && preparedStatement()",
returning = "ret"
)
public void onPreparedStatementExecution(JoinPoint jp, Object ret) {
if (!jp.getSignature().getName().startsWith("execute")) {
return;
}
PreparedStatement ps = (PreparedStatement) jp.getTarget();
String sql = extractSqlFromPreparedStatement(ps);
processSql(sql);
}? 关键:从 PreparedStatement 提取真实 SQL
不同 JDBC 驱动对 PreparedStatement.toString() 的实现各异。为可靠获取 SQL,推荐以下策略(按优先级):
使用驱动特有 API(推荐)
如 PostgreSQL 驱动支持 ((PgPreparedStatement) ps).getPreparedQuery().getSql();MySQL Connector/J 8+ 可通过反射访问 cachedSql 字段。-
通用兜底方案(反射 + 兼容性处理)
private String extractSqlFromPreparedStatement(PreparedStatement ps) { try { // 尝试标准 JDBC 4.2+ 的 getMetaData().getURL() 无帮助,改用反射 Field field = ps.getClass().getDeclaredField("sql"); field.setAccessible(true); Object sqlObj = field.get(ps); return sqlObj != null ? sqlObj.toString() : ps.toString(); } catch (Exception e) { // 回退到 toString(),并记录警告 log.warn("Failed to extract SQL from PreparedStatement: {}", ps.toString(), e); return ps.toString(); } } 启用驱动日志(开发/测试阶段)
如 HikariCP + PostgreSQL 可配置 logLevel=DEBUG,辅助验证切面是否命中。
⚙️ Maven 配置要点
原配置中织入 spring-jdbc 和 hibernate-core 是无效且冗余的——这些库不包含 Statement 实现,真正需织入的是JDBC 驱动本身(如 postgresql、mysql-connector-java):
<weaveDependencies>
<weaveDependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</weaveDependency>
<!-- 若使用 MySQL,替换为 -->
<!-- <groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId> -->
</weaveDependencies>✅ 原因:PreparedStatement 的 executeQuery() 方法实现在驱动 JAR 中(如 org.postgresql.jdbc.PgPreparedStatement),只有织入驱动字节码,才能拦截其执行逻辑。
? 注意事项与最佳实践
- 避免循环织入:!within(AnalyzerAspect) 是必须的,防止切面自身方法被重复拦截导致 StackOverflow。
- 性能敏感场景慎用反射:生产环境建议针对主流驱动(PostgreSQL/MySQL/H2)做白名单优化,跳过反射。
- 事务上下文关联:可通过 TransactionSynchronizationManager 获取当前事务 ID,将 SQL 日志与事务绑定,便于追踪。
- SQL 脱敏处理:在 processSql() 中自动过滤 password=、token= 等敏感关键词,符合安全规范。
- 兼容性验证:务必在目标 JDK 版本(如 Java 17+)和驱动版本下测试 aspectj-maven-plugin 的织入效果,避免因模块化(JPMS)导致类加载问题。
通过以上方案,你的 db-analyzer 库即可真正解耦于任何数据访问框架,稳定捕获所有经 JDBC 执行的 SQL,成为数据库操作的“透明观察者”。

















