在上一篇Java核心: 注解处理器我们提到,通过实现AbstractProcessor,并调用javac -processor能够生成代码来实现特殊逻辑。不过它存在两个明显的问题:
- 只能新增源文件来扩展逻辑,无法修改现有的类或方法
- 必须有一个单独的编译过程,调用javac -processor或者用Maven的annotationProcessor,不适用于已经编译好的jar
Java核心: 使用asm操作字节码讲了怎么使用asm修改类的字节码,解决了上面的第1个问题。不过即使用ASM,仍然需要自己创建ClassReader读取class文件,自定义ClassLoader加载字节码,虽然不需要单独编译,但是这些操作是需要额外做的。
instrumentation提供的能力是可以注册字节码转换器,在类加载时先将字节码传递给字节码转换器,转换之后才被真正的加载。使用者来并不感知这个过程,只是在启动程序的命令中添加一个-javaagent参数即可,应该说使用体验还是不错的。
java -javaagent:AsmToString.jar=hoho -cp ./swing-1.0-SNAPSHOT.jar com.keyniu.asm.diagnostic.DiagnosticMain
1. 定义和使用
接下来我们要做的是定义一个最简单的instrumentation实现,仅仅是打印-javaagent后带的参数,以及执行过程加载的所有的类、它所属的ClassLoader。首先要做的是创建工程,后续用于打包成agent的jar,这里我们命名为AsmToString.jar。这里使用mvn archetype:generate创建工程
mvn archetype:generate \
-DarchetypeGroupId=org.apache.maven.archetypes \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DarchetypeVersion=1.4 \
-DgroupId=com.keyniu.agent \
-DartifactId=AsmToString \
-Dversion=0.1 \
-Dpackage=com.keyniu.agent \
-DinteractiveMode=false
1. 实现Premain
现在我们可以在工程下定义我们的Premain类ToStringGenerator,这个类的核心是实现public static void premain(final String arg, Instrumentation ins)方法,这里我们只打印参数arg,以及传递给字节码转换器的信息。使用Instrumentation.addTransformer注册字节码转换器ClassFileTransformer实现类。
package org.keyniu.agent;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
public class ToStringGenerator {
public static void premain(final String arg, Instrumentation ins) {
System.out.println("arg: " + arg);
ins.addTransformer(new ClassFileTransformer() {
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
System.out.println("ClassLoader: " + loader.getName());
System.out.println("className: " + className);
System.out.println("classBeingRedefined: " + classBeingRedefined);
return classfileBuffer;
}
});
}
}
在继续之前我们来仔细看看ClassFileTransformer.tranform的定义,它有5个参数
参数 | 说明 | 举例 |
loader | 加载当前要进行字节码转换的类的类加载器 | app |
className | 当前要进行字节码转换的类的类名 | org/objectweb/asm/util/TraceRecordComponentVisitor |
classBeingRefefined | 被重定义(Instrumentation.redefineClasses)的类的Class对象,否则是null | null |
protectionDomain | 目标类的安全权限 | null |
classfileBuffer | 转换之前的字节码 | 字节码数组 |
2. 定义MANIFEST.MF
为了让-javaagent能使用这个jar,需要在MANIFEST.MF里添加Premain-Class的定义。在Maven的build -> plugins -> plugin配置maven-jar-plugin。
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<manifest>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
<addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>org.keyniu.agent.ToStringGenerator</Premain-Class>
</manifestEntries>
</archive>
</configuration>
</plugin>
这里我们重点关注和的配置,我们看一下每个配置的作用。addDefaultImplementationEntries用于在MANIFEST.MF文件中生成
Implementation-Title: AsmToString
Implementation-Version: 1.0-SNAPSHOT
addDefaultSpecificationEntries会在MANIFEST.MF里记录Specification相关信息
Specification-Title: AsmToString
Specification-Version: 1.0
addClassPath则会在MANIFEST.MF文件中添加一行Class-Path
Class-Path: lib/dependency1.jar lib/dependency2.jar
manifestEntries用于添加我们自定义的属性,这里是添加Premain-Class属性以及它对应的类信息。现在可以通过mvn package打包了。
3. 使用javaagent
在拿到agent的jar包之后,就可以在另外一个工程里使用这个jar包了。复用在Java核心: 使用asm操作字节码里定义的类Account,代码如下,只是定义了一个remain字段、transfer方法。
package com.keyniu.asm;
import com.keyniu.asm.diagnostic.Diagnostic;
import com.keyniu.asm.lombok.ToString;
@ToString
public class Account {
private int remain = 99;
@Diagnostic
public void transfer(String sb, int amount) {
System.out.println("transfer to " + sb + " amount: " + amount);
try {
Thread.sleep((int) (Math.random() * 1000));
} catch (InterruptedException e) {
}
}
}
用来测试的maif方法也极其简单,仅仅是创建Account实例,打印toString方法的输出,因为我们并没有定义toString方法,所以输出是Object默认的com.keyniu.asm.Account@3feba861
package com.keyniu.testagent;
import com.keyniu.asm.Account;
public class TestAgent {
public static void main(String[] args) {
Account account = new Account();
System.out.println(account.toString());
}
}
现在可以使用-javaagent来测试执行了,我们看看执行的命令以及返回的输出
java -javaagent:D:/Workspace/HelloJava17/AsmToString-1.0-SNAPSHOT.jar=hoho -classpath ./swing-1.0-SNAPSHOT.jar com.keyniu.testagent.TestAgent
2. 实战: 生成toString方法
上一节中我们创建了一个javaagent的jar,整个流程已经跑通了,只是这个agent本身并没有什么实际意义,仅仅是打印我们加载的所有类而已。现在我们让他变得有用一点,为注解了@ToString的类生成toString方法,案例中Account类标注了@ToString,关于ToString的代码,asm的更详细介绍,可以看Java核心: 使用asm操作字节码。这里我们默认读者是会asm的基本操作的。
1.引入asm依赖
首先要做的是在AsmToString工程里引入asm类库的依赖。
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>9.7</version> <!-- 使用时请检查是否有更新的版本 -->
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-util</artifactId>
<version>9.7</version> <!-- 使用时请检查是否有更新的版本 -->
</dependency>
2. 定义ToStringClassVisitor
代码完全复用Java核心: 使用asm操作字节码里的ToStringClassVisitor类,代码如下
package org.keyniu.agent;
import org.objectweb.asm.*;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
public class ToStringClassVisitor extends ClassVisitor {
private boolean isAnnotated = false;
private Map<String, String> fields = new LinkedHashMap<>();
private String className;
@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);
}
protected ToStringClassVisitor(ClassVisitor classVisitor) { // classVisitor接收一个ClassWriter用于生成字节码
super(Opcodes.ASM9, classVisitor);
}
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
if ("Lcom/keyniu/asm/lombok/ToString;".equals(descriptor)) {
isAnnotated = true;
}
return super.visitAnnotation(descriptor, visible);
}
@Override
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
fields.put(name, descriptor);
return super.visitField(access, name, descriptor, signature, value);
}
@Override
public void visitEnd() {
if (isAnnotated && fields.size() > 0) {
MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PUBLIC, "toString", "()Ljava/lang/String;", null, null);
mv.visitCode();
// 创建StringBuilder并调用构造函数
mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
mv.visitInsn(Opcodes.DUP);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
// 拼接类名,比如 Account(
mv.visitLdcInsn(className.substring(className.lastIndexOf("/") + 1) + "(");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
Iterator<Map.Entry<String, String>> it = fields.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String> kv = it.next();
String fieldName = kv.getKey();
String fieldDesc = kv.getValue();
mv.visitVarInsn(Opcodes.ALOAD, 0); // 载入this
mv.visitFieldInsn(Opcodes.GETFIELD, className, fieldName, fieldDesc);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(" + fieldDesc + ")Ljava/lang/StringBuilder;", false);
if (it.hasNext()) {
mv.visitLdcInsn(",");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
}
}
// 拼接右括号,Account(字段=值)
mv.visitLdcInsn(")");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
mv.visitInsn(Opcodes.ARETURN);
mv.visitMaxs(0, 0);
mv.visitEnd();
}
super.visitEnd();
}
}
3.修改ToStringGenerator实现
package org.keyniu.agent;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
public class ToStringGenerator {
public static void premain(final String arg, Instrumentation ins) {
System.out.println("arg: " + arg);
ins.addTransformer(new ClassFileTransformer() {
public byte[] transform(ClassLoader load, String clazz, Class<?> redefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
ClassReader cr = new ClassReader(classfileBuffer);
// 写字节码
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);
ClassVisitor cv = new ToStringClassVisitor(cw);
// 跑事件流
cr.accept(cv, ClassWriter.COMPUTE_FRAMES); // 0表示不计算最大堆栈大小和最大局部变量表大小
// 获取修改后的字节码并写入到文件
byte[] transformedBytes = cw.toByteArray();
return transformedBytes;
}
});
}
}
4. 查看运行输出
我们将AsmToString的jar拷到D:盘下,使用如下命令执行TestAgent,我们看到Accout的toString已经生成了并打印Account类中的字段。
java -javaagent:D:/AsmToString-1.0-SNAPSHOT.jar=hoho -classpath ./swing-1.0-SNAPSHOT.jar com.keyniu.testagent.TestAgent