程序员手撕 JVM虚拟机

3. Class文件结构(2)——用java代码实现解析Clas

2021-02-03  本文已影响0人  源码之路

本文,我们编写JAVA程序来解析class文件,读者注意,阅读本文前先详细了解Class文件结构,可参考笔者前一篇文章:Class文件结构(1)—手动解析每一个字节,你看不懂

代码地址:https://github.com/congzhizhi/classFile,我就贴测试代码了,github上项目路径下的test有测试代码。

1.项目架构

项目架构设计

根据技术架构图搭建项目的框架。先定义对应class文件结构中各项的类型,如常量池、字段表、方法表、属性表、U2、U4,再定义各项的解析器,并使用责任链模式完成class文件结构各项的解析工作。

首先,我么将Class文件抽象成类,即ClassFile类,代码如下所示:

public class ClassFile {

    private U4 magic; // 魔数
    private U2 minor_version; // 副版本号
    private U2 magor_version; // 主版本号
    private U2 constant_pool_count; // 常量池计数器
    private CpInfo[] constant_pool; // 常量池
    private U2 access_flags; // 访问标志
    private U2 this_class; // 类索引
    private U2 super_class; // 父类索引
    private U2 interfaces_count; // 接口总数
    private U2[] interfaces; // 接口数组
    private U2 fields_count; // 字段总数
    private FieldInfo[] fields; // 字段表
    private U2 methods_count; // 方法总数
    private MethodInfo[] methods; // 方法表
    private U2 attributes_count; // 属性总数
    private AttributeInfo[] attributes; // 属性表
}

ClassFile类中的每个字段是按照class文件结构中各项的顺序声明的,其中CpInfo、FieldInfo、MethodInfo、AttributeInfo这几个类目前并未添加任何字段,只是一个空的类,代码如下:

public class CpInfo{}
public class FieldInfo{}
public class MethodInfo{}
public class AttributeInfo{}

U2和U4是基本单位,长度分别为两个字节和四个字节。为了便于理解,我们需要为这两种基本单位创建对应的Java类,这两个类都只需要一个字段,类型为Byte数组,长度在构造方法中控制,要求构造方法必须传入数组每个元素的值。为验证解析结果是否正确,以及解析结果的可读性,还需要为这两个类添加一个byte[]转int的方法,以及byte[]转16进制字符串的方法。

U2

public class U2 {

    private byte[] value;

    public U2(byte b1, byte b2) {
        value = new byte[]{b1, b2};
    }
    public Integer toInt() {
        return (value[0] & 0xff) << 8 | (value[1] & 0xff);
    }
    public String toHexString() {
        char[] hexChar = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
        StringBuilder hexStr = new StringBuilder();
        for (int i = 1; i >= 0; i--) {
            int v = value[i] & 0xff;
            while (v > 0) {
                int c = v % 16;
                v = v >>> 4;
                hexStr.insert(0, hexChar[c]);
            }
            if (((hexStr.length() & 0x01) == 1)) {
                hexStr.insert(0, '0');
            }
        }
        return "0x" + (hexStr.length() == 0 ? "00" : hexStr.toString());
    }
}

U4

public class U4 {
    private byte[] value;
    public U4(byte b1, byte b2, byte b3, byte b4) {
        value = new byte[]{b1, b2, b3, b4};
    }
    public int toInt() {
        int a = (value[0] & 0xff) << 24;
        a |= (value[1] & 0xff) << 16;
        a |= (value[2] & 0xff) << 8;
        return a | (value[3] & 0xff);
    }

    public String toHexString() {
        char[] hexChar = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
        StringBuilder hexStr = new StringBuilder();
        for (int i = 3; i >= 0; i--) {
            int v = value[i] & 0xff;
            while (v > 0) {
                int c = v % 16;
                v = v >>> 4;
                hexStr.insert(0, hexChar[c]);
            }
            if (((hexStr.length() & 0x01) == 1)) {
                hexStr.insert(0, '0');
            }
        }
        return "0x" + hexStr.toString();
    }
}

接着我们需要创建一个接口BaseByteCodeHandler,抽象出class文件结构各项的解析器行为。每个解析器应该只负责完成class文件结构中某一项的解析工作,如常量池解析器就只负责解析常量池。

BaseByteCodeHandler

public interface BaseByteCodeHandler {

    /**
     * 排序
     *
     * @return
     */
    int order();

    /**
     * 读取
     *
     * @param codeBuf
     * @param classFile
     */
    void read(ByteBuffer codeBuf, ClassFile classFile) throws Exception;

}

BaseByteCodeHandler接口只定义了一个read方法,该方法要求传入class文件的字节缓存[2]和ClassFile对象。read方法将从字节缓存中读取相应的字节数据写入ClassFile对象。由于解析是按顺序解析的,因此BaseByteCodeHandler接口还定义了一个返回排序值的方法,用于实现解析器排序,比如版本号解析器排在魔数解析器的后面。

有了解析器之后,我们还需要实现一个管理和调度解析器工作的总指挥ClassFileAnalysiser。

ClassFileAnalysiser

public class ClassFileAnalysiser {

    private final static List<BaseByteCodeHandler> handlers = new ArrayList<>();

    static {
        // 添加各项的解析器
        handlers.add(new MagicHandler());//魔数解析器
        handlers.add(new VersionHandler());//版本解析器
        handlers.add(new ConstantPoolHandler());
        handlers.add(new AccessFlagsHandler());
        handlers.add(new ThisAndSuperClassHandler());
        handlers.add(new InterfacesHandler());
        handlers.add(new FieldHandler());
        handlers.add(new MethodHandler());
        handlers.add(new AttributesHandler());
        // 解析器排序,要按顺序调用
        handlers.sort((Comparator.comparingInt(BaseByteCodeHandler::order)));
    }

    public static ClassFile analysis(ByteBuffer codeBuf) throws Exception {
        codeBuf.position(0); // 重置ByteBuffer的读指针,从头开始
        ClassFile classFile = new ClassFile();
        // 遍历解析器,调用每个解析器的解析方法 
        for (BaseByteCodeHandler handler : handlers) {
            handler.read(codeBuf, classFile);
        }
        System.out.println("class文件结构解析完成,解析是否正常(剩余未解析的字节数):" + codeBuf.remaining());
        return classFile;
    }
}

ClassFileAnalysiser的静态代码块负责实例化各个解释器并排好序。ClassFileAnalysiser暴露analysis方法给外部调用,由analysis方法根据解析器的排序顺序去调用各个解析器的read方法完成class文件结构各项的解析工作,由各项解析器将解析结果赋值给ClassFile对象的对应字段。analysis方法的入参是class文件内容的字节缓存,从class文件中读取而来。使用ByteBuffer而不直接使用byte[]缓存读取的class文件内容是因为ByteBuffer能更好的控制顺序读取。现在我们只需要实现将class文件读取到内存中,再调用ClassFileAnalysiser的analysis方法,就能实现将一个class文件解析为一个ClassFile对象了。

测试入口ClassFileAnalysisMain

public class ClassFileAnalysisMain {
  //将文件读取到内存中
    public static ByteBuffer readFile(String classFilePath) throws Exception {
        File file = new File(classFilePath);
        if (!file.exists()) {
            throw new Exception("file not exists!");
        }
        byte[] byteCodeBuf = new byte[4096];
        int lenght;
        try (InputStream in = new FileInputStream(file)) {
            lenght = in.read(byteCodeBuf);
        }
        if (lenght < 1) {
            throw new Exception("not read byte code.");
        }
        return ByteBuffer.wrap(byteCodeBuf, 0, lenght)
                .asReadOnlyBuffer();
    }

    public static void main(String[] args) throws Exception {
        ByteBuffer codeBuf = readFile("D:\\codespace\\jvm\\target\\classes\\com\\tuling\\smlz\\jvm\\classbyatecode\\TulingByteCode.class");
        ClassFile classFile = ClassFileAnalysiser.analysis(codeBuf);
        System.out.println(classFile.getMagic().toHexString());
        System.out.println(classFile.getMinor_version().toInt());
        System.out.println(classFile.getMagor_version().toInt());
    }

}

当然,这只是整体的框架搭建,class文件结构各项的解释器还没有实现。接下来,我们就按照class文件结构的解析顺序实现各项解析器。

2.解析魔数

魔数占四个字节,它只是用来确定这个文件是否是一个class文件。魔数固定值为0xCAFEBABE,这个值永远不会改变。魔数解析的实现很简单,只需要从class文件字节缓存中连续读取四个字节,将这四个字节转为U4对象,并赋值给ClassFile对象的magic字段。魔数解析器的实现:

MagicHandler

public class MagicHandler implements BaseByteCodeHandler {

    @Override
    public int order() {
        return 0;
    }

    @Override
    public void read(ByteBuffer codeBuf, ClassFile classFile) throws Exception {
        //连续读4个字节
        classFile.setMagic(new U4(codeBuf.get(), codeBuf.get(), codeBuf.get(), codeBuf.get()));
        if (!"0xCAFEBABE".equalsIgnoreCase(classFile.getMagic().toHexString())) {
            throw new Exception("这不是一个Class文件");
        }
    }

}

3.解析版本号

class文件结构的版本号分为主版本号和副版本号,它们共同构成class文件格式的版本号[1]。比如一个class文件的主版本号为56,副版本号为0,那么这个class文件结构的版本号就是52.0。副版本号在前,主版本号在后,分别占两个字节。版本号解析器的职责就是从class文件字节缓存中读取出副版本号和主版本号。按顺序读取,先读取两个字节的副版本号,再读取两个字节的主版本号。版本号解析器的实现

VersionHandler

public class VersionHandler implements BaseByteCodeHandler {
    @Override
    public int order() {
        return 1;
    }
    @Override
    public void read(ByteBuffer codeBuf, ClassFile classFile) throws Exception {
        U2 minorVersion = new U2(codeBuf.get(), codeBuf.get());
        classFile.setMinor_version(minorVersion);
        U2 majorVersion = new U2(codeBuf.get(), codeBuf.get());
        classFile.setMagor_version(majorVersion);
    }
}

class文件格式的各版本号与JDK版本的对应关系如表


4.解析常量池

Java虚拟机执行字节码指令依赖常量池表中的常量信息,如创建对象的new指令,需要指定对象的类型描述符,new指令的操作数的值为该类型描述符对应的常量在常量池中的索引,虚拟机将根据常量信息到方法区寻找对应的class元数据。根据《Java虚拟机规范》规定,常量池表中所有常量项的结构都包含一个tag项,tag值用于标志一个常量是哪种常量结构。只有根据tag确定常量的结构,才能根据常量结构计算常量所占用的字节数。常量结构的通用格式为:

cp_info{
    u1 tag;
    u1 info[];
}

其中U1类型占一个字节。Info[]字节数组存储的内容和长度由tag值决定。tag值对应的常量结构如表



要从class文件中解析出常量池中的所有项,除了要了解每个tag值对应的常量结构之外,我们还需要了解每个常量结构都用于存储哪些信息,才能确定每个常量所占用的字节数。在实现常量池解析器之前,我们需要先根据《Java虚拟机规范》中描述的每个常量结构创建对应的Java类型。

常量池各项的解析

注:详情参考笔者这篇呕心沥血的巨作: Class文件结构(1)—手动解析每一个字节,你看不懂

与class文件结构的各项解析器一样,我们也要求每个常量结构都要实现各自的解析工作。首先定义常量解析器接口ConstantInfoHandler

ConstantInfoHandler

public interface ConstantInfoHandler {

    /**
     * 读取
     *
     * @param codeBuf
     */
    void read(ByteBuffer codeBuf) throws Exception;

}

ConstantInfoHandler接口只定义一个解析方法,方法要求传入class文件字节缓存。该class文件字节缓存与class文件结构各项解析器使用的是同一个缓存对象,都是从同一个class文件读取到内存中的ByteBuffer对象。接着根据常量结构的通用格式将常量结构抽象出一个父类CpInfo,CpInfo类的实现

public abstract class CpInfo implements ConstantInfoHandler {
    private U1 tag;
    protected CpInfo(U1 tag) {
        this.tag = tag;
    }
  @Override
    public String toString() {
        return "tag=" + tag.toString();
    }
}

CpInfo抽象类约定构建方法必须传入tag值,且约定子类必须实现ConstantInfoHandler常量结构解析器接口,并实现常量解析方法。

4.1CONSTANT_Utf8_info

根据《Java虚拟机规范》规定,CONSTANT_Utf8_info常量结构用于存储字符串常量,字符串编码使用UTF-8。除一个必须的tag字段和存储字符串的字节数组外,还要有一个字段存储描述这个字符串字节数组的长度。创建CONSTANT_Utf8_Info类并继承CpInfo抽象类,实现ConstantInfoHandler接口定义的解析方法。

public class CONSTANT_Utf8_info extends CpInfo {

    private U2 length;
    private byte[] bytes;

    public CONSTANT_Utf8_info(U1 tag) {
        super(tag);
    }

    @Override
    public void read(ByteBuffer codeBuf) throws Exception {
        length = new U2(codeBuf.get(), codeBuf.get());
        bytes = new byte[length.toInt()];
        codeBuf.get(bytes, 0, length.toInt());
    }

    @Override
    public String toString() {
        return super.toString() +
                ",length=" + length.toInt() +
                ",str=" + new String(bytes, StandardCharsets.UTF_8);
    }

}

read方法在从Class文件字节缓存ByteBuffer对象中读取该类型的常量时,需按顺序先读取长度,再根据长度n取后续n个字节存放到该常量的字节数组中。

4.2 CONSTANT_Class_Info

根据《Java虚拟机规范》规定,CONSTANT_Class_Info常量存储类的符号信息,除tag字段外,只有一个存储指向常量池表中某一常量的索引字段name_index,name_index指向的常量必须是一个CONSTANT_Utf8_info常量,该常量存储class的类名。创建CONSTANT_Class_Info类并继承CpInfo抽象类,实现ConstantInfoHandler接口定义的解析方法

public class CONSTANT_Class_info extends CpInfo {
    private U2 name_index;
    public CONSTANT_Class_info(U1 tag) {
        super(tag);
    }

    @Override
    public void read(ByteBuffer codeBuf) throws Exception {
        this.name_index = new U2(codeBuf.get(), codeBuf.get());
    }

    @Override
    public String toString() {
        return "CONSTANT_Class_info{" +
                "name_index=" + name_index.toInt() +
                '}';
    }
}

4.3 CONSTANT_Fieldref_info

根据《Java虚拟机规范》规定,CONSTANT_Fieldref_info常量存储字段的符号信息,除tag字段外,有两个U2类型的指向常量池中某个常量的索引字段,分别是class_index、name_and_type_index。CONSTANT_Fieldref_info结构的各项说明:

public class CONSTANT_Fieldref_info extends CpInfo {

    private U2 class_index;
    private U2 name_and_type_index;

    public CONSTANT_Fieldref_info(U1 tag) {
        super(tag);
    }

    @Override
    public void read(ByteBuffer codeBuf) throws Exception {
        class_index = new U2(codeBuf.get(), codeBuf.get());
        name_and_type_index = new U2(codeBuf.get(), codeBuf.get());
    }

    @Override
    public String toString() {
        return "CONSTANT_Fieldref_info";
    }

}

4.4 CONSTANT_Methodref_info

CONSTANT_Methodref_info在结构上与CONSTANT_Fieldref_info一样,因此可通过继承CONSTANT_Fieldref_info类实现其字段的定义和完成解析工作。
CONSTANT_Methodref_info结构的各项说明:


public class CONSTANT_Methodref_info extends CONSTANT_Fieldref_info {

    public CONSTANT_Methodref_info(U1 tag) {
        super(tag);
    }

    @Override
    public String toString() {
        return "CONSTANT_Methodref_info";
    }
}

4.5 CONSTANT_InterfaceMethodref_info

CONSTANT_InterfaceMethodref_info在结构上与CONSTANT_Fieldref_info一样,因此可通过继承CONSTANT_Fieldref_info类实现其字段的定义和完成解析工作。
CONSTANT_InterfaceMethodref_info结构的各项说明:

public class CONSTANT_InterfaceMethodref_info extends CONSTANT_Fieldref_info{

    public CONSTANT_InterfaceMethodref_info(U1 tag) {
        super(tag);
    }

    @Override
    public String toString() {
        return "CONSTANT_InterfaceMethodref_info";
    }
}

4.6 CONSTANT_String_info

根据《Java虚拟机规范》规定,CONSTANT_String_info结构存储Java中String类型的常量,除tag字段外,还有一个U2类型的字段string_index,值为常量池中某个常量的索引,该索引指向的常量必须是一个CONSTANT_Utf8_info常量。创建CONSTANT_String_info类并继承CpInfo抽象类,实现ConstantInfoHandler接口定义的解析方法

public class CONSTANT_String_info extends CpInfo {

    private U2 string_index;

    public CONSTANT_String_info(U1 tag) {
        super(tag);
    }

    @Override
    public void read(ByteBuffer codeBuf) throws Exception {
        string_index = new U2(codeBuf.get(), codeBuf.get());
    }
    @Override
    public String toString() {
        return "CONSTANT_String_info";
    }
}

4.7 CONSTANT_Integer_info

根据《Java虚拟机规范》规定,CONSTANT_Integer_info常量存储一个整型数值,除一个tag字段外,只有一个U4类型的字段bytes,bytes转为10进制数就是这个常量所表示的整型值。创建CONSTANT_Integer_info类并继承Cpinfo抽象类,实现ConstantInfoHandler接口定义的解析方法

public class CONSTANT_Integer_info extends CpInfo {

    private U4 bytes;

    public CONSTANT_Integer_info(U1 tag) {
        super(tag);
    }

    @Override
    public void read(ByteBuffer codeBuf) throws Exception {
        bytes = new U4(codeBuf.get(),codeBuf.get(),codeBuf.get(),codeBuf.get());
    }

    @Override
    public String toString() {
        return "CONSTANT_Integer_info";
    }
}

4.8 CONSTANT_Float_info

CONSTANT_Float_info与CONSTANT_Integer_info在存储结构上是一样的,只是bytes所表示的内容不同,CONSTANT_Float_info的bytes存储的是浮点数。CONSTANT_Float_info类的定义和解析方法的实现也可通过继承CONSTANT_Integer_info实现

public class CONSTANT_Float_info extends CONSTANT_Integer_info {

    public CONSTANT_Float_info(U1 tag) {
        super(tag);
    }

    @Override
    public String toString() {
        return "CONSTANT_Float_info";
    }

}

4.9 CONSTANT_Long_info

与CONSTANT_Integer_info常量不同的是,CONSTANT_Long_info常量使用8个字节存储一个长整型数值,即使用两个U4类型的字段分别存储一个长整型数的高32位和低32位。创建CONSTANT_Long_info类并继承CpInfo抽象类,实现ConstantInfoHandler接口定义的解析方法

public class CONSTANT_Long_info extends CpInfo {

    private U4 hight_bytes;
    private U4 low_bytes;

    public CONSTANT_Long_info(U1 tag) {
        super(tag);
    }

    @Override
    public void read(ByteBuffer codeBuf) throws Exception {
        hight_bytes = new U4(codeBuf.get(), codeBuf.get(), codeBuf.get(), codeBuf.get());
        low_bytes = new U4(codeBuf.get(), codeBuf.get(), codeBuf.get(), codeBuf.get());
    }

    @Override
    public String toString() {
        return "CONSTANT_Long_info";
    }
}

4.10 CONSTANT_Double_info

CONSTANT_Double_info常量与CONSTANT_Long_info常量在结构上也是一样的,只是所表示的值类型不同。因此CONSTANT_Double_info类的定义和解析方法的实现也可通过继承CONSTANT_Long_info实现

public class CONSTANT_Double_info extends CONSTANT_Long_info {

    public CONSTANT_Double_info(U1 tag) {
        super(tag);
    }

    @Override
    public String toString() {
        return "CONSTANT_Double_info";
    }
}

4.11 CONSTANT_NameAndType_info

根据《Java虚拟机规范》规定,CONSTANT_NameAndType_info结构用于存储字段的名称和字段的类型描述符,或者是用于存储方法的名称和方法的描述符。CONSTANT_NameAndType_info结构除tag字段外,还有一个U2类型的字段name_index和一个U2类型的字段descriptor_index,分别对应名称指向常量池中某个常量的索引和描述符指向常量池中某个常量的索引,这两个字段指向的常量都必须是CONSTANT_Utf8_info结构的常量。创建CONSTANT_NameAndType_info类并继承CpInfo抽象类,实现ConstantInfoHandler接口定义的解析方法

public class CONSTANT_NameAndType_info extends CpInfo {

    private U2 name_index;
    private U2 descriptor_index;

    public CONSTANT_NameAndType_info(U1 tag) {
        super(tag);
    }

    @Override
    public void read(ByteBuffer codeBuf) throws Exception {
        name_index = new U2(codeBuf.get(), codeBuf.get());
        descriptor_index = new U2(codeBuf.get(), codeBuf.get());
    }
    @Override
    public String toString() {
        return "CONSTANT_NameAndType_info";
    }
}

4.12 CONSTANT_MethodHandle_info

根据《Java虚拟机规范》规定,CONSTANT_MethodHandle_info结构用于存储方法句柄,这是虚拟机为实现动态调用invokedynamic指令所增加的常量结构。CONSTANT_MethodHandle_info结构除必须的tag字段外,有一个U1类型的字段reference_kind,取值范围为1~9,包括1和9,表示方法句柄的类型,还有一个U1类型的字段reference_index,其值为指向常量池中某个常量的索引。
reference_index指向的常量的结构与reference_kind取值的关系如表



创建CONSTANT_MethodHandle_info类并继承CpInfo抽象类,实现ConstantInfoHandler接口定义的解析方法

public class CONSTANT_MethodHandle_info extends CpInfo {

    private U1 reference_kind;
    private U2 reference_index;

    public CONSTANT_MethodHandle_info(U1 tag) {
        super(tag);
    }

    @Override
    public void read(ByteBuffer codeBuf) throws Exception {
        reference_kind = new U1(codeBuf.get());
        reference_index = new U2(codeBuf.get(), codeBuf.get());
    }

    @Override
    public String toString() {
        return "CONSTANT_MethodHandle_info";
    }
}

4.13 CONSTANT_MethodType_info

CONSTANT_MethodType_info结构表示方法类型,与CONSTANT_MethodHandle_info结构一样,也是虚拟机为实现动态调用invokedynamic指令所增加的常量结构。CONSTANT_MethodType_info除tag字段外,只有一个u2类型的描述符指针字段descriptor_index,指向常量池中的某一CONSTANT_Utf8_info结构的常量。创建CONSTANT_MethodType_info类并继承CpInfo抽象类,实现ConstantInfoHandler接口定义的解析方法

public class CONSTANT_MethodType_info extends CpInfo {

    private U2 descriptor_index;

    public CONSTANT_MethodType_info(U1 tag) {
        super(tag);
    }

    @Override
    public void read(ByteBuffer codeBuf) throws Exception {
        descriptor_index = new U2(codeBuf.get(), codeBuf.get());
    }
    @Override
    public String toString() {
        return "CONSTANT_MethodType_info";
    }
}

4.14 CONSTANT_InvokeDynamic_info

CONSTANT_InvokeDynamic_info表示invokedynamic指令用到的引导方法bootstrap method以及引导方法所用到的动态调用名称、参数、返回类型。CONSTANT_InvokeDynamic_info结构除tag字段外,有两个U2类型的字段,分别是bootstrap_method_attr_index和name_and_type_index,前者指向class文件结构属性表中引导方法表的某个引导方法,后者指向常量池中某个CONSTANT_NameAndType_Info结构的常量。创建CONSTANT_InvokeDynamic_info类并继承CpInfo抽象类,实现ConstantInfoHandler接口定义的解析方法

public class CONSTANT_InvokeDynamic_info extends CpInfo {

    private U2 bootstrap_method_attr_index;
    private U2 name_and_type_index;

    public CONSTANT_InvokeDynamic_info(U1 tag) {
        super(tag);
    }

    @Override
    public void read(ByteBuffer codeBuf) throws Exception {
        bootstrap_method_attr_index = new U2(codeBuf.get(), codeBuf.get());
        name_and_type_index = new U2(codeBuf.get(), codeBuf.get());
    }

    @Override
    public String toString() {
        return "CONSTANT_InvokeDynamic_info";
    }
}

常量池的解析

在创建完各常量结构对应的Java类,和实现各常量结构的解析方法后,我们再来完成整个常量池的解析工作。
我们先修改所有常量类型(我们编写的常量类)的父类CpInfo,在CpInfo类中添加一个静态方法,用于根据tag的值创建不同的常量类型对象。

public abstract class CpInfo implements ConstantInfoHandler {

    private U1 tag;

    protected CpInfo(U1 tag) {
        this.tag = tag;
    }

    @Override
    public String toString() {
        return "tag=" + tag.toString();
    }

    public static CpInfo newCpInfo(U1 tag) throws Exception {
        int tagValue = tag.toInt();
        CpInfo info;
        switch (tagValue) {
            case 1:
                info = new CONSTANT_Utf8_info(tag);
                break;
            case 3:
                info = new CONSTANT_Integer_info(tag);
                break;
            case 4:
                info = new CONSTANT_Float_info(tag);
                break;
            case 5:
                info = new CONSTANT_Long_info(tag);
                break;
            case 6:
                info = new CONSTANT_Double_info(tag);
                break;
            case 7:
                info = new CONSTANT_Class_info(tag);
                break;
            case 8:
                info = new CONSTANT_String_info(tag);
                break;
            case 9:
                info = new CONSTANT_Fieldref_info(tag);
                break;
            case 10:
                info = new CONSTANT_Methodref_info(tag);
                break;
            case 11:
                info = new CONSTANT_InterfaceMethodref_info(tag);
                break;
            case 12:
                info = new CONSTANT_NameAndType_info(tag);
                break;
            case 15:
                info = new CONSTANT_MethodHandle_info(tag);
                break;
            case 16:
                info = new CONSTANT_MethodType_info(tag);
                break;
            case 18:
                info = new CONSTANT_InvokeDynamic_info(tag);
                break;
            default:
                throw new Exception("没有找到该TAG=" + tagValue + "对应的常量类型");
        }
        return info;
    }

}

接着创建常量池解析器ConstantPoolHandler,设置其排序值为版本号解析器的排序值+1,也就是将该解析器排在版本号解析器的后面。ConstantPoolHandler的实现如代码

public class ConstantPoolHandler implements BaseByteCodeHandler {

    @Override
    public int order() {
        return 2;
    }

    @Override
    public void read(ByteBuffer codeBuf, ClassFile classFile) throws Exception {
        U2 cpLen = new U2(codeBuf.get(), codeBuf.get());
        classFile.setConstant_pool_count(cpLen);
        int cpInfoLeng = cpLen.toInt() - 1;
        classFile.setConstant_pool(new CpInfo[cpInfoLeng]);
        for (int i = 0; i < cpInfoLeng; i++) {
            U1 tag = new U1(codeBuf.get());
            CpInfo cpInfo = CpInfo.newCpInfo(tag);
            cpInfo.read(codeBuf);
            // System.out.println("#" + (i + 1) + ":" + cpInfo);
            classFile.getConstant_pool()[i] = cpInfo;
        }
    }

}

常量池解析器实现BaseByteCodeHandler接口的read方法,在read方法中,首先是读取常量池计数器,根据常量池计数器减1得到常量池的常量总数,再根据常量总数创建常量池表。最后是按顺序解析常量池的各项常量。
在解析常量池的常量时,先从Class文件字节缓存中取一个字节码,就是tag,根据tag调用CpInfo的静态方法newCpInfo创建对应常量类型对象,再调用创建出来的常量类型对象的read方法完成该项常量的解析工作。最后,我们还要将编写好的常量池解析器交给ClassFileAnalysiser管理

public class ClassFileAnalysiser {

    private final static List<BaseByteCodeHandler> handlers = new ArrayList<>();

    static {
        // 添加各项的解析器
        .....
        handlers.add(new ConstantPoolHandler());
        ....

5. 解析class文件的访问标志

Class文件结构中的访问标志项access_flags是用U2类型存储的,也就是2个字节。用某个bit位的值是否为1判断该类或接口的访问权限、属性。访问标志与类或接口的访问权限、属性的映射如表



如何判断一个类设置了表中的哪些标志呢?首先从Class文件字节缓存中读取到access_flags的值,再将access_flags转为int类型,将转换后的值“算术与”上各个标志的值,判断结果是否等于这个标志的值。

public class ClassAccessFlagUtils {

    private static final Map<Integer, String> classAccessFlagMap = new HashMap<>();

    static {
        // 公有类型
        classAccessFlagMap.put(0x0001, "public");
        // 不允许有子类
        classAccessFlagMap.put(0x0010, "final");
        classAccessFlagMap.put(0x0020, "super");
        // 接口
        classAccessFlagMap.put(0x0200, "interface");
        // 抽象类
        classAccessFlagMap.put(0x0400, "abstract");
        // 该class非java代码编译后生成
        classAccessFlagMap.put(0x1000, "synthetic");
        // 注解类型
        classAccessFlagMap.put(0x2000, "annotation");
        // 枚举类型
        classAccessFlagMap.put(0x4000, "enum");
    }

    /**
     * 获取16进制对应的访问标志字符串表示 (仅用于类的访问标志)
     *
     * @param flag 访问标志
     * @return
     */
    public static String toClassAccessFlagsString(U2 flag) {
        final int flagVlaue = flag.toInt();
        StringBuilder flagBuild = new StringBuilder();
        classAccessFlagMap.keySet()
                .forEach(key -> {
                    if ((flagVlaue & key) == key) {
                        flagBuild.append(classAccessFlagMap.get(key)).append(",");
                    }
                });
        return flagBuild.length() > 0 && flagBuild.charAt(flagBuild.length() - 1) == ',' ?
                flagBuild.substring(0, flagBuild.length() - 1)
                : flagBuild.toString();
    }

}

现在我们来实现class文件访问标志解析器AccessFlagsHandler,并将AccessFlagsHandler解析器交给ClassFileAnalysiser管理。AccessFlagsHandler的排序值设置为3,即放在常量池解析器之后,约定在常量池解析器解析完成之后再到访问标志解析器解析。

public class AccessFlagsHandler implements BaseByteCodeHandler {

    @Override
    public int order() {
        return 3;
    }

    @Override
    public void read(ByteBuffer codeBuf, ClassFile classFile) throws Exception {
        classFile.setAccess_flags(new U2(codeBuf.get(), codeBuf.get()));
    }

}

6. 解析this与super

在Class文件结构中,紧挨着访问标志access_flags项的是this_class和super_class这两项,也都是U2类型。this_class存储的是常量池中某项常量的索引,super_class要么为0,要么也是存储常量池中某项常量的索引。this_class和super_class指向的常量必须是一个CONSTANT_Class_info结构的常量。
只有Object类的super_class可以为0,接口的super_class指向常量池中Object类的CONSTANT_Class_info常量。this_class与super_class的解析器实现非常简单

public class ThisAndSuperClassHandler implements BaseByteCodeHandler {

    @Override
    public int order() {
        return 4;
    }

    @Override
    public void read(ByteBuffer codeBuf, ClassFile classFile) throws Exception {
        classFile.setThis_class(new U2(codeBuf.get(), codeBuf.get()));
        classFile.setSuper_class(new U2(codeBuf.get(), codeBuf.get()));
    }

}

由于该项目已经完成了常量池的解析,在解析获取到this_class与super_class之后,我们就可以先根据this_class的值到常量池取得对应的CONSTANT_Class_info常量,再从取得的CONSTANT_Class_info常量中获取该常量的name_index的值,最后根据name_index再回到常量池中取得对应的CONSTANT_Utf8_info常量,这样就能获取到具体的类名了。


7. 解析实现的接口

解析完this_class与super_class之后,就可以继续解析获取该class实现的接口总数以及该class实现的所有接口。接口解析器InterfacesHandler的实现

public class InterfacesHandler implements BaseByteCodeHandler {

    @Override
    public int order() {
        return 5;
    }

    @Override
    public void read(ByteBuffer codeBuf, ClassFile classFile) throws Exception {
        classFile.setInterfaces_count(new U2(codeBuf.get(), codeBuf.get()));
        int interfaces_count = classFile.getInterfaces_count().toInt();
        if (interfaces_count > 0) {
            U2[] interfaces = new U2[interfaces_count];
            classFile.setInterfaces(interfaces);
            for (int i = 0; i < interfaces_count; i++) {
                interfaces[i] = new U2(codeBuf.get(), codeBuf.get());
            }
        }
    }

}

read方法完成接口表的解析。在读取class文件字节缓存时,先顺序读取到interfaces_count,interfaces_count是类实现的接口总数。再根据interfaces_count创建接口表interfaces,接口表的数组长度等于interfaces_count。接口表中的每项都是一个常量索引,指向常量池表中CONSTANT_Class_info结构的常量。

8. 解析字段表

同一个Class文件中,不会存在两个相同的字段。相同指定的是字段名与类型描述符都相同。字段结构与Class文件结构一样,都有访问标志项,但两者的访问标志项,在访问权限和属性上有些区别。参照《Java虚拟机规范》,字段中的访问权限和属性标志如表


字段的结构如表



其中,access_flags是字段的访问标志,name_index是字段名称,descriptor_index是字段的类型描述符。字段结构与方法结构、Class文件结构都有属性表attributes,属性表的属性个数可以是0个或多个。属性的通用结构如表



关于属性,我们先了解属性的通用结构,实现属性的初步解析,让字段解析器能够完成字段的解析工作,至于属性info是什么暂时先不关心。
创建字段结构对应的Java类FieldInfo,如代码
public class FieldInfo {

    private U2 access_flags;
    private U2 name_index;
    private U2 descriptor_index;
    private U2 attributes_count;
    private AttributeInfo[] attributes;

}

建属性结构对应的Java类AttributeInfo

public class AttributeInfo {
    private U2 attribute_name_index;
    private U4 attribute_length;
    private byte[] info;
}

创建字段表解析器FieldHandler,实现字段表的解析。字段结构的属性表的解析工作也由字段表解析器完成。解析流程如下:
1、先从class文件字节缓存中读取到字段总数,根据字段总数创建字段表;
2、循环解析出每个字段;
3、解析字段的属性表时,先解析获取到属性总数,再根据属性总数创建属性表;
4、使用通用属性结构循环解析出字段的每个属性;
5、解析属性时,先解析出attribute_name_index,再解析attribute_length获取属性info的长度,根据长度读取指定长度的字节数据存放到属性的info字段。
字段表解析器的实现

public class FieldHandler implements BaseByteCodeHandler {

    @Override
    public int order() {
        // 排在接口解析器的后面
        return 6;
    }

    @Override
    public void read(ByteBuffer codeBuf, ClassFile classFile) throws Exception {
        classFile.setFields_count(new U2(codeBuf.get(), codeBuf.get()));
        // 获取字段总数
        int len = classFile.getFields_count().toInt();
        if (len == 0) {
            return;
        }
        // 创建字段表
        FieldInfo[] fieldInfos = new FieldInfo[len];
        classFile.setFields(fieldInfos);
        for (int i = 0; i < fieldInfos.length; i++) {
            // 解析字段
            fieldInfos[i] = new FieldInfo();
            fieldInfos[i].setAccess_flags(new U2(codeBuf.get(), codeBuf.get()));
            fieldInfos[i].setName_index(new U2(codeBuf.get(), codeBuf.get()));
            fieldInfos[i].setDescriptor_index(new U2(codeBuf.get(), codeBuf.get()));
            fieldInfos[i].setAttributes_count(new U2(codeBuf.get(), codeBuf.get()));
            // 获取字段的属性总数
            int attr_len = fieldInfos[i].getAttributes_count().toInt();
            if (attr_len == 0) {
                continue;
            }
            // 创建字段的属性表
            fieldInfos[i].setAttributes(new AttributeInfo[attr_len]);
            for (int j = 0; j < attr_len; j++) {
                fieldInfos[i].getAttributes()[j] = new AttributeInfo();
                // 解析字段的属性
                fieldInfos[i].getAttributes()[j]
                        .setAttribute_name_index(new U2(codeBuf.get(), codeBuf.get()));
                // 获取属性info的长度
                U4 attr_info_len = new U4(codeBuf.get(), codeBuf.get(), codeBuf.get(), codeBuf.get());
                fieldInfos[i].getAttributes()[j]
                        .setAttribute_length(attr_info_len);
                if (attr_info_len.toInt() == 0) {
                    continue;
                }
                // 解析info
                byte[] info = new byte[attr_info_len.toInt()];
                codeBuf.get(info, 0, attr_info_len.toInt());
                fieldInfos[i].getAttributes()[j].setInfo(info);
            }
        }
    }

}

编写将字段的访问标志access_flags转为字符串输出的工具类FieldAccessFlagUtils

public class FieldAccessFlagUtils {
    private static final Map<Integer, String> fieldAccessFlagMap = new HashMap<>();

    static {
        fieldAccessFlagMap.put(0x0001, "public");
        fieldAccessFlagMap.put(0x0002, "private");
        fieldAccessFlagMap.put(0x0004, "protected");
        fieldAccessFlagMap.put(0x0008, "static");
        fieldAccessFlagMap.put(0x0010, "final");
        fieldAccessFlagMap.put(0x0040, "volatile");
        fieldAccessFlagMap.put(0x0080, "transient");
        fieldAccessFlagMap.put(0x1000, "synthtic");
        fieldAccessFlagMap.put(0x4000, "enum");
    }

    /**
     * 获取16进制对应的访问标志和属性字符串表示 (仅用于类的访问标志)
     *
     * @param flag 字段的访问标志
     * @return
     */
    public static String toFieldAccessFlagsString(U2 flag) {
        final int flagVlaue = flag.toInt();
        StringBuilder flagBuild = new StringBuilder();
        fieldAccessFlagMap.keySet()
                .forEach(key -> {
                    if ((flagVlaue & key) == key) {
                        flagBuild.append(fieldAccessFlagMap.get(key)).append(",");
                    }
                });
        return flagBuild.length() > 0 && flagBuild.charAt(flagBuild.length() - 1) == ',' ?
                flagBuild.substring(0, flagBuild.length() - 1)
                : flagBuild.toString();
    }

}

9. 解析方法表

方法表存放一个类或者接口的所有方法。与字段结构一样,方法结构也有属性表,方法编译后的字节码指令是存放在方法结构的属性表中的,对应Code属性。但不是所有方法都会有Code属性,如接口中的方法不一定会有Code属性,如抽象方法一定没有Code属性。方法包括静态方法、以及类的初始化方法<clinit>和类的实例初始化方法<init>。参照《Java虚拟机规范》,方法结构如表



其中方法名称索引、方法描述符索引与字段结构中的字段名索引和字段类型描述符索引,都是指向常量池中某个CONSTABT_Utf8_info结构的常量,属性总数与属性表也与字段结构中的一样,但不同的是,属性的结构不同,如方法有Code属性而字段没有。访问标志也与字段的访问标志有些区别,如字段有ACC_VOLATILE标志而方法没有。关于方法的访问权限及属性标志如表



根据表方法结构创建对应的Java类MethodInfo
public class MethodInfo {

    private U2 access_flags;
    private U2 name_index;
    private U2 descriptor_index;
    private U2 attributes_count;
    private AttributeInfo[] attributes;

}

与字段表的解析流程一样,我们暂时不关心属性表的具体属性的解析,属性表的解析只使用通用属性结构解析。方法表解析器的实现如代码

public class MethodHandler implements BaseByteCodeHandler {

    @Override
    public int order() {
        // 排在字段解析器的后面
        return 7;
    }

    @Override
    public void read(ByteBuffer codeBuf, ClassFile classFile) throws Exception {
        classFile.setMethods_count(new U2(codeBuf.get(), codeBuf.get()));
        // 获取方法总数
        int len = classFile.getMethods_count().toInt();
        if (len == 0) {
            return;
        }
        // 创建方法表
        MethodInfo[] methodInfos = new MethodInfo[len];
        classFile.setMethods(methodInfos);
        for (int i = 0; i < methodInfos.length; i++) {
            // 解析方法
            methodInfos[i] = new MethodInfo();
            methodInfos[i].setAccess_flags(new U2(codeBuf.get(), codeBuf.get()));
            methodInfos[i].setName_index(new U2(codeBuf.get(), codeBuf.get()));
            methodInfos[i].setDescriptor_index(new U2(codeBuf.get(), codeBuf.get()));
            methodInfos[i].setAttributes_count(new U2(codeBuf.get(), codeBuf.get()));
            // 获取方法的属性总数
            int attr_len = methodInfos[i].getAttributes_count().toInt();
            if (attr_len == 0) {
                continue;
            }
            // 创建方法的属性表
            methodInfos[i].setAttributes(new AttributeInfo[attr_len]);
            for (int j = 0; j < attr_len; j++) {
                methodInfos[i].getAttributes()[j] = new AttributeInfo();
                // 解析方法的属性
                methodInfos[i].getAttributes()[j]
                        .setAttribute_name_index(new U2(codeBuf.get(), codeBuf.get()));
                // 获取属性info的长度
                U4 attr_info_len = new U4(codeBuf.get(), codeBuf.get(), codeBuf.get(), codeBuf.get());
                methodInfos[i].getAttributes()[j]
                        .setAttribute_length(attr_info_len);
                if (attr_info_len.toInt() == 0) {
                    continue;
                }
                // 解析info
                byte[] info = new byte[attr_info_len.toInt()];
                codeBuf.get(info, 0, attr_info_len.toInt());
                methodInfos[i].getAttributes()[j].setInfo(info);
            }
        }
    }

}

10 解析class文件的属性表

字段结构和方法结构也都有属性表,所以要注意不要将这些属性表混在一起理解。但所有属性都有一个通用的结构,这在解析字段那部分已经介绍。因此,解析class文件结构的属性表我们也可以使用通用的属性结构来解析。
解析步骤是先从class文件字节缓存中读取两个字节,如果前面的解析工作都正常,那么现在读取到的这两个字节就是该class文件属性表的长度。接着根据长度创建属性表,使用通用属性结构循环解析出每个属性,循环次数为属性的总数。class文件结构的属性表解析器AttributesHandler的实现如代码

public class AttributesHandler implements BaseByteCodeHandler {

    @Override
    public int order() {
        return 8;
    }

    @Override
    public void read(ByteBuffer codeBuf, ClassFile classFile) throws Exception {
        classFile.setAttributes_count(new U2(codeBuf.get(), codeBuf.get()));
        // 获取属性总数
        int len = classFile.getAttributes_count().toInt();
        if (len == 0) {
            return;
        }
        // 创建属性表
        AttributeInfo[] attributeInfos = new AttributeInfo[len];
        classFile.setAttributes(attributeInfos);
        for (int i = 0; i < len; i++) {
            // 创建属性
            AttributeInfo attributeInfo = new AttributeInfo();
            attributeInfos[i] = attributeInfo;
            // 解析属性
            attributeInfo.setAttribute_name_index(new U2(codeBuf.get(), codeBuf.get()));
            attributeInfo.setAttribute_length(new U4(codeBuf.get(), codeBuf.get(), codeBuf.get(), codeBuf.get()));
            int attr_len = attributeInfo.getAttribute_length().toInt();
            if (attr_len == 0) {
                continue;
            }
            // 解析属性的info项
            byte[] bytes = new byte[attr_len];
            attributeInfo.setInfo(bytes);
            codeBuf.get(bytes, 0, bytes.length);
        }
    }

}

至此,我们对整个class文件结构的解析工作就已经基本完成了。而对于属性的解析,我们都只是使用通用的解析器解析。在《Java虚拟机规范》Java SE 8版本中,预定义属性就有23个。如果想要深入理解某个属性,我们可再对其进行二次解析。如何使用我们编写的项目对class文件结构、字段结构、方法结构的属性表中的属性进行二次解析呢?我们以字段的ConstantValue属性为例。

解析ConstantValue属性

ConstantValue属性用于通知虚拟机在类或接口初始化阶段为被标志为ACC_STATIC的字段自动赋值,如接口中声明的字段,类中声明的静态常量字段。其它非ACC_STATIC的字段是在类的实例初始化方法中完成的。
在字段结构的属性表中最多只能有一个ConstantValue属性。字段的类型必须是基本数据类型或者String类,因为从常量池中只能引用到基本类型和String类型的字面量。ConstantValue属性的结构如下:

public class ConstantValue_attribute {

    private U2 attribute_name_index;
    private U4 attribute_length;
    private U2 constantvalue_index;

}

由于ConstantValue属性是定长属性,因此attribute_length的值固定为2。attribute_name_index指向常量池中某个CONSTANT_Utf8_info常量,该常量表示的字符串为“ConstantValue”。因为属性表的属性结构并不像常量池的常量结构那样,有一个tag字段映射到哪种常量结构,因此,只能通过attribute_name_index属性判断是哪种属性。constantvalue_index指向基本数据类型或String类型常量。

以一个例子来说明ConstantValue属性的使用场景。在一个接口中定义一个字段并赋值,通过分析其Class文件结构,找到这个字段的属性表,看是否有ConstantValue属性。

public interface TestConstantValueInterface {

    int value = 1000;

}

现在,我们需要编写一个属性解析工具类,添加支持解析ConstantValue属性的静态方法,以完成属性的二次解析工作。

/**
 * 属性加工厂,即对属性进行二次解析
 */
public class AttributeProcessingFactory {

    public static ConstantValue_attribute processingConstantValue(AttributeInfo attributeInfo) {
        ConstantValue_attribute attribute = new ConstantValue_attribute();
        attribute.setAttribute_name_index(attributeInfo.getAttribute_name_index());
        attribute.setAttribute_length(attributeInfo.getAttribute_length());
        attribute.setConstantvalue_index(new U2(attributeInfo.getInfo()[0], attributeInfo.getInfo()[1]));
        return attribute;
    }
}

编写单元测试,解析TestConstantValueInterface接口编译后的class文件,获取该接口的所有字段信息,然后遍历字段,拿到每个字段的属性表,接着遍历属性表,根据属性的名称在常量池中的索引到常量池中取到属性名,最后根据属性名去调用对应的属性结构的解析方法对其进行二次解析。因为当前只实现了ConstantValue属性的解析,所以单元测试中只对名称为“ConstantValue”的属性进行二次解析。

public class ConstantValueAttributeTest {

    @Test
    public void testConstantValue() throws Exception {
        ByteBuffer codeBuf = ClassFileAnalysisMain.readFile("D:\\codespace\\monitor_26\\target\\classes\\com\\caecc\\controller\\BusinesschainController.class");
        ClassFile classFile = ClassFileAnalysiser.analysis(codeBuf);
        // 获取所有字段
        FieldInfo[] fieldInfos = classFile.getFields();
        for (FieldInfo fieldInfo : fieldInfos) {
            // 获取字段的所有属性
            AttributeInfo[] attributeInfos = fieldInfo.getAttributes();
            if (attributeInfos == null || attributeInfos.length == 0) {
                continue;
            }
            System.out.println("字段:" + classFile.getConstant_pool()[fieldInfo.getName_index().toInt() - 1]);
            for (AttributeInfo attributeInfo : attributeInfos) {
                // 获取属性的名称
                U2 name_index = attributeInfo.getAttribute_name_index();
                CONSTANT_Utf8_info name_info = (CONSTANT_Utf8_info) classFile.getConstant_pool()[name_index.toInt() - 1];
                String name = new String(name_info.getBytes());
                if (name.equalsIgnoreCase("ConstantValue")) {
                    // 属性二次解析
                    ConstantValue_attribute constantValue = AttributeProcessingFactory.processingConstantValue(attributeInfo);
                    // 说去constantvalue_index,从常量池中取值
                    U2 cv_index = constantValue.getConstantvalue_index();
                    Object cv = classFile.getConstant_pool()[cv_index.toInt() - 1];
                    // 需要判断常量的类型
                    if (cv instanceof CONSTANT_Utf8_info) {
                        System.out.println("ConstantValue:" + cv.toString());
                    } else if (cv instanceof CONSTANT_Integer_info) {
                        System.out.println("ConstantValue:" +
                                ((CONSTANT_Integer_info) cv).getBytes().toInt());
                    } else if (cv instanceof CONSTANT_Float_info) {
                        // todo
                    } else if (cv instanceof CONSTANT_Long_info) {
                        // todo
                    } else if (cv instanceof CONSTANT_Double_info) {
                        // todo
                    }
                }
            }
        }
    }

}

单元测试结果输出如图



解析结果如图2-12所示,字段名为value的字段,其属性表有一个ConstantValue属性,常量值是1000。

解析Code属性

字节码指令存储在方法结构的属性表的Code属性中。这一节我们将通过完成对Code属性的二次解析了解Code属性,了解字节码指令是怎么存储的在Code属性中的。
Code属性是种可变长的属性,属性中包含了方法指向的字节码指令及相关辅助信息。实例初始化方法<init>、类或接口的初始化方法<clinit>都会有Code属性,但不是每个方法都有Code属性,如声明为native的方法、abstract抽象方法、接口中的非default方法就没有Code属性。

方法结构的属性表中最多只能有一个Code属性,Code属性的结构如表


max_stack与max_locals分别对应操作数栈的大小和局部变量表的大小。code项用一个字节数组存储该方法的所有字节码指令。属性也可以有属性表,attributes项便是Code属性的属性表。在Code属性中,属性表可能存在的属性如LineNumerTable属性、LocalVariableTable属性。

LineNumerTable属性:被用来映射源码文件中给定的代码行号对应code[]字节码指令中的哪一部分,在调试时用到,在方法抛出异常打印异常栈信息也会用到。

LocalVariableTable属性:用来描述code[]中的某个范围内,局部变量表中某个位置存储的变量的名称是什么,用于与源码文件中局部变量名做映射。该属性不一定会编译到class文件中,如果没有该属性,那么查看反编译后的java代码将会使用诸如arg0、arg1、arg2之类的名称代替局部变量的名称。
Code属性的exception_table项是存储方法中的try-catch信息的异常表,异常表的每一项都是固定的结构体,异常结构如表


Code_attribute

public class Code_attribute {

    private U2 attribute_name_index;
    private U4 attribute_length;
    private U2 max_stack;
    private U2 max_locals;
    private U4 code_length;
    private byte[] code;
    private U4 exception_table_length;

    @Getter
    @Setter
    public static class Exception {
        private U2 start_pc;
        private U2 end_pc;
        private U2 handler_pc;
        private U2 catch_type;
    }

    private Exception[] exception_table;
    private U2 attributes_count;
    private AttributeInfo[] attributes;

}

对Code属性进行二次解析主要是想拿到字节码信息,属性表和异常表我们就不解析了

/**
 * 属性加工厂,即对属性进行二次解析
 */
public class AttributeProcessingFactory {

    public static ConstantValue_attribute processingConstantValue(AttributeInfo attributeInfo) {
        ConstantValue_attribute attribute = new ConstantValue_attribute();
        attribute.setAttribute_name_index(attributeInfo.getAttribute_name_index());
        attribute.setAttribute_length(attributeInfo.getAttribute_length());
        attribute.setConstantvalue_index(new U2(attributeInfo.getInfo()[0], attributeInfo.getInfo()[1]));
        return attribute;
    }

    public static Code_attribute processingCode(AttributeInfo attributeInfo) {
        Code_attribute code = new Code_attribute();
        ByteBuffer body = ByteBuffer.wrap(attributeInfo.getInfo());
        // 操作数栈大小
        code.setMax_stack(new U2(body.get(),body.get()));
        // 局部变量表大小
        code.setMax_locals(new U2(body.get(),body.get()));
        // 字节码数组长度
        code.setCode_length(new U4(body.get(),body.get(),body.get(),body.get()));
        // 解析获取字节码
        byte[] byteCode = new byte[code.getCode_length().toInt()];
        body.get(byteCode,0,byteCode.length);
        code.setCode(byteCode);
        // 其它暂时不做解析
        body.clear();
        return code;
    }

}

现在编写单元测试,使用我们编写好的Class文件结构解析项目,先将class文件解析为一个ClassFile对象,然后再遍历该ClassFile中的方法表,获取每个方法中的Code属性,再对Code属性进行二次解析

CodeAttributeTest

public class CodeAttributeTest {

    @Test
    public void testCodeAttribute() throws Exception {
        ByteBuffer codeBuf = ClassFileAnalysisMain.readFile("D:\\codespace\\monitor_26\\target\\classes\\com\\caecc\\controller\\BusinesschainController.class");

        ClassFile classFile = ClassFileAnalysiser.analysis(codeBuf);
        // 获取所有方法
        MethodInfo[] methodInfos = classFile.getMethods();
        for (MethodInfo methodInfo : methodInfos) {
            // 获取方法的所有属性
            AttributeInfo[] attributeInfos = methodInfo.getAttributes();
            if (attributeInfos == null || attributeInfos.length == 0) {
                continue;
            }
            System.out.println("方法:" + classFile.getConstant_pool()[methodInfo.getName_index().toInt() - 1]);
            for (AttributeInfo attributeInfo : attributeInfos) {
                // 获取属性的名称
                U2 name_index = attributeInfo.getAttribute_name_index();
                CONSTANT_Utf8_info name_info = (CONSTANT_Utf8_info) classFile.getConstant_pool()[name_index.toInt() - 1];
                String name = new String(name_info.getBytes());
                if (name.equalsIgnoreCase("Code")) {
                    // 属性二次解析
                    Code_attribute code = AttributeProcessingFactory.processingCode(attributeInfo);
                    System.out.println("操作数栈大小:" + code.getMax_stack().toInt());
                    System.out.println("局部变量表大小:" + code.getMax_locals().toInt());
                    System.out.println("字节码数组长度:" + code.getCode_length().toInt());
                    System.out.println("字节码:\n" + HexStringUtils.toHexString(code.getCode()));
                    System.out.println("\n");
                }
            }
        }
    }

}

测试结果


懂的都懂,一定要结合我前面的文章看,通过编写一个简单的Class文件结构解析工具项目,不仅对Class文件结构有了深刻的了解,还能自己实现Class文件结构的解析。了解Class文件结构是学习Java虚拟机字节码指令的前提条件。

下一篇我们正式学习字节码指令!

上一篇下一篇

猜你喜欢

热点阅读