1. 转转公众号发文有感:
加Log就卡?不加Log就瞎?”——这个插件治好了我的精神内耗
2.核心功能就是对某条存在的日志,进行控制是否打印日志
3. 使用到的工具: idea自定义插件 + maven自定义插件 + asm增强框架 + apollo 配置中心功能
4. 转转流程图:
5. 然后我就自己做了一个maven自定义插件 + asm 来复现下修改编译后的class功能:
使用到了maven3.9 + jdk17
6. 插件项目pom:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ddj</groupId>
<artifactId>log-enhance</artifactId>
<version>1.0.0</version>
<packaging>maven-plugin</packaging>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>9.2</version>
</dependency>
<!-- ASM 字节码操作库 -->
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-commons</artifactId>
<version>9.2</version>
</dependency>
<!-- Maven Plugin 注解支持 -->
<dependency>
<groupId>org.apache.maven.plugin-tools</groupId>
<artifactId>maven-plugin-annotations</artifactId>
<version>3.9.0</version>
</dependency>
<!-- Maven Plugin API (包含 AbstractMojo) -->
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-plugin-api</artifactId>
<version>3.9.9</version> <!-- 或使用与你的 Maven 版本兼容的版本 -->
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.34</version>
<scope>provided</scope>
</dependency>
<!-- <!– SLF4J 门面 –>-->
<!-- <dependency>-->
<!-- <groupId>org.slf4j</groupId>-->
<!-- <artifactId>slf4j-api</artifactId>-->
<!-- <version>1.7.36</version>-->
<!-- </dependency>-->
<!-- <!– Logback 实现 –>-->
<!-- <dependency>-->
<!-- <groupId>ch.qos.logback</groupId>-->
<!-- <artifactId>logback-classic</artifactId>-->
<!-- <version>1.2.12</version>-->
<!-- </dependency>-->
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-plugin-plugin</artifactId>
<version>3.13.1</version>
<configuration>
<goalPrefix>enhance</goalPrefix> <!-- 自定义前缀 -->
</configuration>
<executions>
<execution>
<id>default-descriptor</id>
<phase>process-classes</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
7. 核心类:
maven 插件需要继承的类:
package com.ddj.plugin;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
@Mojo(name = "enhance", defaultPhase = LifecyclePhase.PROCESS_CLASSES)
public class LogWrapperMojo extends AbstractMojo {
@Parameter(defaultValue = "${project.build.outputDirectory}")
private File outputDirectory;
public void execute() throws MojoExecutionException {
try {
Files.walk(outputDirectory.toPath())
.filter(p -> p.toString().endsWith(".class"))
.parallel()
.forEach(p -> {
try {
byte[] original = Files.readAllBytes(p);
byte[] transformed = LogTransformer.transform(original);
Files.write(p, transformed);
} catch (IOException e) {
getLog().error("Failed to transform class: " + p, e);
}
});
} catch (IOException e) {
throw new MojoExecutionException("Failed to process classes", e);
}
}
}
LogTransformer.class:
package com.ddj.plugin;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
public class LogTransformer {
public static byte[] transform(byte[] classData) {
ClassReader cr = new ClassReader(classData);
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
ClassVisitor cv = new LogClassVisitor(cw);
cr.accept(cv, ClassReader.EXPAND_FRAMES);
return cw.toByteArray();
}
}
8. asm类:
package com.ddj.plugin;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import static org.objectweb.asm.Opcodes.ACC_ABSTRACT;
/**
*
* @date 2025-04-17
*/
public class LogClassVisitor extends ClassVisitor {
private String className;
public LogClassVisitor(ClassVisitor cv) {
super(Opcodes.ASM9, cv);
}
@Override
public void visit(int version, int access, String name, String signature,
String superName, String[] interfaces) {
this.className = name;
super.visit(version, access, name, signature, superName, interfaces);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
// 跳过构造方法、静态块、抽象方法
if (name.equals("<init>") || name.equals("<clinit>") || (access & ACC_ABSTRACT) != 0) {
return mv;
}
return new LogMethodVisitor(api, mv, access, name, desc,className);
}
}
方法修改:
package com.ddj.plugin;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.AdviceAdapter;
public class LogMethodVisitor extends AdviceAdapter {
private final String methodName;
private final String className;
private int startTimeVarIndex; // 局部变量索引,用于存储开始时间
private int durationVarIndex; // 局部变量索引,用于存储耗时
protected LogMethodVisitor(int api, MethodVisitor mv, int access,
String name, String desc, String className) {
super(api, mv, access, name, desc);
this.methodName = name;
this.className = className;
}
@Override
protected void onMethodEnter() {
// 记录方法开始时间
// 获取当前时间并存储在局部变量中
visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
startTimeVarIndex = newLocal(Type.LONG_TYPE); // 分配一个局部变量来存储开始时间,并获取其索引
storeLocal(startTimeVarIndex); // 使用正确的索引存储值
}
@Override
protected void onMethodExit(int opcode) {
if (opcode != ATHROW) { // 跳过异常退出的情况
// 计算方法耗时
// 获取当前时间
visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
// 加载开始时间
loadLocal(startTimeVarIndex); // 使用正确的索引加载值
// 计算时间差(当前时间 - 开始时间)
visitInsn(LSUB); // 长整型减法
// 将结果存储在局部变量中
durationVarIndex = newLocal(Type.LONG_TYPE); // 分配一个局部变量来存储耗时,并获取其索引
storeLocal(durationVarIndex); // 使用正确的索引存储值
// 插入日志代码
visitLdcInsn(className);
visitMethodInsn(INVOKESTATIC, "org/slf4j/LoggerFactory", "getLogger",
"(Ljava/lang/String;)Lorg/slf4j/Logger;", false);
// 加载耗时(从局部变量中)
loadLocal(durationVarIndex); // 使用正确的索引加载值
// 将长整型转换为字符串
visitMethodInsn(INVOKESTATIC, "java/lang/Long", "toString", "(J)Ljava/lang/String;", false);
// 构建日志消息
visitTypeInsn(NEW, "java/lang/StringBuilder");
visitInsn(DUP);
visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
visitLdcInsn("Method " + methodName + " took ");
visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
visitLdcInsn("ms");
visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
// 调用日志方法
visitMethodInsn(INVOKEINTERFACE, "org/slf4j/Logger", "info", "(Ljava/lang/String;)V", true);
}
}
}
9. 然后将该模块 maven install到本地库里面,在另外一个java 17 相同maven 中,引入插件,就可以了
另外一个项目中,我是多模块,所以在需要模块中,引入增强,不能在父模块配置,不然会报错
<!-- 子模块 依赖 --> <build> <plugins> <plugin> <groupId>com.ddj</groupId> <artifactId>log-enhance</artifactId> <version>1.0.0</version> <executions> <execution> <phase>process-classes</phase> <goals> <goal>enhance</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
10:然后在这个模块,点击idea的 maven插件,package
11. 原始文件:
11. 增强后的class文件:
public void publish(UserRegisterEvent event) {
long var2 = System.currentTimeMillis();
log.info("------ {}", LocalDateTime.now());
long var4 = System.currentTimeMillis() - var2;
LoggerFactory.getLogger("net/csig/vs/infra/acl/publisher/UserRegisterEventPublisherImpl").info(Long.toString(var4).append((new StringBuilder()).append("Method publish took ")).append("ms").toString());
}
12. 最主要的是继承了AdviceAdapter的类:LogMethodVisitor
方法:OnMethodEnter(), OnMethodExit(int opcode);
13. 核心逻辑验证完毕
未完成:
1. 从apollo 获取是否开启日志配置开关,根据是否开启进行修改两个asm方法
2. idea插件修改apollo 配置中心参数
3. onMethodEnter(), OnMethodExit()两个核心方法的asm编程,进行代码增删