ASM核心API-类

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();
    }
}

猜你喜欢

转载自blog.csdn.net/it_freshman/article/details/81152286