深入理解 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));
    }
    
}

以上,大功告成。


巨人的肩膀

相关推荐
丘山子1 小时前
一些鲜为人知的 IP 地址怪异写法
前端·后端·tcp/ip
CopyLower1 小时前
在 Spring Boot 中实现 WebSockets
spring boot·后端·iphone
.生产的驴2 小时前
SpringBoot 封装统一API返回格式对象 标准化开发 请求封装 统一格式处理
java·数据库·spring boot·后端·spring·eclipse·maven
景天科技苑2 小时前
【Rust】Rust中的枚举与模式匹配,原理解析与应用实战
开发语言·后端·rust·match·enum·枚举与模式匹配·rust枚举与模式匹配
追逐时光者3 小时前
MongoDB从入门到实战之Docker快速安装MongoDB
后端·mongodb
豌豆花下猫3 小时前
Python 潮流周刊#99:如何在生产环境中运行 Python?(摘要)
后端·python·ai
嘻嘻嘻嘻嘻嘻ys3 小时前
《Spring Boot 3 + Java 17:响应式云原生架构深度实践与范式革新》
前端·后端
异常君3 小时前
线程池隐患解析:为何阿里巴巴拒绝 Executors
java·后端·代码规范
mazhimazhi3 小时前
GC垃圾收集时,居然还有用户线程在奔跑
后端·面试