深入理解 AOP:使用 AspectJ 实现对 Maven 依赖中 Jar 包类的织入
2025-04-28 08:07 阅读(18)

网上大部分文章都是在 Spring 框架中使用 AOP 对 Bean 进行织入,而对“其他不被 Spring 容器管理的类的织入”的相关文章很少。本篇文章则是讲解如何借助 AspectJ 和 Maven 插件 aspectj-maven-plugin 来实现对 Maven 依赖中 Jar 包类的织入,以帮助大家实现类似的需求。

在文章展开之前,我想简单交代下事件的背景:某应用因为创建较早,使用的 ORM 框架是 3.2.4 版本的 spring-orm,现在有一个诉求是想在 SQL 执行前对 SQL 进行打标(添加 StatementId 和线程方法堆栈注释),以快速、清楚的知道慢 SQL 和高并发执行 SQL 的方法调用链路,根据它的源码需要在 com.ibatis.sqlmap.engine.execution.SqlExecutor#executeQuery 方法执行前进行织入,但是由于该 ORM 框架并不像 Mybatis 一样支持自定义插件,所以并不能借助这种方式来实现,而且 SqlExecutor 执行器并没有注册成 Bean 被 Spring 框架管理,所以也不能借助 Spring 提供的 AOP 框架来实现。最终在有限的资料中发现能借助 AspectJ 实现对 Maven 依赖中 Jar 包类的织入,接下来我们就来讲解如何实现。

添加依赖和配置插件

借助 AspectJ 在 编译期 实现对 Maven 依赖中 Jar 包类的织入,这与运行时织入(如 Spring AOP 使用的代理机制)不同,编译期织入是在生成的字节码中直接包含切面逻辑,生成的类文件已经包含了切面代码。

首先,需要先添加依赖:

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.8.13</version>
</dependency>

并且在 Maven 的 plugins 标签下添加 aspectj-maven-plugin 插件配置,否则无法实现在编译期织入:


<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>aspectj-maven-plugin</artifactId>
    <version>1.11</version>
    <configuration>
        <!-- 解决与 Lombok 的冲突 -->
        <forceAjcCompile>true</forceAjcCompile>
        <sources/>
        <weaveDirectories>
            <weaveDirectory>${project.build.directory}/classes</weaveDirectory>
        </weaveDirectories>
        <!-- JDK版本 -->
        <complianceLevel>1.8</complianceLevel>
        <source>1.8</source>
        <target>1.8</target>
        <!-- 展示织入信息 -->
        <showWeaveInfo>true</showWeaveInfo>
        <encoding>UTF-8</encoding>
        <!-- 重点!配置要织入的 maven 依赖 -->
        <weaveDependencies>
            <weaveDependency>
                <groupId>org.apache.ibatis</groupId>
                <artifactId>ibatis-sqlmap</artifactId>
            </weaveDependency>
        </weaveDependencies>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>compile</goal>
            </goals>
        </execution>
    </executions>
</plugin>

解决与 Lombok 的冲突配置内容不再解释,详细请看 CSDN: AspectJ和lombok。重点需要关注的配置内容是 weaveDependency 标签:配置织入依赖(详细可参见 Maven: aspectj-maven-plugin 官方文档),也就是说如果我们想对 SqlExecutor 进行织入,那么需要将它对应的 Maven 依赖添加到这个标签下才能生效,否则无法完成织入。

完成以上内容之后,现在去实现对应的拦截器即可。

拦截器实现

拦截器的实现原理非常简单,要织入的方法是 com.ibatis.sqlmap.engine.execution.SqlExecutor#executeQuery,这个方法的签名如下:

public void executeQuery(StatementScope statementScope, Connection conn, String sql, Object[] parameters, int skipResults, int maxResults, RowHandlerCallback callback) throws SQLException;

根据我们的诉求:在 SQL 执行前对 SQL 进行打标,那么可以直接在这个方法的第三个参数 String sql 上打标,以下是拦截器的实现:

@Slf4j
@Aspect
public class SqlExecutorInterceptor {

    private static final int DEFAULT_INDEX = 2;

    @Around("execution(* com.ibatis.sqlmap.engine.execution.SqlExecutor.executeQuery(..))")
    public Object aroundExecuteQuery(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取方法参数
        Object[] args = joinPoint.getArgs();
        String sqlTemplate = "";
        Object arg2 = args[2];
        if (arg2 instanceof String) {
            // 实际的 SQL
            sqlTemplate = (String) arg2;
        }

        if (StringUtils.containsIgnoreCase(sqlTemplate, "select")) {
            try {
                // SQL 声明的 ID
                String mappedStatementId = "";
                Object arg0 = args[0];
                if (arg0 instanceof StatementScope) {
                    StatementScope statementScope = (StatementScope) arg0;
                    MappedStatement statement = statementScope.getStatement();
                    if (statement != null) {
                        mappedStatementId = statement.getId();
                    }
                }
                // 方法调用栈
                String trace = trace();

                // 按顺序创建打标的内容
                LinkedHashMap<String, Object> markingMap = new LinkedHashMap<>();
                markingMap.put("STATEMENT_ID", mappedStatementId);
                markingMap.put("STACK_TRACE", trace);

                String marking = "[SQLMarking] ".concat(markingMap.toString());
                // 先打标后SQL,避免有些平台展示SQL时进行尾部截断,而看不到染色信息
                String markingSql = String.format(" /* %s */ %s", marking, sqlTemplate);

                args[2] = markingSql;
            } catch (Exception e) {
                // 发生异常的话恢复最原始 SQL 保证执行
                args[2] = sqlTemplate;
                log.error(e.getMessage(), e);
            }
        }
        // 正常执行逻辑
        return joinPoint.proceed(args);
    }
}

逻辑上非常简单,获取了 MappedStatementId 和线程的执行堆栈以注释的形式标记在 SELECT 语句前,注意如果大家要 对 INSERT 语句进行打标时,需要将标记打在 SQL 的最后,因为部分插件如 InsertStatementParser 会识别 INSERT,如果注释在前,INSERT 识别会有误报错。最后我们再简单介绍下获取线程堆栈信息的方法,比较简单:

public class SqlExecutorInterceptor {

    private static final int DEFAULT_INDEX = 2;
    
    // ...

    private String trace() {
        StackTraceElement[] stackTraceArray = Thread.currentThread().getStackTrace();
        if (stackTraceArray.length <= DEFAULT_INDEX) {
            return EMPTY;
        }
        LinkedList<String> methodInfoList = new LinkedList<>();
        for (int i = stackTraceArray.length - 1 - DEFAULT_INDEX; i >= DEFAULT_INDEX; i--) {
            StackTraceElement stackTraceElement = stackTraceArray[i];
            String className = stackTraceElement.getClassName();
            // 过滤掉不想看到的方法
            if (!className.startsWith("com.your.package") || className.contains("FastClassBySpringCGLIB")
                    || className.contains("EnhancerBySpringCGLIB") || stackTraceElement.getMethodName().contains("lambda$")
            ) {
                continue;
            }
            // 过滤拦截器相关
            if (className.contains("Interceptor") || className.contains("Aspect")) {
                continue;
            }

            // 只拼接类和方法,不拼接文件名和行号
            String methodInfo = String.format("%s#%s",
                    className.substring(className.lastIndexOf('.') + 1),
                    stackTraceElement.getMethodName()
            );
            methodInfoList.add(methodInfo);
        }

        if (methodInfoList.isEmpty()) {
            return EMPTY;
        }

        // 格式化结果
        StringJoiner stringJoiner = new StringJoiner(" ==> ");
        for (String method : methodInfoList) {
            stringJoiner.add(method);
        }
        return stringJoiner.toString();
    }
}

验证织入

完成以上工作后,我们需要验证拦截器是否织入成功,因为织入是在编译期完成的,所以执行以下 Maven 编译命令即可:

mvn clean compile

在控制台中可以发现如下日志信息提示织入成功:


[INFO] --- aspectj-maven-plugin:1.11:compile (default) @ ---
[INFO] Showing AJC message detail for messages of types: [error, warning, fail]
[INFO] Join point 'method-execution(void com.ibatis.sqlmap.engine.execution.SqlExecutor.executeQuery(com.ibatis.sqlmap.engine.scope.StatementScope, java.sql.Connection, java.lang.String, java.lang.Object[], int, int, com.ibatis.sqlmap.engine.mapping.statement.RowHandlerCallback))' in Type 'com.ibatis.sqlmap.engine.execution.SqlExecutor' (SqlExecutor.java:163) advised by around advice from 'com.your.package.sqlmarking.SqlExecutorInterceptor' (SqlExecutorInterceptor.class(from SqlExecutorInterceptor.java))

并且在相应的 target/classes 目录下的 SqlExecutor.class 文件中也能发现被织入的逻辑:


public class SqlExecutor {

    public void executeQuery(StatementScope statementScope, Connection conn, String sql, Object[] parameters, int skipResults, int maxResults, RowHandlerCallback callback) throws SQLException {
        JoinPoint.StaticPart var10000 = ajc$tjp_0;
        Object[] var24 = new Object[]{statementScope, conn, sql, parameters, Conversions.intObject(skipResults), Conversions.intObject(maxResults), callback};
        JoinPoint var23 = Factory.makeJP(var10000, this, this, var24);
        SqlExecutorInterceptor var26 = SqlExecutorInterceptor.aspectOf();
        Object[] var25 = new Object[]{this, statementScope, conn, sql, parameters, Conversions.intObject(skipResults), Conversions.intObject(maxResults), callback, var23};
        var26.aroundExecuteQuery((new SqlExecutor$AjcClosure1(var25)).linkClosureAndJoinPoint(69648));
    }
    
}

以上,大功告成。

https://www.zuocode.com