写在前面
源码 。
本文尝试来解析下class文件的内容,了解了class文件内容后,对我们提升java认知将会带来很大的帮助,有多大呢,不好说,总之很大很大,大到受不了😍😍😍。
1:前置知识
在开始正式解析class文件的结构之前,我们需要对class文件的结构有一个大概的了解,关于这部分内容,你可以参考我的另一篇文章class字节码文件结构是什么样子的? 。
另外还需要对class查找相关知识有一些了解,关于这部分内容你可以参考我的另一篇文章用Java手写jvm之实现查找class 。以下内容涉及到class查找的内容将不做单独讲解,源码部分也不再做单独说明,请知悉!
你还需要对常量池的结构有所了解,关于这部分内容可以参考我的另一篇文章class字节码文件常量池的结构以及都有哪些类型的数据 。
内容稍多,稳住!!!
2:正戏
在Java中万物皆对象,所以先来定义一个代表class文件的对象ClassFile:
java
/**
* 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]
* }
*/
public class ClassFile {
// minor version,小版本,一般是0
private int minorVersion;
// major version,主版本,8 52 其他依次+1和-1
private int majorVersion;
// 常量池
private ConstantPool constantPool;
// 访问修饰符,不同的int值代表不同的访问修饰符
private int accessFlags;
// 类信息值 常量池位置
private int thisClassIdx;
// 父类信息值 常量池位置
private int supperClassIdx;
// 父接口信息值 常量池位置
private int[] interfaces;
// 字段信息
private MemberInfo[] fields;
// 方法信息
private MemberInfo[] methods;
private AttributeInfo[] attributes;
/**
* @param classData class对应的字节码二进制内容
*/
public ClassFile(byte[] classData) {
ClassReader reader = new ClassReader(classData);
this.readAndCheckMagic(reader);
this.readAndCheckVersion(reader);
this.constantPool = this.readConstantPool(reader);
// this.accessFlags = reader.readUint16();
/*
访问修饰符(十六进制表示)
ACC_PUBLIC = 0x0001;
ACC_PRIVATE = 0x0002;
ACC_PROTECTED = 0x0004;
ACC_STATIC = 0x0008;
ACC_FINAL = 0x0010;
ACC_SUPER = 0x0020;
ACC_SYNCHRONIZED = 0x0020;
ACC_VOLATILE = 0x0040;
ACC_BRIDGE = 0x0040;
ACC_TRANSIENT = 0x0080;
ACC_VARARGS = 0x0080;
ACC_NATIVE = 0x0100;
ACC_INTERFACE = 0x0200;
ACC_ABSTRACT = 0x0400;
ACC_STRICT = 0x0800;
ACC_SYNTHETIC = 0x1000;
ACC_ANNOTATION = 0x2000;
ACC_ENUM = 0x4000;
*/
this.accessFlags = reader.readU2();
// this.thisClassIdx = reader.readUint16();
// 当前所属类 utf-8常量值 常量池数组索引地址
this.thisClassIdx = reader.readU2();
// this.supperClassIdx = reader.readUint16();
// 父类
this.supperClassIdx = reader.readU2();
// 实现的接口们
this.interfaces = reader.readUint16s();
this.fields = MemberInfo.readMembers(reader, constantPool);
this.methods = MemberInfo.readMembers(reader, constantPool);
this.attributes = AttributeInfo.readAttributes(reader, constantPool);
}
// ...
}
在ClassFile中定义了魔法数字,版本号,常量值,字段信息等,这些数据都来自于class字节码文件,所以我们需要拥有解析class文件的能力,为此,需要再来定义一个class文件的读取类ClassReader:
java
/**
* 负责读取class文件信息的类
*/
public class ClassReader {
/**
* class文件对应的二进制数据
*/
private byte[] clazzData;
public ClassReader(byte[] clazzData) {
this.clazzData = clazzData;
}
/**
* jvm虚拟机规范,定义了u1,u2,u4三种数据类型,分别表示1字节无符号整数,2字节无符号整数,4字节无符号整数
* class字节码文件中的各种结构,基本都是通过这3中数据类型来定义的,可参考如下结构体
* ClassFile {
* u4 magic;
* u2 minor_version;
* u2 major_version;
* ...
* }
* 说明magic是u4类型,即magic是一个4字节的无符号整数,其他类似
*/
private final static int JVM_DATA_TYPE_U1 = 1;
private final static int JVM_DATA_TYPE_U2 = 2;
private final static int JVM_DATA_TYPE_U4 = 4;
// u1 转int 读1个字节
public int readU1() {
// byte[] val = readByte(1);
byte[] val = readByte(JVM_DATA_TYPE_U1);
return byte2int(val);
}
// u2 转int 读2个字节
public int readU2() {
// byte[] val = readByte(2);
byte[] val = readByte(JVM_DATA_TYPE_U2);
return byte2int(val);
}
// u4 转long 转long就可以高位填0,就可以作为正数显示
public long readU4() {
// byte[] val = readByte(4);
byte[] val = readByte(JVM_DATA_TYPE_U4);
String str_hex = new BigInteger(1, val).toString(16);
return Long.parseLong(str_hex, 16);
}
// u4 转int
public int readU4TInteger(){
byte[] val = readByte(4);
return new BigInteger(1, val).intValue();
}
public float readUint64TFloat() {
byte[] val = readByte(8);
return new BigInteger(1, val).floatValue();
}
public long readUint64TLong() {
byte[] val = readByte(8);
return new BigInteger(1, val).longValue();
}
public double readUint64TDouble() {
byte[] val = readByte(8);
return new BigInteger(1, val).doubleValue();
}
public int[] readUint16s() {
// int n = this.readUint16();
int n = this.readU2();
int[] s = new int[n];
for (int i = 0; i < n; i++) {
// s[i] = this.readUint16();
s[i] = this.readU2();
}
return s;
}
public byte[] readBytes(int n) {
return readByte(n);
}
private byte[] readByte(int length) {
byte[] copy = new byte[length];
System.arraycopy(clazzData, 0, copy, 0, length);
System.arraycopy(clazzData, length, clazzData, 0, clazzData.length - length);
return copy;
}
private int byte2int(byte[] val) {
String str_hex = new BigInteger(1, val).toString(16);
return Integer.parseInt(str_hex, 16);
}
}
在我们了解了class文件的结构之后就可以通过该工具类按照u1,u2,u4的长度来读取对应的信息,之后进行封装了。
2.1:常量池
为了表示常量池,我们定义类ConstantPool:
java
public class ConstantPool {
private ConstantInfo[] constantInfos;
private final int siz;
public ConstantPool(ClassReader reader) {
// siz = reader.readUint16();
siz = reader.readU2();
constantInfos = new ConstantInfo[siz];
for (int i = 1; i < siz; i++) {
constantInfos[i] = ConstantInfo.readConstantInfo(reader, this);
switch (constantInfos[i].tag()) {
case ConstantInfo.CONSTANT_TAG_DOUBLE:
case ConstantInfo.CONSTANT_TAG_LONG:
i++;
break;
}
}
}
// ...
}
ConstantInfo是代表常量类型的接口,如下:
java
/**
* 常量池接口
*/
public interface ConstantInfo {
int CONSTANT_TAG_CLASS = 7;
int CONSTANT_TAG_FIELDREF = 9;
int CONSTANT_TAG_METHODREF = 10;
int CONSTANT_TAG_INTERFACEMETHODREF = 11;
int CONSTANT_TAG_STRING = 8;
int CONSTANT_TAG_INTEGER = 3;
int CONSTANT_TAG_FLOAT = 4;
int CONSTANT_TAG_LONG = 5;
int CONSTANT_TAG_DOUBLE = 6;
int CONSTANT_TAG_NAMEANDTYPE = 12;
int CONSTANT_TAG_UTF8 = 1;
int CONSTANT_TAG_METHODHANDLE = 15;
int CONSTANT_TAG_METHODTYPE = 16;
int CONSTANT_TAG_INVOKEDYNAMIC = 18;
}
因为不同类型的常量取值方式都都不尽相同,所以针对每种常量定义对应的子类,如下:
具体看源码吧!
为了方便测试,我们先来编写一个要解析的类:
java
public class TestConstantsPool {
private static final String nameA = "jackkkk";
private static final int ageB = 56;
private static final Object objC = new Object();
public static void main(String[] args) {
System.out.println("jjjjjjjjjjjjjjjjj");
}
}
接着如下代码测试:
java
/**
* program arguments:-Xthejrepath D:\programs\javas\java1.8/jre -Xthetargetclazz D:\test\itstack-demo-jvm-master\find-class-from-classpath\target\test-classes\com\dahuyou\find\clazz\test\ShowMeByteCode
*/
public class Main {
public static void main(String[] args) {
Cmd cmd = Cmd.parse(args);
if (!cmd.ok || cmd.helpFlag) {
System.out.println("Usage: <main class> [-options] class [args...]");
return;
}
if (cmd.versionFlag) {
//注意案例测试都是基于1.8,另外jdk1.9以后使用模块化没有rt.jar
System.out.println("java version \"1.8.0\"");
return;
}
startJVM(cmd);
}
private static void startJVM(Cmd cmd) {
// 创建classpath
Classpath cp = new Classpath(cmd.thejrepath, cmd.classpath);
// System.out.printf("classpath:%s class:%s args:%s\n", cp, cmd.getMainClass(), cmd.getAppArgs());
System.out.printf("classpath:%s parsed class:%s \n", cp, cmd.thetargetclazz);
//获取className
// String className = cmd.getMainClass().replace(".", "/");
try {
// byte[] classData = cp.readClass(className);
/*byte[] classData = cp.readClass(cmd.thetargetclazz.replace(".", "/"));
System.out.println(Arrays.toString(classData));
System.out.println("classData:");
for (byte b : classData) {
//16进制输出
System.out.print(String.format("%02x", b & 0xff) + " ");
}*/
String clazzName = cmd.thetargetclazz.replace(".", "/");
// 创建className对应的ClassFile对象
ClassFile classFile = loadClass(clazzName, cp);
printClassInfo(classFile);
} catch (Exception e) {
System.out.println("Could not find or load main class " + cmd.getMainClass());
e.printStackTrace();
}
}
private static void printClassInfo(ClassFile cf) {
System.out.println("version: " + cf.majorVersion() + "." + cf.minorVersion());
System.out.println("constants count:" + cf.constantPool().getSiz());
ConstantInfo[] constantInfoArr = cf.constantPool().getConstantInfos();
System.out.println("-------常量池信息开始------");
for (int i = 1; i < constantInfoArr.length; i++) {
System.out.println("常量位置:" + i);
ConstantInfo constantInfo = constantInfoArr[i];
if (constantInfo != null) constantInfo.showInfo();
}
System.out.println("-------常量池信息结束------");
/*System.out.format("access flags:0x%x\n", cf.accessFlags());
System.out.println("this class:" + cf.className());
System.out.println("super class:" + cf.superClassName());
System.out.println("interfaces:" + Arrays.toString(cf.interfaceNames()));
System.out.println("fields count:" + cf.fields().length);
for (MemberInfo memberInfo : cf.fields()) {
System.out.format("000000%s \t\t %s\n", memberInfo.name(), memberInfo.descriptor());
}*/
/*System.out.println("methods count: " + cf.methods().length);
for (MemberInfo memberInfo : cf.methods()) {
System.out.format("%s \t\t %s\n", memberInfo.name(), memberInfo.descriptor());
}*/
}
/**
* 生成class文件对象
* @param clazzName
* @param cp
* @return
*/
private static ClassFile loadClass(String clazzName, Classpath cp) {
try {
// 获取类class对应的byte数组
byte[] classData = cp.readClass(clazzName);
return new ClassFile(classData);
} catch (Exception e) {
System.out.println("无法加载到类: " + clazzName);
return null;
}
}
}
然后要配置program arguments,如下:
-Xthejrepath D:\programs\javas\java1.8/jre -Xthetargetclazz D:\test\itstack-demo-jvm-master\try-to-parse-clazz-file\target\test-classes\com\dahuyou\test\TestConstantsPool
运行:
classpath:com.dahuyou.tryy.too.parse.clazz.file.classpath.Classpath@bebdb06 parsed class:D:\test\itstack-demo-jvm-master\try-to-parse-clazz-file\target\test-classes\com\dahuyou\test\TestConstantsPool
version: 52.0
constants count:47
-------常量池信息开始------
常量位置:1
常量位置:2
tag is: 9, 字段符号引用信息是:{name=out, _type=Ljava/io/PrintStream;}
常量位置:3
tag 是:8, string 常量值是:jjjjjjjjjjjjjjjjj
常量位置:4
常量位置:5
tag 是:7, class 或 interface的符号引用是: java/lang/Object
常量位置:6
tag is: 9, 字段符号引用信息是:{name=objC, _type=Ljava/lang/Object;}
常量位置:7
tag 是:7, class 或 interface的符号引用是: com/dahuyou/test/TestConstantsPool
常量位置:8
tag 是:1, utf8值是:nameA
常量位置:9
tag 是:1, utf8值是:Ljava/lang/String;
常量位置:10
tag 是:1, utf8值是:ConstantValue
常量位置:11
tag 是:8, string 常量值是:jackkkk
常量位置:12
tag 是:1, utf8值是:ageB
常量位置:13
tag 是:1, utf8值是:I
常量位置:14
tag 是:3, integer值是:56
常量位置:15
tag 是:1, utf8值是:objC
常量位置:16
tag 是:1, utf8值是:Ljava/lang/Object;
常量位置:17
tag 是:1, utf8值是:<init>
常量位置:18
tag 是:1, utf8值是:()V
常量位置:19
tag 是:1, utf8值是:Code
常量位置:20
tag 是:1, utf8值是:LineNumberTable
常量位置:21
tag 是:1, utf8值是:LocalVariableTable
常量位置:22
tag 是:1, utf8值是:this
常量位置:23
tag 是:1, utf8值是:Lcom/dahuyou/test/TestConstantsPool;
常量位置:24
tag 是:1, utf8值是:main
常量位置:25
tag 是:1, utf8值是:([Ljava/lang/String;)V
常量位置:26
tag 是:1, utf8值是:args
常量位置:27
tag 是:1, utf8值是:[Ljava/lang/String;
常量位置:28
tag 是:1, utf8值是:<clinit>
常量位置:29
tag 是:1, utf8值是:SourceFile
常量位置:30
tag 是:1, utf8值是:TestConstantsPool.java
常量位置:31
tag 是:12, name and type 是:
常量位置:32
tag 是:7, class 或 interface的符号引用是: java/lang/System
常量位置:33
tag 是:12, name and type 是:
常量位置:34
tag 是:1, utf8值是:jjjjjjjjjjjjjjjjj
常量位置:35
tag 是:7, class 或 interface的符号引用是: java/io/PrintStream
常量位置:36
tag 是:12, name and type 是:
常量位置:37
tag 是:1, utf8值是:java/lang/Object
常量位置:38
tag 是:12, name and type 是:
常量位置:39
tag 是:1, utf8值是:com/dahuyou/test/TestConstantsPool
常量位置:40
tag 是:1, utf8值是:jackkkk
常量位置:41
tag 是:1, utf8值是:java/lang/System
常量位置:42
tag 是:1, utf8值是:out
常量位置:43
tag 是:1, utf8值是:Ljava/io/PrintStream;
常量位置:44
tag 是:1, utf8值是:java/io/PrintStream
常量位置:45
tag 是:1, utf8值是:println
常量位置:46
tag 是:1, utf8值是:(Ljava/lang/String;)V
-------常量池信息结束------
Process finished with exit code 0
可以看到变量名称,变量的值字面量,符号应用信息等都可以正常输出出来了。
2.2:字段信息和方法信息
java
System.out.format("access flags:0x%x\n", cf.accessFlags());
System.out.println("this class:" + cf.className());
System.out.println("super class:" + cf.superClassName());
System.out.println("interfaces:" + Arrays.toString(cf.interfaceNames()));
System.out.println("fields count:" + cf.fields().length);
for (MemberInfo memberInfo : cf.fields()) {
System.out.format("字段信息:%s \t\t %s\n", memberInfo.name(), memberInfo.descriptor());
}
System.out.println("methods count: " + cf.methods().length);
for (MemberInfo memberInfo : cf.methods()) {
System.out.format("方法信息:%s \t\t %s\n", memberInfo.name(), memberInfo.descriptor());
}
输出:
classpath:com.dahuyou.tryy.too.parse.clazz.file.classpath.Classpath@bebdb06 parsed class:D:\test\itstack-demo-jvm-master\try-to-parse-clazz-file\target\test-classes\com\dahuyou\test\TestConstantsPool
version: 52.0
constants count:47
access flags:0x21
this class:com/dahuyou/test/TestConstantsPool
super class:java/lang/Object
interfaces:[]
fields count:3
字段信息:nameA Ljava/lang/String;
字段信息:ageB I
字段信息:objC Ljava/lang/Object;
methods count: 3
方法信息:<init> ()V
方法信息:main ([Ljava/lang/String;)V
方法信息:<clinit> ()V
Process finished with exit code 0