手写Java类解析器-01.class文件的基本结构

付威     2020-04-12   5257   15min  

java的运行过程

在运行一段java代码的时候需要经过编译,验证,加载运行,具体如下图:

类加载过程

这个系列的文章是为了探讨Java字节码是什么样的结构,如何能够准确的表达我们代码的含义。

为了探讨我们的源代码和Java字节码的关系,我们先写一段代码,尽量多用上java的关键字和特殊的方法,以便我们测试和对比:

package org.rz;
public class AppMain {
	private String userName;
	private int age;
	private short sex;
	private long userCode;
	private double income;
	private float ablity;
	private final static String userId="000000001";
	private final long indentityCardNo=34122202019029L;
	
	public String getUserName() {
		return userName;
	}
	
	public void setUserName(String userName) {
		this.userName = userName;
	}
	
	public int getAge() {
		return age;
	}
	
	public void setAge(int age) {
		this.age = age;
	}
	
	public short getSex() {
		return sex;
	}
	
	public void setSex(short sex) {
		this.sex = sex;
	}
	
	public long getUserCode() {
		return userCode;
	}
	
	public void setUserCode(long userCode) {
		this.userCode = userCode;
	}
	
	public double getIncome() {
		return income;
	}
	
	public float getAblity() {
		return ablity;
	}
	
	public void setAblity(float ablity) {
		this.ablity = ablity;
	}
	
	public static String getUserId() {
		return userId;
	}
	
	public long getIndentityCardNo() {
		return indentityCardNo;
	}
	public String getUserInfo() {
		return "AppMain{" + "userName='" + userName + '\'' + ", age=" + age + ", sex=" + sex + ", userCode=" + userCode + ", income=" + income + ", ablity=" + ablity + ", indentityCardNo=" + indentityCardNo + '}';
	}
	
	private static AppMain appmain;
	public static AppMain getInstance() {
		synchronized (AppMain.class) {
			if (appmain == null) {
				appmain = new AppMain();
			}
			return appmain;
		}
	}
	
	public synchronized boolean changeName(String name) {
		if (StringUtils.isEmpty(name)) {
			return false;
		}
		if (StringUtils.isEmpty(this.getUserName())) {
			this.setUserName(name);
		}
		return true;
	}
	
	private void testMain(){
		System.out.println("init ok");
	}
}

编译后得到AppMain.class文件,用文本工具打开后,结果如下图:
AppClass文件
对于这个文本,我们可以尝试使用JDK中的类加载工具加载看下效果。


JDK中如何解析class文件

在原生的JDK中有对java字节码的读取的工具类com.sun.tools,具体使用如下:


File file=new File("/Users/fuwei/work/rzframe/rz-lib/src/test/resources/AppMain.class");
try {
    ClassFile read = ClassFile.read(file);
    System.out.println(read.toString());
} catch (IOException e) {
    e.printStackTrace();
} catch (ConstantPoolException e) {
    e.printStackTrace();
}

加载结果调试监控如下:

类加载过程

class文件的基本结构

根据JVM的虚拟机规范(SE8)提供的资料,字节码对应的结构体如下:

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];
}

其中u2u4分别代表占用的字节,u2代表占用两个字节,u4代表占用两个字节 对应的结构图如下:

class文件结构

在我们了解了class的结构之后,就可以开始试着解析class文件。

解析过程

  1. 读取类文件

     private DataInputStream dataInputStream;		
     public ClassReadCursor(String filePath) {
         try {
             byte[] bytes = Files.readAllBytes(Paths.get(filePath));
             ByteBuffer byteBuffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN);
             this.dataInputStream = new DataInputStream(new ByteArrayInputStream(byteBuffer.array()));
         } catch (IOException e) {
             e.printStackTrace();
         }
    		
     }
    

    在上面的操作中,我们把类的文件成功的转换成流文件DataInputStream,我们是类文件是按照顺序读取的,所以可以定义的游标的对象cursor来读取,对cursor可以封装几个读取的方法:

     public void readFully(byte[] byteArr) throws IOException {
             this.dataInputStream.readFully(byteArr);
         }
    		
         public int readUnsignedByte() throws IOException {
             return this.dataInputStream.readUnsignedByte();
         }
    		
         public int readUnsignedShort() throws IOException {
             return this.dataInputStream.readUnsignedShort();
         }
    		
         public int readInt() throws IOException {
             return this.dataInputStream.readInt();
         }
    		
         public long readLong() throws IOException {
             return this.dataInputStream.readLong();
         }
    		
         public float readFloat() throws IOException {
             return this.dataInputStream.readFloat();
         }
    		
         public double readDouble() throws IOException {
             return this.dataInputStream.readDouble();
         }
    		
         public String readUTF() throws IOException {
             return this.dataInputStream.readUTF();
         }
     }
    

  2. 获得魔数

    Java中的魔数是一个固定的值cafe babe,一共占用4个字节,我们可以通过简单的方式把魔数取出来:

     byte[] byteArr=new byte[4];
     cursor.readFully(byteArr);
     System.out.println(StringUtils.binaryToHexString(byteArr));
    
    

    对应16进制转字符的方法:

    public static String binaryToHexString(byte[] bytes) {
         String hexStr = "0123456789ABCDEF";
         StringBuilder result = new StringBuilder();
         String hex = "";
         for (int i = 0; i < bytes.length; i++) {
             //字节高4位
             hex = String.valueOf(hexStr.charAt((bytes[i] & 0xF0) >> 4));
             //字节低4位
             hex += String.valueOf(hexStr.charAt(bytes[i] & 0x0F));
             result.append(hex);
         }
         return result.toString();
     }
    

  3. 获得jdk版本

     minor_version = cursor.readUnsignedShort();
     major_version = cursor.readUnsignedShort();
    

    得到的结果分别是:

      minor_version=0, major_version=52
    

class文件结构

根据版本号插叙对应的jdk的版本可以看出,笔者的jdk的版本是1.8

(本文完)

作者:付威

博客地址:http://blog.laofu.online

如果觉得对您有帮助,可以下方的RSS订阅,谢谢合作

如有任何知识产权、版权问题或理论错误,还请指正。

本文是付威的网络博客原创,自由转载-非商用-非衍生-保持署名,请遵循:创意共享3.0许可证

交流请加群113249828: 点击加群   或发我邮件 laofu_online@163.com

付威

获得最新的博主文章,请关注上方公众号