Java字节码处理框架ASM设计思想解析

最近进行组内分享时选择了这个Java字节码处理这个主题,特此记录下来。众所周知,Java是一门运行在虚拟机上的语言,在创建之初就是为了"write once ,run anywhere "的目的,为了解决不同架构处理器区别,通过虚拟机来屏蔽不同的各个操作系统之间的区别,其虚拟机上运行的平台中立的二进制文件正是class字节码文件,当然JIT,AOT之类的技术是后来为了让Java运行更快加入的,不过这里我们只是对Java的字节码文件的处理。
ASM是什么呢,ASM是一个Java字节码层次的处理框架。它可以直接对class文件进行增删改的操作,Java中许多的框架的实现正是基于ASM,比如AOP的实现,Java自身的动态代理只能支持接口的形式,而使用ASM就能很方便的扩展到类的代理。可以说ASM就是一把利剑,是深入Java必须学习的一个点。
本章主要通过以下几点来解析ASM

  • ASM的基础使用
  • ASM的设计模式
  • Class的文件格式
  • ASM的源码解析
  • ASM总结

ASM的基础使用

对于ASM的使用最推荐的莫过于官方的 《ASM Guide》,介绍的十分详细。这里先抛砖引玉给出一个比较简单的例子,引出ASM中最重要的三个类。假如我们有一个需求是给一个class文件添加一个field字段,代码如下

public class AddField extends ClassVisitor {

    private String name;
    private int access;
    private String desc;
    private Object value;

    private boolean duplicate;

    public AddField(ClassVisitor cv, String name, int access, String desc, Object value) {
        super(ASM5, cv);
        this.name = name;
        this.access = access;
        this.desc = desc;
        this.value = value;
    }

    @Override
    public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
        if (this.name.equals(name)) {
            duplicate = true;
        }
        return super.visitField(access, name, desc, signature, value);
    }

    @Override
    public void visitEnd() {
        if (!duplicate) {
            FieldVisitor fv = super.visitField(access, name, desc, null, value);
            if (fv != null) {
                fv.visitEnd();
            }
        }
        super.visitEnd();
    }

    public static void main(String[] args) throws Exception {
        String output = System.getProperty("user.dir") + "/libjava/output";
        String classDir = System.getProperty("user.dir") + "/libjava/output/MainActivity.class";
        ClassReader classReader = new ClassReader(new FileInputStream(classDir));
        ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
        ClassVisitor addField = new AddField(classWriter,
                "field",
                Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC + Opcodes.ACC_FINAL,
                Type.getDescriptor(String.class),
                "value"
        );
        classReader.accept(addField, ClassReader.EXPAND_FRAMES);
        byte[] newClass = classWriter.toByteArray();
        File newFile = new File(output, "MainActivity1.class");
        new FileOutputStream(newFile).write(newClass);
    }
}

这段代码是给一个Class文件多加一个字段的操作,在ASM的封装下,这样的操作实现非常简单,下面我们就将学习到ASM是如何做到这一切的。

ASM的设计模式

从前面的ClassVisitor以及accept方法就能看出这是明显的visitor(访问者)模式,visitor模式在Java的设计模式中是一种比较冷门的设计模式,冷门并不是因为这个模式的缺陷,而是相较于一些风格比较明显的比如单例,工厂,观察者模式,访问者模式的实际应用场景是比较少的,后面会介绍到这个模式实际存在的一个不小的问题。
访问者模式主要包含被访问的元素以及访问者两部分,元素一般是不同的类型,不同的访问者对于这些元素的操作一般也是不同的,访问者模式使得用户可以在不修改现有系统的情况下扩展系统的功能,为这些不同类型的元素增加新的操作。
访问者的模式讲解比较好的文章可以看这篇,操作复杂对象结构——访问者模式
一般访问者模式的元素会接受一个访问者的参数,在元素内部这个访问者会直接访问这个元素,说的比较绕,用代码来解释

public interface Employee {

    public void accept(Department handler);
}

首先对于元素类我们提供一个接口,内部的方法就是accept,用来接收一个访问者类

public abstract class Department {

    public abstract void visit(FulltimeEmployee employee);

    public abstract void visit(ParttimeEmployee employee);

}

这个抽象的访问者类提供了两个访问具体元素的抽象方法,然后我们具体的实现元素类和访问者类。

public class FADepartment extends Department {

    @Override
    public void visit(FulltimeEmployee employee) {
        System.out.println("正式员工 : " + employee.getName());
    }

    @Override
    public void visit(ParttimeEmployee employee) {
        System.out.println("临时工 : " + employee.getName());
    }
}

public class FulltimeEmployee implements Employee {

    private String name;
    private String salary;

    public FulltimeEmployee(String name, String salary) {
        this.name = name;
        this.salary = salary;
    }

    @Override
    public void accept(Department handler) {
        handler.visit(this);
    }
}

这个的元素实现类中的accept方法中传进来的是抽象的访问者类,然后将自身转发出去,达到外界访问这个元素的目的。

public class EmployeeList {

    private List<Employee> list = new ArrayList<>();

    public void addEmployee(Employee employee) {
        list.add(employee);
    }

    public void accept(Department department) {
        for (Employee employee : list) {
            employee.accept(department);
        }
    }

    public static void main(String[] args) {
        EmployeeList list = new EmployeeList();
        Employee employee1 = new FulltimeEmployee("张三", "1000");
        Employee employee2 = new ParttimeEmployee("李四", "500");
        Employee employee3 = new ParttimeEmployee("王五", "400");
        list.addEmployee(employee1);
        list.addEmployee(employee2);
        list.addEmployee(employee3);

        Department department1 = new FADepartment();
        list.accept(department1);
    }
}

看上面这个例子,通过这种转发的方式,我们就能在不修改元素的条件下,添加对于元素访问的操作,只需要让元素类accept一个新的访问者就行。这样看起来我们通过accept的转发行为让元素类和操作类进行了解耦,所以这种模式这种模式对于添加新的访问者的操作是符合“开闭原则”的,但是如果我们一旦要增加新的元素时,就会导致所有的访问者类都需要增加相应的访问方法,这是明显违反设计模式,所以访问者模式并不适合元素类频繁变动的场景,这也是访问者模式自身最大的缺陷。
对于Java class文件来说,其中的元素从设计到现在,基本没有变化,仅仅只添加了一些细节,比如泛型的引入,其实Java就是为了保证低版本的兼容性,才导致了一些不合理的存在,比如为了引入泛型,但是又不想破坏兼容性,才有了泛型擦除这样的坑爹操作,相比C#这样的语言,Java的泛型可以说只是伪泛型,但是Java的优势就是兼容性好,对于访问者模式也就是元素类不会随意改动,这个缺陷也就不存在了。

Class的文件格式

前面说到Class文件结构,想要理解ASM的内部运行原理,首先需要了解Class文件,这样才能知道ASM如何对于Class文件进行操作,Class文件作为虚拟机所执行的平台中立文件,内部结构设计的十分的清晰,每一个Class文件都对应着唯一一个类或接口的定义信息。每个Class文件都由以8位为单位的字节流组成,下面是一个Class文件中所包括的项,在Class文件中,各项按照严格顺序连续存放,中间没有任何填充或者对齐作为各项间的分隔符号。

ClassFile { 
    u4 magic; 
    u2 minor_version; 
    u2 major_version; 
    u2 constant_pool_count; 
    cp_info constant_pool[constant_pool_count-1]; 
    u2 access_flags; 
    u2 this_class; 
    u2 super_class; 
    u2 interfaces_count; 
    u2 interfaces[interfaces_count]; 
    u2 fields_count; 
    field_info fields[fields_count]; 
    u2 methods_count; 
    method_info methods[methods_count]; 
    u2 attributes_count; 
    attribute_info attributes[attributes_count]; 
}

在Class文件结构中,上面各项的含义如下:
magic: 作为一个魔数,确定这个文件是否是一个能被虚拟机接受的class文件,值固定为0xCAFEBABE。
minor_version,major_version:分别表示class文件的副,主版本号,不同版本的虚拟机实现支持的Class文件版本号不同。
constant_pool_count:常量池计数器,constant_pool_count的值等于常量池表中的成员数加1。
constant_pool:常量池,constant_pool是一种表结构,包含class文件结构及其子结构中引用的所有字符常量、类或接口名、字段名和其他常量。
access_flags:access_flags是一种访问标志,表示这个类或者接口的访问权限及属性,包括有ACC_PUBLIC,ACC_FINAL,ACC_SUPER等等。
this_class:类索引,指向常量池表中项的一个索引。
super_class:父类索引,这个值必须为0或者是对常量池中项的一个有效索引值,如果为0,表示这个class只能是Object类,只有它是唯一没有父类的类。
interfaces_count:接口计算器,表示当前类或者接口的直接父接口数量。
interfaces[]:接口表,里面的每个成员的值必须是一个对常量池表中项的一个有效索引值。
fields_count:字段计算器,表示当前class文件中fields表的成员个数,每个成员都是一个field_info。
fields:字段表,每个成员都是一个完整的fields_info结构,表示当前类或接口中某个字段的完整描述,不包括父类或父接口的部分。
methods_count:方法计数器,表示当前class文件methos表的成员个数。
methods:方法表,每个成员都是一个完整的method_info结构,可以表示类或接口中定义的所有方法,包括实例方法,类方法,以及类或接口初始化方法。
attributes_count:属性表,其中是每一个attribute_info,包含以下这些属性,InnerClasses,EnclosingMethod,Synthetic,Signature,Annonation等。

ASM的源码解析

前面我们分析了ASM的场景刚好适合访问者模式,同时也是这么做的,这里我们就从accept方法开始分析ASM,限于篇幅,这里省略了accept方法中部分的visitor调用,不过原理都是一样的,可以自行对照源码参考

//ClassReader.java
public void accept(final ClassVisitor classVisitor,
                       final Attribute[] attrs, final int flags) {
        int u = header; // current offset in the class file
        char[] c = new char[maxStringLength]; // buffer used to read strings

        Context context = new Context();
        context.attrs = attrs;
        context.flags = flags;
        context.buffer = c;

        // reads the class declaration
        int access = readUnsignedShort(u);
        String name = readClass(u + 2, c);
        String superClass = readClass(u + 4, c);
        String[] interfaces = new String[readUnsignedShort(u + 6)];
        u += 8;
        for (int i = 0; i < interfaces.length; ++i) {
            interfaces[i] = readClass(u, c);
            u += 2;
        }

        // reads the class attributes
       ......

        // visits the class declaration
        classVisitor.visit(readInt(items[1] - 7), access, name, signature,
                superClass, interfaces);

        // visits the source and debug info
        if ((flags & SKIP_DEBUG) == 0
                && (sourceFile != null || sourceDebug != null)) {
            classVisitor.visitSource(sourceFile, sourceDebug);
        }

        // visits the outer class
        if (enclosingOwner != null) {
            classVisitor.visitOuterClass(enclosingOwner, enclosingName,
                    enclosingDesc);
        }

        // visits the class annotations and type annotations
        if (ANNOTATIONS && anns != 0) {
            for (int i = readUnsignedShort(anns), v = anns + 2; i > 0; --i) {
                v = readAnnotationValues(v + 2, c, true,
                        classVisitor.visitAnnotation(readUTF8(v, c), true));
            }
        }
        if (ANNOTATIONS && ianns != 0) {
            for (int i = readUnsignedShort(ianns), v = ianns + 2; i > 0; --i) {
                v = readAnnotationValues(v + 2, c, true,
                        classVisitor.visitAnnotation(readUTF8(v, c), false));
            }
        }
        if (ANNOTATIONS && tanns != 0) {
            for (int i = readUnsignedShort(tanns), v = tanns + 2; i > 0; --i) {
                v = readAnnotationTarget(context, v);
                v = readAnnotationValues(v + 2, c, true,
                        classVisitor.visitTypeAnnotation(context.typeRef,
                                context.typePath, readUTF8(v, c), true));
            }
        }
        if (ANNOTATIONS && itanns != 0) {
            for (int i = readUnsignedShort(itanns), v = itanns + 2; i > 0; --i) {
                v = readAnnotationTarget(context, v);
                v = readAnnotationValues(v + 2, c, true,
                        classVisitor.visitTypeAnnotation(context.typeRef,
                                context.typePath, readUTF8(v, c), false));
            }
        }

        // visits the attributes
        while (attributes != null) {
            Attribute attr = attributes.next;
            attributes.next = null;
            classVisitor.visitAttribute(attributes);
            attributes = attr;
        }

        // visits the inner classes
        if (innerClasses != 0) {
            int v = innerClasses + 2;
            for (int i = readUnsignedShort(innerClasses); i > 0; --i) {
                classVisitor.visitInnerClass(readClass(v, c),
                        readClass(v + 2, c), readUTF8(v + 4, c),
                        readUnsignedShort(v + 6));
                v += 8;
            }
        }

        // visits the fields and methods
        u = header + 10 + 2 * interfaces.length;
        for (int i = readUnsignedShort(u - 2); i > 0; --i) {
            u = readField(classVisitor, context, u);
        }
        u += 2;
        for (int i = readUnsignedShort(u - 2); i > 0; --i) {
            u = readMethod(classVisitor, context, u);
        }

        // visits the end of the class
        classVisitor.visitEnd();
    }

在accept方法中传进来了一个参数ClassVisitor,然后接着往下看,这里我们省略了解析class文件的过程,整个class的解析过程遵循上一小节中Class的文件格式,通过不断的读取ClassReader的构造函数中class二进制byte[],然后在解析后通过参数classVisitor的抽象visitXXX方法将属性全部转发出去,将其中的visitXXX方法按顺序抽离出来就是

            classVisitor.visit(readInt(items[1] - 7), access, name, signature,superClass, interfaces);
            classVisitor.visitSource(sourceFile, sourceDebug);
            classVisitor.visitOuterClass(enclosingOwner, enclosingName,enclosingDesc);
            classVisitor.visitAnnotation(readUTF8(v, c);
            classVisitor.visitTypeAnnotation(context.typeRef,context.typePath, readUTF8(v, c);
            classVisitor.visitAttribute(attributes);
            classVisitor.visitInnerClass(readClass(v, c),readClass(v + 2, c), readUTF8(v + 4, c),readUnsignedShort(v + 6));
            classVisitor.visitField(access, name, desc,signature, value);
            classVisitor.visitMethod(context.access,context.name, context.desc, signature, exceptions);
            classVisitor.visitEnd();

整个class文件在accept这个方法中,相当于以庖丁解牛的方式,肢解成了上一小节中ClassFile中的每一项,而这个classVisitor实际上是一个抽象类。

public abstract class ClassVisitor 

有抽象类,自然会有实现类,也就是前面的ClassWriter。

public class ClassWriter extends ClassVisitor

在前面我们添加字段的示例之中,在最后处理完class文件后,通过toByteArray方法生成了新的class文件,所以这里有两个疑问,第一,这个toByteArray 在这里担当了什么角色,为什么能够生成新的Class文件,第二,在参数转发出来之后,我们是如何通过visit系列方法改变整个Class文件的。带着这两个问题,我们继续往下看。
首先看一下 toByteArray 这个方法的前半部分

    public byte[] toByteArray() {
        if (index > 0xFFFF) {
            throw new RuntimeException("Class file too large!");
        }
        // computes the real size of the bytecode of this class
        int size = 24 + 2 * interfaceCount;
        int nbFields = 0;
        FieldWriter fb = firstField;
        while (fb != null) {
            ++nbFields;
            size += fb.getSize();
            fb = (FieldWriter) fb.fv;
        }
        int nbMethods = 0;
        MethodWriter mb = firstMethod;
        while (mb != null) {
            ++nbMethods;
            size += mb.getSize();
            mb = (MethodWriter) mb.mv;
        }
        int attributeCount = 0;
        ......
        if (ClassReader.ANNOTATIONS && itanns != null) {
            ++attributeCount;
            size += 8 + itanns.getSize();
            newUTF8("RuntimeInvisibleTypeAnnotations");
        }
        if (attrs != null) {
            attributeCount += attrs.getCount();
            size += attrs.getSize(this, null, 0, -1, -1);
        }
        size += pool.length;
        // allocates a byte vector of this size, in order to avoid unnecessary
        // arraycopy operations in the ByteVector.enlarge() method
        ByteVector out = new ByteVector(size);

同样限于篇幅,这里省略了中间的一部分计算size的部分,作者在里面给出了注释,计算Class字节的真实大小,这个字节怎么计算呢,这里首先给了一个24,想必是前面ClassFile中的魔数以及版本号之类的字节数大小,然后在后面分别添加了字段,方法,属性等等的大小,通过这个最终的size,构造了一个ByteVetcor。

    public byte[] toByteArray() {
        ......
        ByteVector out = new ByteVector(size);
        out.putInt(0xCAFEBABE).putInt(version);
        out.putShort(index).putByteArray(pool.data, 0, pool.length);
        int mask = Opcodes.ACC_DEPRECATED | ACC_SYNTHETIC_ATTRIBUTE
                | ((access & ACC_SYNTHETIC_ATTRIBUTE) / TO_ACC_SYNTHETIC);
        out.putShort(access & ~mask).putShort(name).putShort(superName);
        out.putShort(interfaceCount);
        for (int i = 0; i < interfaceCount; ++i) {
            out.putShort(interfaces[i]);
        }
        out.putShort(nbFields);
        fb = firstField;
        while (fb != null) {
            fb.put(out);
            fb = (FieldWriter) fb.fv;
        }
        out.putShort(nbMethods);
        mb = firstMethod;
        while (mb != null) {
            mb.put(out);
            mb = (MethodWriter) mb.mv;
        }
        out.putShort(attributeCount);
        if (bootstrapMethods != null) {
            out.putShort(newUTF8("BootstrapMethods"));
            out.putInt(bootstrapMethods.length + 2).putShort(
                    bootstrapMethodsCount);
            out.putByteArray(bootstrapMethods.data, 0, bootstrapMethods.length);
        }
        if (ClassReader.SIGNATURES && signature != 0) {
            out.putShort(newUTF8("Signature")).putInt(2).putShort(signature);
        }
        if (sourceFile != 0) {
            out.putShort(newUTF8("SourceFile")).putInt(2).putShort(sourceFile);
        }
        if (sourceDebug != null) {
            int len = sourceDebug.length;
            out.putShort(newUTF8("SourceDebugExtension")).putInt(len);
            out.putByteArray(sourceDebug.data, 0, len);
        }
        if (enclosingMethodOwner != 0) {
            out.putShort(newUTF8("EnclosingMethod")).putInt(4);
            out.putShort(enclosingMethodOwner).putShort(enclosingMethod);
        }
        if ((access & Opcodes.ACC_DEPRECATED) != 0) {
            out.putShort(newUTF8("Deprecated")).putInt(0);
        }
        if ((access & Opcodes.ACC_SYNTHETIC) != 0) {
            if ((version & 0xFFFF) < Opcodes.V1_5
                    || (access & ACC_SYNTHETIC_ATTRIBUTE) != 0) {
                out.putShort(newUTF8("Synthetic")).putInt(0);
            }
        }
        if (innerClasses != null) {
            out.putShort(newUTF8("InnerClasses"));
            out.putInt(innerClasses.length + 2).putShort(innerClassesCount);
            out.putByteArray(innerClasses.data, 0, innerClasses.length);
        }
        if (ClassReader.ANNOTATIONS && anns != null) {
            out.putShort(newUTF8("RuntimeVisibleAnnotations"));
            anns.put(out);
        }
        if (ClassReader.ANNOTATIONS && ianns != null) {
            out.putShort(newUTF8("RuntimeInvisibleAnnotations"));
            ianns.put(out);
        }
        if (ClassReader.ANNOTATIONS && tanns != null) {
            out.putShort(newUTF8("RuntimeVisibleTypeAnnotations"));
            tanns.put(out);
        }
        if (ClassReader.ANNOTATIONS && itanns != null) {
            out.putShort(newUTF8("RuntimeInvisibleTypeAnnotations"));
            itanns.put(out);
        }
        if (attrs != null) {
            attrs.put(this, null, 0, -1, -1, out);
        }
        if (invalidFrames) {
            anns = null;
            ianns = null;
            attrs = null;
            innerClassesCount = 0;
            innerClasses = null;
            bootstrapMethodsCount = 0;
            bootstrapMethods = null;
            firstField = null;
            lastField = null;
            firstMethod = null;
            lastMethod = null;
            computeMaxs = false;
            computeFrames = true;
            invalidFrames = false;
            new ClassReader(out.data).accept(this, ClassReader.SKIP_FRAMES);
            return toByteArray();
        }
        return out.data;
    }

方法的后半部分就是给这个ByteVector开始填数据了,按照ClassFile的格式依次填入数据,和在ClassReader中读取的顺序一模一样,这样生成的Class结构就是符合虚拟机规范的,也能被虚拟机正常的加载。
那么这些数据是从哪里来的呢,聪明的读者现在肯定猜到了,就是前面这些抽象的visit系列方法,它们从原始Class文件中依次读取了数据然后又作为参数传了进来,我们先看第一个方法

    @Override
    public final void visit(final int version, final int access,
            final String name, final String signature, final String superName,
            final String[] interfaces) {
        this.version = version;
        this.access = access;
        this.name = newClass(name);
        thisName = name;
        if (ClassReader.SIGNATURES && signature != null) {
            this.signature = newUTF8(signature);
        }
        this.superName = superName == null ? 0 : newClass(superName);
        if (interfaces != null && interfaces.length > 0) {
            interfaceCount = interfaces.length;
            this.interfaces = new int[interfaceCount];
            for (int i = 0; i < interfaceCount; ++i) {
                this.interfaces[i] = newClass(interfaces[i]);
            }
        }
    }

这个visit方法是转发的第一个方法,其中的几个参数分别表示了原Class文件中的编译版本,访问标志(是否是final,static,abstract等等),类或接口名,泛型,父类以及实现的接口,可以看到这个方法只是单纯的赋了一下值,并没有什么其他的操作,这些值在最后生成新Class文件的时候会再次写入到byte数组中。
再看一个有点复杂的,ASM对于method的处理

    @Override
    public final MethodVisitor visitMethod(final int access, final String name,
                                           final String desc, final String signature, final String[] exceptions) {
        return new MethodWriter(this, access, name, desc, signature,
                exceptions, computeMaxs, computeFrames);
    }

直接代理给了 MethodWriter 类进行处理,继续跟进去构造函数

    MethodWriter(final ClassWriter cw, final int access, final String name,
            final String desc, final String signature,
            final String[] exceptions, final boolean computeMaxs,
            final boolean computeFrames) {
        super(Opcodes.ASM5);
        if (cw.firstMethod == null) {
            cw.firstMethod = this;
        } else {
            cw.lastMethod.mv = this;
        }
        cw.lastMethod = this;
      ......
}

前面分析Class的文件结构的时候,方法表是一个数组类型,一个类中存在多个方法是很正常的,这个visitMethod同样会被调用多次,这里重点关注这个 firstMethodcw.lastMethod.mv,在ClassWriter类中其实并没有明显的数组类型来存放多方法结构,这也是ASM对于method和field写的比较模糊的一点,每一次的 visitMethod 的调用都是 new 一个 MethodWriter对象,第一次 cw.firstMethod == null,于是给了 firstMethodlastMethod 赋值当前,既有头又有尾,有些双向链表的意思,但是这里在第二次进来的时候走的是 cw.lastMethod.mv = this ,而 cw.lastMethod 指向的是刚才的 MethodWriter 对象,这就很清晰了,每一个 MethodWriter 的 mv 字段保留着下一个的引用,实际上只是单向的链表。
然后我们看一下在输出新的Class文件的时候

        mb = firstMethod;
        while (mb != null) {
            mb.put(out);
            mb = (MethodWriter) mb.mv;
        }

刚才我们构造的 method 链表又重新的一个个的写进去了,而我们每一次的visitMethod 都会加长这个链表。
ASM就是通过这种方式来修改Class结构的,当我们想要加一个方法的时候,只需要多调用一次 visitMethod 方法,而当我们想删除其中一个方法的时候,只需要 return null,让这个MethodWrite 对象不会被构建即可。

ASM总结

ClassReader通过读取整个class文件,作为访问者模式中的元素类,而ClassWrite作为ClassVisitor的实现类,是一个特殊的具体访问者,通过一个accept方法将两个连接起来,并在对应的visitXXX系列方法中决定最终元素的属性,而且元素的访问操作是可以进行链式转发的,这样我们既可以拥有ClassWrite的class文件生成能力,也能在自定义的visitor中实现我们想要对于class元素的处理。整体来说,ASM是一款十分优秀的字节码处理的框架,访问者模式十分适合对于class这样的格式基本不会改变的元素形式。
这一章主要是介绍ASM对于单个class文件的处理,对于一个完整的Java工程来说,我们想要处理的class文件可能是整个项目所编译出来的全部,后面会带来一篇hook编译过程来实现class处理的文章,两篇结合起来就能实现许多的功能,动态代理,无侵入式埋点,热修复框架都将不是遥不可及。

参考文献

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,087评论 18 139
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,292评论 18 399
  • 原文链接http://www.cnblogs.com/liuhaorain/p/3747470.html 本文中很...
    cmlong_阅读 386评论 2 0
  • 隔壁二班的语文课在预备铃声响时,就会按时地响起一片掌声,我都羡慕嫉妒了。 那是陈校长的课——偶尔经过,陈校长那如鸣...
    秋笏笑月阅读 247评论 0 2
  • 遥望星河守苍穹,银汉迢迢不可期。 盈盈难寻终不遇,始是夜半无语时。
    巴图鲁阅读 168评论 0 1