大家好,我是 方圆 。网上大部分文章都是在 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));
}
}
以上,大功告成。