AMS4使用指南
实战java虚拟机
基本概念
- 内部名
已编译类不包含Package 和 Import 部分,因此,所有类名都必须是完全限定的。一个类的内部名就是这个类的完全限定名,其中的点号用斜线代替。例如, String 的内部名为 java/lang/String
。 - 类型描述符
String的类型描述符为 Ljava/lang/String;
- 方法描述符:用一个字符串描述一个方法的参数类型和返回类型
ClassVisitor
用于生成和变转已编译类的都是基于 ClassVisitor 抽象类的,如下类图:
- Opcodes接口定义了一些常量:尤其是版本号,访问标识符,字节码等信息
- ClassReader类
分析
以字节数组形式给出的已编译类,并针对在其 accept 方法参数中传送的 ClassVisitor 实例,调用相应的 visitXxx 方法。这个类可以看作一个事件产生器
- ClassWriter 类是 ClassVisitor 抽象类的一个子类,它直接以二进制形式
生成编译后的类
。它会生成一个字节数组形式的输出,其中包含了已编译类,可以用toByteArray方法来提取。这个类可以看作一个事件使用器
。- ClassVisitor类将它收到的所有方法调用都委托给另一个 ClassVisitor 类。 这个类可以看作一个事件筛选器。ClassWriter,它负责Class文件的输出和生成。ClassVisitor在进行字段和方法处理的时候会委托FieldVisitor和Met
通过ClassVisitor 的方法可以直观的调用类图中其他部分。如:通过visitAnnotation、 visitField 和 visitMethod 方法它们分别返回AnnotationVisitor、 FieldVisitor 和 MethodVisitor。
public abstract class ClassVisitor {
/**
* @param api : Opcodes#ASM4 或者 Opcodes#ASM5
*/
public ClassVisitor(final int api){
this(api, null);
};
public ClassVisitor(final int api, final ClassVisitor cv) {}
/**
*
* @param version :jdk版本
* @param access pulbic,...staitc,final, ACC_ABSTRACT:抽象类 ,ACC_INTERFACE:接口,ACC_ANNOTATION:注解,ACC_ENUM:枚举
* @param name 类名,注意packagename 以 java/lang/String 形式。
* @param signature ---泛型信息
* @param superName 父类
* @param interfaces 接口数组
*/
public final void visit(final int version, final int access,
final String name, final String signature, final String superName,
final String[] interfaces){}
public void visitSource(String source, String debug){}
public void visitOuterClass(String owner, String name, String desc){}
public AnnotationVisitor visitAnnotation(String desc, boolean visible){}
public void visitInnerClass(String name, String outerName,String innerName, int access) {}
public FieldVisitor visitField(int access, String name, String desc,String signature, Object value) {}
public MethodVisitor visitMethod(int access, String name, String desc,String signature, String[] exceptions) {}
public void visitEnd() {}
}
ClassVisitor 类的方法必须按以下顺序调用(在这个类的 Javadoc 中规定
):
visit
--------------------------------首先调用visit
visitSource?-------------------------?次调用visitSource的
visitOuterClass?----------------------?次调用visitOuterClass
( visitAnnotation | visitAttribute )*----------*次调用visitAnnotation和visitAttribute
( visitInnerClass | visitField | visitMethod )*--*次调用visitInnerClass,visitField,visitMethod
visitEnd-----------------------------调用结束
分析类(ClassReader)
在分析一个已经存在的类时,惟一必需的组件是ClassReader 组件。用一个例子来说明:假设希望打印一个类的内容,类似简化版javap.
第一步是编写 ClassVisitor类的一个子类, 打印它所访问的类的相关信息。
public class ClassPrinter extends ClassVisitor {
public ClassPrinter() {
super(ASM5);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
System.out.println(name + " extends " + superName + " {");
}
@Override
public FieldVisitor visitField(int access, String name, String desc,String signature, Object value) {
System.out.println(" " + desc + " " + name);
return null;
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
System.out.println(" " + name + desc);
return null;
}
@Override
public void visitEnd() {
System.out.println("}");
}
}
第二步是将这个ClassPrinter 与一个ClassReader 组件合并在一起。
@Test
public void testClassPrinter() throws IOException {
ClassReader cr = new ClassReader("java/lang/Runnable");
cr.accept(new ClassPrinter(),0);
}
输出结果:
java/lang/Runnable extends java/lang/Object {
run()V
}
ClassReader-API
public class ClassReader {
//通过inputstream构造ClassReader
public ClassReader(final InputStream is) throws IOException {
this(readClass(is, false));
}
/**
* @param name: 如java/lang/Runnable,或者java.lang.Runnable
*/
public ClassReader(final String name) throws IOException {
this(readClass(
//通过类加载器.加载类的二进制流
ClassLoader.getSystemResourceAsStream(name.replace('.', '/')
+ ".class"), true));
}
/**
* @param close: true to close the input stream after reading.
*/
private static byte[] readClass(final InputStream is, boolean close)
throws IOException {
}
}
生成类(ClassWriter)
为生成一个类,惟一必需的组件是 ClassWriter 组件。
例如:要生成如下接口
package cn.jhs.asm;
public interface InterfaceA extends Runnable {
int LESS = -1;
int EQUAL = 0;
int GREATER = 1;
int compareTo(Object var1);
}
使用如下代码:
@Test
public void testClassWriter() throws IOException {
String clazzName = "cn/jhs/asm/InterfaceA";
ClassWriter cw = new ClassWriter(0);
//定义一个接口
cw.visit(V1_8, ACC_PUBLIC | ACC_INTERFACE, clazzName, null, "java/lang/Object", new String[]{"java/lang/Runnable"});
//接口中字段访问修饰符都是 public static final
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "LESS", "I",null, new Integer(-1)).visitEnd();
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "EQUAL", "I",null, new Integer(0)).visitEnd();
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "GREATER", "I",null, new Integer(1)).visitEnd();
//接口中方法都是 public abstract ; `jdk8支持多个static非抽象方法和一个default方法`
cw.visitMethod(ACC_PUBLIC + ACC_ABSTRACT, "compareTo","(Ljava/lang/Object;)I", null, null).visitEnd();
cw.visitEnd();//通知 cw:这个类已经结束
//将生成的类,存储到磁盘上
FileOutputStream out = new FileOutputStream("out/" + clazzName + ".class");
out.write(cw.toByteArray());
out.close();
}
上述的代码中,将生成的类文件InterfaceA.class
,存储到磁盘上,也可以直接通过ClassLoader来动态加载它:
a).defineClass实现,参照上述demo,extends ClassLoader
Class c = myClassLoader.defineClass("cn.jhs.asm.InterfaceA", cw.toByteArray());
b).重写findClass实现
class StubClassLoader extends ClassLoader {
@Override
protected Class findClass(String name)throws ClassNotFoundException {
if (name.endsWith("_Stub")) {
ClassWriter cw = new ClassWriter(0);
...
byte[] b = cw.toByteArray();
return defineClass(name, b, 0, b.length);
}
return super.findClass(name);
}
}
如果你正在编写动态代理类生成器或方面编织器,使用重写
findClass
的方式,统一命名代理类名称为$PxoyXxx
。
转换类
到目前为止, ClassReader 和 ClassWriter 组件都是单独使用的。当这些组件一同使用时,如在执行方法的代码前织入开始时间,在代码返回前织入结束时间,达到一个简易的logger功能
,这些操作才使得ASM变得真正有实际意义起来。
第一步是将 ClassReader 产生的事件转给 ClassWriter
。
byte[] b1 = ...;
ClassWriter cw = new ClassWriter(0);
ClassReader cr = new ClassReader(b1);
cr.accept(cw, 0);
byte[] b2 = cw.toByteArray(); // b2 和 b1 表示同一个类
下一步是在类读取器和类写入器之间引入一个 ClassVisitor
。
byte[] b1 = ...;
ClassWriter cw = new ClassWriter(0);
// cv 将所有事件转发给 cw
ClassVisitor cv = new ClassVisitor(ASM5, cw) { };
ClassReader cr = new ClassReader(b1);
cr.accept(cv, 0);//改为接收accept cv ,
byte[] b2 = cw.toByteArray(); // b2 与 b1 表示同一个类
上述的代码体系结构,就变动如下图所示。
到目前为止,上述代码,仍然没有改变原有类,我们只需重写一些ClassVisitor
的一些方法,筛选一些事件就可以了做到改变类,例如下面ClassVisitor子类
public class ChangeVersionAdapter extends ClassVisitor {
public ChangeVersionAdapter(int i, ClassVisitor cv) {
super(ASM5, cv);
}
@Override
public void visit(int i, int i1, String s, String s1, String s2, String[] strings) {
//强制将version该为1.2
super.visit(V1_2, i1, s, s1, s2, strings);
}
}
子类ChangeVersionAdapter
仅重写了visit
方法,所有的方法都会被转发给构造器的cv
,只有visit
方法除外,在转发它时,将版本号进行了修改。
优化
子类ChangeVersionAdapter
仅修改了原类的四个字节,但是在使用上面代码时,整个b1均被分析,并利用相应的事件从头构建了b2,这种效率并不高。如果b1中不被转换的部分直接复制到b2,不对其分析,也不生成相应的事件,其效率会高很多。ASM自动为方法执行了这已优化:
- 在ClassReader组件accept方法有参数ClassVisitor,当检测到这个ClassVisitor返回的MethodVisitor来自一个ClassWriter(认为方法没有发生改变),这意味着方法的内容不会被转换。
- 这种情况下,ClassReader组件不会分析这个方法的内容,不会生成相应事件,只是复制ClassWriter中表示这个方法的字节数组。
如果 ClassReader 和 ClassWriter 组件拥有对对方的引用,则由它们进行这种优化
,可设置如下
byte[] b1 = ...
ClassReader cr = new ClassReader(b1);
ClassWriter cw = new ClassWriter(cr, 0);//ClassWriter 添加 ClassReader 引用
ChangeVersionAdapter ca = new ChangeVersionAdapter(cw);
cr.accept(ca, 0);
byte[] b2 = cw.toByteArray();
执行这一优化后,由于ChangeVersionAdapter 没有转换任何方法,所以以上代码的速度可以达到之前代码的两倍。对于转换部分或全部方法的常见转换,这一速度升幅度可能要小一些,但仍然是很可观的:实际上在 10%到 20%的量级。
遗憾的是,这一优化需要将原类中定义的所有常量都复制到转换后的类中。对于那些增加字段、方法或指令的转换来说,这一点不成问题,但对于那些要移除或重命名
许多类成员的转换来说,这一优化将导致类文件大于未优化
时的情况。因此,建议仅对“增加性”
转换应用这一优化。
使用转换后的类
转换后的类b2
可以存储在磁盘上,或者使用ClassLoader加载。但在ClassLoader中执行的类转换只能转换这个加载器加载的类。如果希望转换所有类
,则必须将转换放到ClassFileTransformer
内部:
public class AsmPremainClass {
public static void premain(String args, Instrumentation inst) {
inst.addTransformer(new ClassFileTransformer() {
public byte[] transform(ClassLoader l, String name, Class c,
ProtectionDomain d, byte[] b)
throws IllegalClassFormatException {
ClassReader cr = new ClassReader(b);
ClassWriter cw = new ClassWriter(cr, 0);
ClassVisitor cv = new ChangeVersionAdapter(cw);
cr.accept(cv, 0);
return cw.toByteArray();
}
});
}
}
Instrumentation 新功能
它的作用,简单的来说就是:在JVM执行main函数前动点手脚,自己实现一个Instrumentation的代理,在得到虚拟机载入的正常的类的字节码后,通过ASM提供的类生成转换后的字节码再丢给虚拟机。Instrumentation 的代理必须实现方法 premain,且该代理类必须定义在MANIFEST文件中
Manifest-Version: 1.0
Premain-Class: xxx.xxx.AsmPremainClass
成员变换
移除成员
public class RemoveMethodAdapter extends ClassVisitor {
private String mName;
private String mDesc;
public RemoveMethodAdapter(int i, ClassVisitor cv, String methodName, String methodDesc) {
super(ASM5, cv);
this.mName = mName;
this.mDesc = mDesc;
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
if (name.equals(mName) && desc.equals(mDesc)) {
// 不要委托至下一个访问器 -> 这样将移除该方法
return null;
}
return super.visitMethod(access, name, desc, signature, exceptions);
}
}
增加成员
新调用放在 visitEnd 方法中, 那这个字段将总会被添加(除非增加显式条件),因为这个方法总会被调用
。
public class AddFieldAdapter extends ClassVisitor {
private int fAcc;
private String fName;
private String fDesc;
private boolean isFieldPresent;
public AddFieldAdapter(ClassVisitor cv, int fAcc, String fName,
String fDesc) {
super(ASM5, cv);
this.fAcc = fAcc;
this.fName = fName;
this.fDesc = fDesc;
}
@Override
public FieldVisitor visitField(int access, String name, String desc,
String signature, Object value) {
if (name.equals(fName)) {
//如果已经存在字段,设置isFieldPresent为true
isFieldPresent = true;
}
return cv.visitField(access, name, desc, signature, value);
}
@Override
public void visitEnd() {
if (!isFieldPresent) {
//防止生成重复字段
FieldVisitor fv = cv.visitField(fAcc, fName, fDesc, null, null);
if (fv != null) {
fv.visitEnd();
}
}
cv.visitEnd();
}
}
工具
除了 ClassVisitor 类和相关的 ClassReader、 ClassWriter 组件之外, ASM 还在org.objectweb.asm.util
包中提供了几个工具
Type
@Test
public void testType() throws NoSuchMethodException {
// 内部名:java/lang/String
System.out.println(Type.getType(String.class).getInternalName());
// 描述符 :Ljava/lang/String;
System.out.println(Type.getType(String.class).getDescriptor());
// 描述符 :I
System.out.println(Type.INT_TYPE.getDescriptor());
Method method = Runnable.class.getMethod("run", new Class[]{});
// 方法描述符 : ()V
System.out.println(Type.getMethodDescriptor(method));
// 方法的参数类型 :Type.INT_TYPE []
System.out.println(Type.getArgumentTypes("(I)V"));
// 方法的返回值 : Type.VOID
System.out.println(Type.getReturnType("(I)V"));// Type.VOID_TYPE 对象
}
TraceClassVisitor
要确认所生成或转换后的类符合你的预期, ClassWriter 返回的字母数组并没有什么真正的用处,因为它对人类来说是不可读的。如果有文本表示形式,那使用起来就容易多了。这正是TraceClassVisitor 类提供的东西。
ClassWriter cw = new ClassWriter(0);
TraceClassVisitor cv = new TraceClassVisitor(cw, new PrintWriter(System.out)); //将类的信息打印在控制台
cv.visit(...);
...
cv.visitEnd();
byte b[] = cw.toByteArray();
CheckClassAdapter
ClassWriter类并不会核实对其方法的调用顺序是否恰当,以及参数是否有效。因此,有可能会生成一些被 Java虚拟机验证器拒绝的无效类。
ClassWriter cw = new ClassWriter(0);
TraceClassVisitor tcv = new TraceClassVisitor(cw, new PrintWriter(System.out)); //将类的信息打印在控制台
CheckClassAdapter cv = new CheckClassAdapter(tcv);
cv.visit(...);
...
cv.visitEnd();
byte b[] = cw.toByteArray();
ASMifier
这个类为 TraceClassVisitor 工具ᨀ供了一种替代后端(该工具在默认情况下使用Textifier 后端,生成如上所示类型的输出)。这个后端使 TraceClassVisitor 类的每个方法都会打印用于调用它的 Java 代码。
例:
@Test
public void testTraceClassVisitor() throws IOException {
ClassWriter cw = new ClassWriter(0);
TraceClassVisitor tcv = new TraceClassVisitor(cw,new ASMifier(), new PrintWriter(System.out));
CheckClassAdapter cv = new CheckClassAdapter(tcv);
cv.visit(V1_8, ACC_PUBLIC | ACC_INTERFACE, "MifierDemo", null, "java/lang/Object", new String[]{"java/lang/Runnable"});
cv.visitEnd();
}
它会生成如下打印信息:
import java.util.*;
import org.objectweb.asm.*;
public class MifierDemoDump implements Opcodes {
public static byte[] dump () throws Exception {
ClassWriter cw = new ClassWriter(0);
FieldVisitor fv;
MethodVisitor mv;
AnnotationVisitor av0;
cw.visit(52, ACC_PUBLIC + ACC_INTERFACE, "MifierDemo", null, "java/lang/Object", new String[] { "java/lang/Runnable" });
cw.visitEnd();
return cw.toByteArray();
}
}