ASM 简介
1. 简介
在本文中,我们将了解如何使用ASM 库通过添加字段、添加方法和更改现有方法的行为来操作现有 Java 类。
2. 依赖
我们需要将 ASM 依赖项添加到我们的pom.xml中:
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>6.0</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-util</artifactId>
<version>6.0</version>
</dependency>
我们可以从 Maven Central获取最新版本的asm 和asm-util 。
3. ASM API 基础
ASM API 为转换和生成提供了两种与 Java 类交互的方式:基于事件的和基于树的。
3.1. 基于事件的 API
此 API 很大程度上基于Visitor模式,并且在感觉上类似于处理 XML 文档的 SAX 解析模型。它的核心由以下组件组成:
- ClassReader - 帮助读取类文件,是转换类的开始
- ClassVisitor – 提供读取原始类文件后用于转换类的方法
- *ClassWriter——*用于输出类变换的最终产物
在ClassVisitor中,我们拥有所有访问者方法,我们将使用这些方法来接触给定 Java 类的不同组件(字段、方法等)。我们通过提供ClassVisitor 的子类来实现给定类中的任何更改来做到这一点。
由于需要保持与 Java 约定和生成的字节码有关的输出类的完整性,这个类需要一个严格的顺序来调用它的方法来生成正确的输出。
基于事件的 API 中的ClassVisitor方法按以下顺序调用:
visit
visitSource?
visitOuterClass?
( visitAnnotation | visitAttribute )*
( visitInnerClass | visitField | visitMethod )*
visitEnd
3.2. 基于树的 API
该 API 是一个更加面向对象的API,类似于处理 XML 文档的 JAXB 模型。
它仍然基于基于事件的 API,但它引入了ClassNode根类。此类用作类结构的入口点。
4. 使用基于事件的 ASM API
我们将使用 ASM 修改java.lang.Integer类。此时我们需要掌握一个基本概念:ClassVisitor类包含创建或修改类的所有部分所需的所有访问者方法。
我们只需要重写必要的访问者方法来实现我们的更改。让我们从设置必备组件开始:
public class CustomClassWriter {
static String className = "java.lang.Integer";
static String cloneableInterface = "java/lang/Cloneable";
ClassReader reader;
ClassWriter writer;
public CustomClassWriter() {
reader = new ClassReader(className);
writer = new ClassWriter(reader, 0);
}
}
我们以此为基础将Cloneable接口添加到 stock Integer类中,并且我们还添加了一个字段和一个方法。
4.1. 使用字段
让我们创建我们的ClassVisitor,我们将使用它向Integer类添加一个字段:
public class AddFieldAdapter extends ClassVisitor {
private String fieldName;
private String fieldDefault;
private int access = org.objectweb.asm.Opcodes.ACC_PUBLIC;
private boolean isFieldPresent;
public AddFieldAdapter(
String fieldName, int fieldAccess, ClassVisitor cv) {
super(ASM4, cv);
this.cv = cv;
this.fieldName = fieldName;
this.access = fieldAccess;
}
}
接下来,让我们重写visitField方法,我们首先检查我们计划添加的字段是否已经存在,并设置一个标志来指示状态。
我们仍然必须将方法调用转发给父类——这需要发生,因为类中的每个字段都调用了visitField方法。未能转发呼叫意味着不会将任何字段写入类。
此方法还允许我们修改现有字段的可见性或类型:
@Override
public FieldVisitor visitField(
int access, String name, String desc, String signature, Object value) {
if (name.equals(fieldName)) {
isFieldPresent = true;
}
return cv.visitField(access, name, desc, signature, value);
}
我们首先检查前面的visitField方法中设置的标志,然后再次调用visitField方法,这次提供了名称、访问修饰符和描述。此方法返回FieldVisitor的一个实例。
visitEnd方法是按访问者方法顺序调用的最后一个方法。这是执行字段插入逻辑的推荐位置。 然后,我们需要在这个对象上调用visitEnd方法来表示我们已经完成了对这个字段的访问:
@Override
public void visitEnd() {
if (!isFieldPresent) {
FieldVisitor fv = cv.visitField(
access, fieldName, fieldType, null, null);
if (fv != null) {
fv.visitEnd();
}
}
cv.visitEnd();
}
确保使用的所有 ASM 组件都来自org.objectweb.asm包很重要——许多库在内部使用 ASM 库,并且 IDE 可以自动插入捆绑的 ASM 库。
我们现在在addField方法中使用我们的适配器,使用我们添加的字段获得java.lang.Integer的转换版本:
public class CustomClassWriter {
AddFieldAdapter addFieldAdapter;
//...
public byte[] addField() {
addFieldAdapter = new AddFieldAdapter(
"aNewBooleanField",
org.objectweb.asm.Opcodes.ACC_PUBLIC,
writer);
reader.accept(addFieldAdapter, 0);
return writer.toByteArray();
}
}
我们已经覆盖了visitField和visitEnd方法。
与字段相关的所有事情都发生在visitField方法中。这意味着我们还可以通过更改传递给visitField方法的所需值来修改现有字段(例如,将私有字段转换为公共字段)。
4.2. 使用方法
在 ASM API 中生成整个方法比在类中的其他操作更复杂。这涉及大量低级字节码操作,因此超出了本文的范围。
然而,对于大多数实际用途,我们可以修改现有方法以使其更易于访问(也许将其公开以便可以覆盖或重载)或修改类以使其可扩展。
让我们公开 toUnsignedString 方法:
public class PublicizeMethodAdapter extends ClassVisitor {
public PublicizeMethodAdapter(int api, ClassVisitor cv) {
super(ASM4, cv);
this.cv = cv;
}
public MethodVisitor visitMethod(
int access,
String name,
String desc,
String signature,
String[] exceptions) {
if (name.equals("toUnsignedString0")) {
return cv.visitMethod(
ACC_PUBLIC + ACC_STATIC,
name,
desc,
signature,
exceptions);
}
return cv.visitMethod(
access, name, desc, signature, exceptions);
}
}
就像我们对字段修改所做的那样,我们只是拦截访问方法并更改我们想要的参数。
在这种情况下,我们使用org.objectweb.asm.Opcodes包中的访问修饰符来更改方法的可见性。然后我们插入我们的ClassVisitor:
public byte[] publicizeMethod() {
pubMethAdapter = new PublicizeMethodAdapter(writer);
reader.accept(pubMethAdapter, 0);
return writer.toByteArray();
}
4.3. 使用类
与修改方法一样,我们通过拦截适当的访问者方法来修改类。在这种情况下,我们拦截visit,这是访问者层次结构中的第一个方法:
public class AddInterfaceAdapter extends ClassVisitor {
public AddInterfaceAdapter(ClassVisitor cv) {
super(ASM4, cv);
}
@Override
public void visit(
int version,
int access,
String name,
String signature,
String superName, String[] interfaces) {
String[] holding = new String[interfaces.length + 1];
holding[holding.length - 1] = cloneableInterface;
System.arraycopy(interfaces, 0, holding, 0, interfaces.length);
cv.visit(V1_8, access, name, signature, superName, holding);
}
}
我们重写visit方法,将Cloneable接口添加到Integer类支持的接口数组中。我们将其插入,就像我们的适配器的所有其他用途一样。
5. 使用修改后的类
所以我们修改了Integer类。现在我们需要能够加载和使用修改后的类。
除了简单地将writer.toByteArray的输出作为类文件写入磁盘之外,还有其他一些方法可以与我们自定义的Integer类进行交互。
5.1. 使用TraceClassVisitor
ASM 库提供了TraceClassVisitor实用程序类,我们将使用它来内省修改后的类。这样我们就可以确认我们的改变已经发生了。
因为TraceClassVisitor是ClassVisitor,所以我们可以将它用作标准ClassVisitor的替代品:
PrintWriter pw = new PrintWriter(System.out);
public PublicizeMethodAdapter(ClassVisitor cv) {
super(ASM4, cv);
this.cv = cv;
tracer = new TraceClassVisitor(cv,pw);
}
public MethodVisitor visitMethod(
int access,
String name,
String desc,
String signature,
String[] exceptions) {
if (name.equals("toUnsignedString0")) {
System.out.println("Visiting unsigned method");
return tracer.visitMethod(
ACC_PUBLIC + ACC_STATIC, name, desc, signature, exceptions);
}
return tracer.visitMethod(
access, name, desc, signature, exceptions);
}
public void visitEnd(){
tracer.visitEnd();
System.out.println(tracer.p.getText());
}
我们在这里所做的是使用 TraceClassVisitor 调整我们传递给我们之前的PublicizeMethodAdapter的ClassVisitor。
现在所有的访问都将使用我们的跟踪器完成,然后它可以打印出转换后的类的内容,显示我们对其所做的任何修改。
虽然 ASM 文档声明TraceClassVisitor可以打印到提供给构造函数的PrintWriter,但这似乎在最新版本的 ASM 中无法正常工作。
幸运的是,我们可以访问类中的底层打印机,并且能够在我们重写的visitEnd方法中手动打印出跟踪器的文本内容。
5.2. 使用 Java 插装
这是一个更优雅的解决方案,它允许我们通过Instrumentation 在更接近的级别上使用 JVM 。
为了检测java.lang.Integer类,我们编写了一个代理,它将被配置为 JVM 的命令行参数。代理需要两个组件:
- 一个实现名为premain的方法的类
- ClassFileTransformer 的实现,我们将有条件地提供我们类的修改版本
public class Premain {
public static void premain(String agentArgs, Instrumentation inst) {
inst.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(
ClassLoader l,
String name,
Class c,
ProtectionDomain d,
byte[] b)
throws IllegalClassFormatException {
if(name.equals("java/lang/Integer")) {
CustomClassWriter cr = new CustomClassWriter(b);
return cr.addField();
}
return b;
}
});
}
}
我们现在使用 Maven jar 插件在 JAR 清单文件中定义我们的premain实现类:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<configuration>
<archive>
<manifestEntries>
<Premain-Class>
com.blogdemo.examples.asm.instrumentation.Premain
</Premain-Class>
<Can-Retransform-Classes>
true
</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
到目前为止,构建和打包我们的代码会生成我们可以作为代理加载的 jar。在假设的“ YourClass.class ”中使用我们定制的Integer类:
java YourClass -javaagent:"/path/to/theAgentJar.jar"