深入理解 AOP:使用 AspectJ 实现对 Maven 依赖中 Jar 包类的织入

大家好,我是 方圆 。网上大部分文章都是在 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 使用的代理机制)不同,编译期织入是在生成的字节码中直接包含切面逻辑,生成的类文件已经包含了切面代码。

首先,需要先添加依赖:

xml 复制代码
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.8.13</version>
</dependency>

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

xml 复制代码
<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,这个方法的签名如下:

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

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

java 复制代码
@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 识别会有误报错。最后我们再简单介绍下获取线程堆栈信息的方法,比较简单:

java 复制代码
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 编译命令即可:

shell 复制代码
mvn clean compile

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

xml 复制代码
[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 文件中也能发现被织入的逻辑:

java 复制代码
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));
    }
    
}

以上,大功告成。


巨人的肩膀

相关推荐
励志成为架构师13 分钟前
跟小白一起领悟Thread——如何开启一个线程(上)
java·后端
hankeyyh15 分钟前
golang 易错点-slice copy
后端·go
考虑考虑24 分钟前
Redis事务
redis·后端
Victor3561 小时前
Redis(6)Redis的单线程模型是如何工作的?
后端
Victor3561 小时前
Redis(7)Redis如何实现高效的内存管理?
后端
David爱编程2 小时前
进程 vs 线程到底差在哪?一文吃透操作系统视角与 Java 视角的关键差异
后端
smileNicky12 小时前
SpringBoot系列之从繁琐配置到一键启动之旅
java·spring boot·后端
David爱编程13 小时前
为什么必须学并发编程?一文带你看懂从单线程到多线程的演进史
java·后端
long31613 小时前
java 策略模式 demo
java·开发语言·后端·spring·设计模式
rannn_11114 小时前
【Javaweb学习|黑马笔记|Day1】初识,入门网页,HTML-CSS|常见的标签和样式|标题排版和样式、正文排版和样式
css·后端·学习·html·javaweb