Aspect无法拦截Spring Data JPA的JDBC Statement执行求助
我开发了一个基于Aspect的库,用于扫描所有Statement执行并处理SQL语句,核心Aspect代码如下:
@Pointcut("target(java.sql.Statement)") public void statement() {} @AfterReturning("!within(AnalyzerAspect) && statement() && args(sql)") public void after(JoinPoint jp, String sql) throws Throwable { // some logic }
该Aspect在原生JDBC和Spring-Jdbc场景下能正常工作,对应的AspectJ Maven插件配置如下:
<plugin> <groupId>dev.aspectj</groupId> <artifactId>aspectj-maven-plugin</artifactId> <version>${aspect.compiler.plugin.version}</version> <configuration> <weaveDependencies> <weaveDependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> </weaveDependency> <weaveDependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> </weaveDependency> </weaveDependencies> <aspectLibraries> <aspectLibrary> <groupId>edu.ifmo.diploma</groupId> <!-- My library --> <artifactId>db-analyzer</artifactId> </aspectLibrary> </aspectLibraries> <complianceLevel>${maven.compiler.target}</complianceLevel> <showWeaveInfo>true</showWeaveInfo> <Xlint>ignore</Xlint> <encoding>UTF-8</encoding> </configuration> <executions> <execution> <goals> <goal>compile</goal> <goal>test-compile</goal> </goals> </execution> </executions> <dependencies> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjtools</artifactId> <version>${aspectj.version}</version> </dependency> </dependencies> </plugin>
使用Spring-Jdbc执行查询时,Aspect能正常拦截:
jdbcTemplate.query("SELECT * FROM person", new BeanPropertyRowMapper<>(Person.class));
但通过Spring Data JPA Repository执行原生SQL查询时:
public interface PersonRepository extends JpaRepository<Person, UUID> { @Query(nativeQuery = true, value = "SELECT * FROM person") List<Person> findAll(); }
personRepository.findAll();
尽管插件中配置了Hibernate织入,Aspect却无法拦截该执行。我的核心需求是改进该库,使其不依赖具体数据库框架,仅拦截JDBC Statement的执行(所有数据库框架底层都会用到它),希望得到更优的实现方案建议。
拦截失效原因
当前Aspect的切点仅匹配目标对象为Statement且入参为String sql的方法,但Hibernate底层存在两个关键问题:
- 实际使用的是JDBC驱动实现的
Statement子类或动态代理类,不在编译时织入的覆盖范围内; - Hibernate更多调用
PreparedStatement的无参执行方法(如executeQuery()),无法被args(sql)的切点匹配。
优化方案
1. 扩展Aspect切点范围,覆盖所有Statement执行场景
修改切点,覆盖Statement的所有子类(PreparedStatement、CallableStatement)及所有执行方法,同时适配不同类型Statement的SQL获取逻辑:
@Pointcut("target(java.sql.Statement) || target(java.sql.PreparedStatement) || target(java.sql.CallableStatement)") public void jdbcStatement() {} @AfterReturning("!within(AnalyzerAspect) && jdbcStatement() && execution(* execute*(..))") public void afterStatementExecution(JoinPoint jp) throws Throwable { Statement statement = (Statement) jp.getTarget(); String sql = null; // 适配普通Statement Object[] args = jp.getArgs(); if (args != null && args.length > 0 && args[0] instanceof String) { sql = (String) args[0]; } // 适配PreparedStatement(通过反射获取SQL,需兼容不同JDBC驱动) else if (statement instanceof PreparedStatement) { try { Field sqlField = PreparedStatement.class.getDeclaredField("sql"); sqlField.setAccessible(true); sql = (String) sqlField.get(statement); } catch (Exception e) { // 处理反射失败,可添加驱动适配逻辑 } } if (sql != null) { // 执行你的SQL处理逻辑 } }
2. 切换为AspectJ加载时织入(LTW)
编译时织入仅能处理项目编译阶段的类和配置的依赖包,无法覆盖第三方JAR中的动态加载类。使用加载时织入可在类加载阶段对所有JDBC相关类进行织入,无需依赖具体框架配置:
- 添加LTW依赖:
<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>${aspectj.version}</version> </dependency>
- 创建
META-INF/aop.xml声明Aspect:
<aspectj> <weaver options="-showWeaveInfo"> <include within="java.sql..*"/> <include within="edu.ifmo.diploma..*"/> <!-- 你的Aspect所在包 --> </weaver> <aspects> <aspect name="edu.ifmo.diploma.AnalyzerAspect"/> <!-- 你的Aspect全类名 --> </aspects> </aspectj>
- 配置JVM启动参数:
-javaagent:/path/to/aspectjweaver-${aspectj.version}.jar,Spring Boot项目可通过插件配置:
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <agents> <agent>${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar</agent> </agents> </configuration> </plugin>
3. 替代方案:JDBC代理驱动(无框架依赖)
如果希望完全脱离AOP框架,可实现JDBC代理驱动,直接在JDBC层面拦截所有Statement执行,通用性更强:
核心思路是代理JDBC的Connection和Statement,拦截执行方法:
public class ProxyDriver implements Driver { private final Driver delegate; static { try { DriverManager.registerDriver(new ProxyDriver()); } catch (SQLException e) { throw new RuntimeException(e); } } public ProxyDriver() throws SQLException { this.delegate = DriverManager.getDriver("jdbc:mysql://"); // 替换为真实驱动URL前缀 } @Override public Connection connect(String url, Properties info) throws SQLException { Connection realConn = delegate.connect(url.replace("jdbc:proxy:", "jdbc:"), info); return Proxy.newProxyInstance( Connection.class.getClassLoader(), new Class[]{Connection.class}, (proxy, method, args) -> { if ("createStatement".equals(method.getName())) { Statement realStmt = (Statement) method.invoke(realConn, args); return createStmtProxy(realStmt); } else if ("prepareStatement".equals(method.getName())) { String sql = (String) args[0]; PreparedStatement realPStmt = (PreparedStatement) method.invoke(realConn, args); return createPStmtProxy(realPStmt, sql); } return method.invoke(realConn, args); } ); } private Statement createStmtProxy(Statement realStmt) { return Proxy.newProxyInstance( Statement.class.getClassLoader(), new Class[]{Statement.class}, (proxy, method, args) -> { if (method.getName().startsWith("execute")) { String sql = args != null && args.length > 0 ? (String) args[0] : null; if (sql != null) { // 执行SQL处理逻辑 } } return method.invoke(realStmt, args); } ); } private PreparedStatement createPStmtProxy(PreparedStatement realPStmt, String sql) { return Proxy.newProxyInstance( PreparedStatement.class.getClassLoader(), new Class[]{PreparedStatement.class}, (proxy, method, args) -> { if (method.getName().startsWith("execute")) { // 执行SQL处理逻辑,直接使用传入的sql参数 } return method.invoke(realPStmt, args); } ); } // 实现Driver接口其他方法,直接委托给delegate }
使用时将JDBC URL改为jdbc:proxy:mysql://xxx,即可自动拦截所有JDBC操作。
总结
- 若坚持使用AspectJ,优先选择加载时织入(LTW),配合扩展后的切点,解决编译时织入范围不足的问题;
- 追求最高兼容性和无框架依赖,推荐使用JDBC代理驱动,直接在JDBC层面拦截,完全满足"不依赖具体数据库库"的需求。
内容的提问来源于stack exchange,提问作者Maxim Rybalchenko

