位元組碼詳解

封何發表於2020-10-08

位元組碼詳解

前言

萬事開頭難

位元組碼相關內容往深了挖其實東西很多,我就按照自己學習的一個心理歷程去分享一下這塊兒的內容,起個拋磚引玉的作用,很多地方沒有特別深入的研究,有待大家補充。

什麼是位元組碼

Java作為一款“一次編譯,到處執行”的程式語言,跨平臺靠的是JVM實現對不同作業系統API的支援,而一次編譯指的就是class位元組碼;即我們編寫好的.java檔案,通過編譯器編譯成.class檔案,JVM負責載入解釋位元組碼檔案,並生成系統可識別的程式碼執行(具體解析本次不做深入研究).

Class檔案

The class File Format

hello world

從程式碼開始:

package com.qty.first;

public class ClassDemo {
	
	public static void main(String[] args) {
		System.out.println("hello world!!");
	}
}

直接在IDE下新建專案,寫一個Hello World程式,用文字編輯器開啟生成的ClassDemo.class檔案,如下:
在這裡插入圖片描述
不可讀的亂碼,我們用16進位制方式開啟:
在這裡插入圖片描述

已經有點可讀的樣子,跟程式碼比起來,可讀性確實不高,但這就是接下來的任務,分析這些16進位制。

class結構

下面是官方文件給出的定義:

ClassFile {
    u4             magic; //魔數
    u2             minor_version; //次版本號
    u2             major_version; //主版本號
    u2             constant_pool_count; //常量池數量+1
    cp_info        constant_pool[constant_pool_count-1]; //常量池
    u2             access_flags; // 訪問標識
    u2             this_class; // 常量池的有效下標
    u2             super_class; // 常量池的有效下標
    u2             interfaces_count; // 介面數
    u2             interfaces[interfaces_count];// 下標從0開始,元素為常量池的有效下標
    u2             fields_count;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

為什麼是CafeBabe

其他地方的16進位制沒那麼顯眼,唯獨開頭的4個位元組開起來像是個單詞CAFEBABE.

為什麼所有檔案都要有一個魔數開頭,其實就是讓JVM有一個穩定快速的途徑來確認這個檔案是位元組碼檔案。

為什麼一定是CafeBabe,源於Java與咖啡的不解之緣。像是zip檔案的PK.

Unsupported major.minor version 51.0

這個報錯大家應該都見過,出現這個報錯的時候都知道是JDK版本不對,立馬去IDE上修改JDK編譯版本、執行版本,OK報錯解決。不過為什麼JDK不一致時會報錯呢,JVM是怎麼確定版本不一致的?

從位元組碼檔案說,CafeBabe繼續往後看八個位元組,分別是00000034,我本地環境使用的是JDK1.8

class檔案中看到的是16進位制,把0034轉為10進位制的數字就是52。我用JDK1.7編譯之後,如下:
在這裡插入圖片描述
主版本號對應的兩個位元組,根據我們本地編譯版本不同也會不同。

下面是JDK版本與版本號對應關係:

jdk版本major.minor version
1.145
1.246
1.347
1.448
549
650
751
852

類的訪問標識

訪問標識型別表:

Flag NameValueInterpretation
ACC_PUBLIC0x0001Declared public; may be accessed from outside its package.
ACC_FINAL0x0010Declared final; no subclasses allowed.
ACC_SUPER0x0020Treat superclass methods specially when invoked by the invokespecial instruction.
ACC_INTERFACE0x0200Is an interface, not a class.
ACC_ABSTRACT0x0400Declared abstract; must not be instantiated.
ACC_SYNTHETIC0x1000Declared synthetic; not present in the source code.這個關鍵字不是原始碼生成,而是編譯器生成的
ACC_ANNOTATION0x2000Declared as an annotation type.
ACC_ENUM0x4000Declared as an enum type.

型別同時存在時進行+操作,如public final的值就是0x0011.

ACC_SYNTHETIC型別是編譯器根據實際情況生成,比如內部類的private方法在外部類呼叫的時候,違反了private只能本類呼叫的原則,但IDE編譯時並不會報錯,因為在生成內部類的時候加上了ACC_SYNTHETIC型別修飾

常量池

常量池數量是實際常量個數+1,常量池下標從1開始,到n-1結束;cp_info結構根據不同型別的常量,擁有不同的位元組數,通用結構為:

cp_info {
    u1 tag;
    u1 info[];//根據tag不同,長度不同
}

即每個結構體第一個位元組標識了當前常量的型別,型別表如下:

Constant TypeValue
CONSTANT_Class7
CONSTANT_Fieldref9
CONSTANT_Methodref10
CONSTANT_InterfaceMethodref11
CONSTANT_String8
CONSTANT_Integer3
CONSTANT_Float4
CONSTANT_Long5
CONSTANT_Double6
CONSTANT_NameAndType12
CONSTANT_Utf81
CONSTANT_MethodHandle15
CONSTANT_MethodType16
CONSTANT_InvokeDynamic18

不同常量對應後續位元組數不同,如CONSTANT_ClassCONSTANT_Utf8_info

CONSTANT_Class_info {
    u1 tag;
    u2 name_index;//name_index需要是常量池中有效下標
}

CONSTANT_Utf8_info {
    u1 tag;
    u2 length; //bytes的長度,即位元組數
    u1 bytes[length];
}

PS: 為什麼constant_pool_count的值是常量池的數量+1,從1開始到n-1結束?不從0開始的原因是什麼?

這個問題在這裡提一下,因為常量池中很多常量需要引用其他常量,而有可能存在常量並不需要任何有效引用,所以常量池空置了下標0的位置作為備用

還是拿Hello World為例,複製前面一段來講:

CA FE BA BE 00 00 00 33  00 22 07 00 02 01 00 17
63 6F 6D 2F 71 74 79 2F  66 69 72 73 74 2F 43 6C
61 73 73 44 65 6D 6F 07  00 04 01 00 10 6A 61 76
61 2F 6C 61 6E 67 2F 4F  62 6A 65 63 74 01 00 06
  • CA FE BA BE是魔數,00 00 00 33為主次版本號
  • 00 22表示常量池數量+1,0X22 = 34即常量池長度為33
  • 再往後一個位元組就是第一個常量的tag,07從常量型別表中可以看到型別是CONSTANT_Class_info,那麼第一個常量就是CONSTANT_Class_info,name_index為:00 02,即是常量池中第二個常量
  • 繼續往後取一個位元組就是第二個常量的tag,01CONSTANT_Utf8_info,那麼接下來的兩個自己就是bytes陣列的長度即後續的位元組數,0X0017 = 23也就是第二個常量還需要在讀取23個位元組63 6F 6D 2F 71 74 79 2F 66 69 72 73 74 2F 43 6C 61 73 73 44 65 6D 6F,這個23個位元組轉成字串就是com/qty/first/ClassDemo也就是我們的類名

PS : CONSTANT_Utf8_info中字元可以參考UTF-8編碼的規則

下面貼上所有常量型別的結構,如果有興趣可以詳細去了解每個型別的結構及其含義:

CONSTANT_Fieldref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

CONSTANT_Methodref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

CONSTANT_InterfaceMethodref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}
CONSTANT_String_info {
    u1 tag;
    u2 string_index;
}
CONSTANT_Integer_info {
    u1 tag;
    u4 bytes;
}

CONSTANT_Float_info {
    u1 tag;
    u4 bytes;
}
CONSTANT_Long_info {
    u1 tag;
    u4 high_bytes;
    u4 low_bytes;
}

CONSTANT_Double_info {
    u1 tag;
    u4 high_bytes;
    u4 low_bytes;
}
CONSTANT_NameAndType_info {
    u1 tag;
    u2 name_index;
    u2 descriptor_index;
}
CONSTANT_MethodHandle_info {
    u1 tag;
    u1 reference_kind;
    u2 reference_index;
}
CONSTANT_MethodType_info {
    u1 tag;
    u2 descriptor_index;
}
CONSTANT_InvokeDynamic_info {
    u1 tag;
    u2 bootstrap_method_attr_index;
    u2 name_and_type_index;
}

Field-欄位

field結構如下:

field_info {
    u2             access_flags; //訪問標識
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count; //屬性個數
    attribute_info attributes[attributes_count];
}

field訪問標識型別如下:

Flag NameValueInterpretation
ACC_PUBLIC0x0001Declared public; may be accessed from outside its package.
ACC_PRIVATE0x0002Declared private; usable only within the defining class.
ACC_PROTECTED0x0004Declared protected; may be accessed within subclasses.
ACC_STATIC0x0008Declared static.
ACC_FINAL0x0010Declared final; never directly assigned to after object construction (JLS §17.5).
ACC_VOLATILE0x0040Declared volatile; cannot be cached.
ACC_TRANSIENT0x0080Declared transient; not written or read by a persistent object manager.
ACC_SYNTHETIC0x1000Declared synthetic; not present in the source code.
ACC_ENUM0x4000Declared as an element of an enum.

關於attribute_info後面再講。

Methods-方法

method_info的結構如下:

method_info {
    u2             access_flags; //訪問標識
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

類、欄位與方法的訪問標識型別都不太相同,方法的訪問標識如下:

Flag NameValueInterpretation
ACC_PUBLIC0x0001Declared public; may be accessed from outside its package.
ACC_PRIVATE0x0002Declared private; accessible only within the defining class.
ACC_PROTECTED0x0004Declared protected; may be accessed within subclasses.
ACC_STATIC0x0008Declared static.
ACC_FINAL0x0010Declared final; must not be overridden (§5.4.5).
ACC_SYNCHRONIZED0x0020Declared synchronized; invocation is wrapped by a monitor use.
ACC_BRIDGE0x0040A bridge method, generated by the compiler.
ACC_VARARGS0x0080Declared with variable number of arguments.
ACC_NATIVE0x0100Declared native; implemented in a language other than Java.
ACC_ABSTRACT0x0400Declared abstract; no implementation is provided.
ACC_STRICT0x0800Declared strictfp; floating-point mode is FP-strict.
ACC_SYNTHETIC0x1000Declared synthetic; not present in the source code.

ACC_BRIDGE也是由編譯器生成的,比如泛型的子類重寫父類方法, 就會有一個在子類生成一個新的方法用ACC_BRIDGE標識

ACC_VARARGS可變引數的方法會出現這個標記

ACC_STRICT strictfp標識的方法中,所有float和double表示式都嚴格遵守FP-strict的限制,符合IEEE-754規範.

Descriptors-描述

方法和欄位都有自己的描述資訊,方法的描述包括引數、返回值的型別,欄位描述為欄位的型別,下面是型別表:

FieldType termTypeInterpretation
Bbytesigned byte
CcharUnicode character code point in the Basic Multilingual Plane, encoded with UTF-16
Ddoubledouble-precision floating-point value
Ffloatsingle-precision floating-point value
Iintinteger
Jlonglong integer
L ClassName ;referencean instance of class ClassName
Sshortsigned short
Zbooleantrue or false
[referenceone array dimension

方法描述格式為:( {ParameterDescriptor} ) ReturnDescriptor

例如:

Object m(int i, double d, Thread t);

描述資訊就是:(IDLjava/lang/Thread;)Ljava/lang/Object;

物件型別的後面需要用;分割,基礎型別不需要

attribute-屬性

attribute_info型別比較多,這裡只把我們最關心的程式碼說下,即Code_attribute:

Code_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 max_stack;
    u2 max_locals;
    u4 code_length;
    u1 code[code_length];
    u2 exception_table_length;
    {   u2 start_pc;
        u2 end_pc;
        u2 handler_pc;
        u2 catch_type;
    } exception_table[exception_table_length];
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}

只要不是native、abstact修飾的方法,必須含有Code_attribute屬性

Code_attribute中包含codeexceptionattribute_info等資訊,這裡主要說下code中的內容。

code陣列中的內容就是方法中編譯後的程式碼:

         0: aload_0
         1: invokespecial #10                 // Method java/lang/Object."<init>":()V
         4: return

這個就是我們上面那個類的無參建構函式編譯後的效果,那這裡面的aload_0invokespecialreturn學過JVM相關知識的話,大家已經很熟悉了.

  • aload_0就是變數0進棧
  • invokespecial呼叫例項的初始化方法,即構造方法
  • return 即方法結束,返回值為void

那這些aload_0invokespecialreturn相關的指令是如何儲存在code陣列中的,或者說是以什麼形式存在的?

其實JVM有這樣一個指令陣列,code陣列中的記錄的就是指令陣列的有效下標,下面是部分指令:

CodeFormsdescribe
return0xB1當前方法返回void
areturn0xB0從方法中返回一個物件的引用
ireturn0xAC當前方法返回int
iload_00x1A第一個int型區域性變數進棧
lload_00x1E第一個long型區域性變數進棧
istore_00x3B將棧頂int型數值存入第一個區域性變數
lstore_00x3F將棧頂long型數值存入第一個區域性變數
getstatic0xB2獲取指定類的靜態域,並將其值壓入棧頂
putstatic0xB3為指定的類的靜態域賦值
invokespecial0xB7呼叫超類構造方法、例項初始化方法、私有方法
invokevirtual0xB6呼叫例項方法
iadd0x60棧頂兩int型數值相加,並且結果進棧
iconst_00x03int型常量值0進棧
ldc0x12將int、float或String型常量值從常量池中推送至棧頂

詳細指令列表可以檢視官方文件

關於attribute_info還有其他型別,有興趣的可以檢視Attribute,型別及其出現位置如下:

AttributeLocation
SourceFileClassFile
InnerClassesClassFile
EnclosingMethodClassFile
SourceDebugExtensionClassFile
BootstrapMethodsClassFile
ConstantValuefield_info
Codemethod_info
Exceptionsmethod_info
RuntimeVisibleParameterAnnotations, RuntimeInvisibleParameterAnnotationsmethod_info
AnnotationDefaultmethod_info
MethodParametersmethod_info
SyntheticClassFile, field_info, method_info
DeprecatedClassFile, field_info, method_info
SignatureClassFile, field_info, method_info
RuntimeVisibleAnnotations, RuntimeInvisibleAnnotationsClassFile, field_info, method_info
LineNumberTableCode
LocalVariableTableCode
LocalVariableTypeTableCode
StackMapTableCode
RuntimeVisibleTypeAnnotations, RuntimeInvisibleTypeAnnotationsClassFile, field_info, method_info, Code

javap

熟悉16進位制內容後,再來看看JDK提供的工具:

javap -verbose ClassDemo.class

可以參照反編譯效果對比之前16進位制檔案的分析,輸入如下:

Classfile /D:/eclipse-workspace/class-demo/bin/com/qty/first/ClassDemo.class
  Last modified 2020-10-7; size 560 bytes
  MD5 checksum 9e627e92c2887591a4d9d1cfd11d1f89
  Compiled from "ClassDemo.java"
public class com.qty.first.ClassDemo
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Class              #2             // com/qty/first/ClassDemo
   #2 = Utf8               com/qty/first/ClassDemo
   #3 = Class              #4             // java/lang/Object
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Methodref          #3.#9          // java/lang/Object."<init>":()V
   #9 = NameAndType        #5:#6          // "<init>":()V
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/qty/first/ClassDemo;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Fieldref           #17.#19        // java/lang/System.out:Ljava/io/PrintStream;
  #17 = Class              #18            // java/lang/System
  #18 = Utf8               java/lang/System
  #19 = NameAndType        #20:#21        // out:Ljava/io/PrintStream;
  #20 = Utf8               out
  #21 = Utf8               Ljava/io/PrintStream;
  #22 = String             #23            // hello world!!
  #23 = Utf8               hello world!!
  #24 = Methodref          #25.#27        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #25 = Class              #26            // java/io/PrintStream
  #26 = Utf8               java/io/PrintStream
  #27 = NameAndType        #28:#29        // println:(Ljava/lang/String;)V
  #28 = Utf8               println
  #29 = Utf8               (Ljava/lang/String;)V
  #30 = Utf8               args
  #31 = Utf8               [Ljava/lang/String;
  #32 = Utf8               SourceFile
  #33 = Utf8               ClassDemo.java
{
  public com.qty.first.ClassDemo();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #8                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/qty/first/ClassDemo;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #16                 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #22                 // String hello world!!
         5: invokevirtual #24                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 6: 0
        line 7: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
}
SourceFile: "ClassDemo.java"

位元組碼技術應用

位元組碼技術的應用場景包括但不限於AOP,動態生成程式碼,接下來講一下位元組碼技術相關的第三方類庫,第三方框架的講解是為了幫助大家瞭解位元組碼技術的應用方向,文件並沒有對框架機制進行詳細分析,有興趣的可以去了解相關框架實現原理和架構,也可以後續為大家奉上相關詳細講解。

ASM

ASM 是一個 Java 位元組碼操控框架,它能被用來動態生成類或者增強既有類的功能。ASM 可以直接產生二進位制 class 檔案,也可以在類被載入入 Java 虛擬機器之前動態改變類行為。

說白了,ASM可以在不修改Java原始碼檔案的情況下,直接對Class檔案進行修改,改變或增強原有類功能。

在熟悉了位元組碼原理的情況下,理解動態修改位元組碼技術會更加容易,接下來我們只針對ASM框架中幾個主要類進行分析,並舉個例子幫助大家理解。

主要類介紹

ClassVisitor

提供各種對位元組碼操作的方法,包括對屬性、方法、註解等內容的修改:

public abstract class ClassVisitor {
    /**
     * 	建構函式
     * @param api api的值必須等當前ASM版本號一直,否則報錯
     */
    public ClassVisitor(final int api) {
        this(api, null);
    }
    
    /**
     * 對類的頭部資訊進行修改
     *
     * @param version 版本號,從Opcodes中獲取
     * @param access 訪問標識,多種型別疊加使用'+'
     * @param name 類名,帶報名路徑,使用'/'分割
     * @param signature 簽名
     * @param superName 父類
     * @param interfaces 介面列表
     */
    public void visit(int version,int access,String name,String signature,String superName,String[] interfaces)
    {
        if (cv != null) {
            cv.visit(version, access, name, signature, superName, interfaces);
        }
    }
    
    /**
	 * 對欄位進行修改
	 * 
	 * @param access    訪問標識
	 * @param name      欄位名稱
	 * @param desc      描述
	 * @param signature 簽名
	 * @param value     欄位值
	 * @return
	 */
	public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
		if (cv != null) {
			return cv.visitField(access, name, desc, signature, value);
		}
		return null;
	}
	
	/**
	 * 對方法進行修改
	 * 
	 * @param access     訪問標識
	 * @param name       方法名稱
	 * @param desc       方法描述
	 * @param signature  簽名
	 * @param exceptions 異常列表
	 * @return
	 */
	public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
		if (cv != null) {
			return cv.visitMethod(access, name, desc, signature, exceptions);
		}
		return null;
	}
	
	/**
	* 終止編輯,對當前類的編輯結束時呼叫
	*/
	public void visitEnd() {
		if (cv != null) {
			cv.visitEnd();
		}
	}

}
ClassWriter

主要功能就是記錄所有位元組碼相關欄位,並提供轉換為位元組陣列的方法:

//ClassWriter繼承了ClassVisitor 即擁有了對class修改的功能
public class ClassWriter extends ClassVisitor {
    //下面這些成員變數,是不是很眼熟了
    private int access;
    private int name;
    String thisName;
    private int signature;
    private int superName;
    private int interfaceCount;
    private int[] interfaces;
    private int sourceFile;
    private Attribute attrs;
    private int innerClassesCount;
    private ByteVector innerClasses;
	FieldWriter firstField;
    MethodWriter firstMethod;
    
    //這個就是將快取的位元組碼封裝物件再進行轉換,按照Class檔案格式轉成位元組陣列
    public byte[] toByteArray() {
    }
}
ClassReader
//讀取Class檔案
public class ClassReader {
    /**
     * 建構函式
     * @param b Class檔案的位元組陣列
     */
	public ClassReader(final byte[] b) {
		this(b, 0, b.length);
	}
    
    /**
     * 相當於將ClassReader中讀取到的資料,轉存到classVisitor中,後續通過使用ClassVisitor的API對原Class進行修改、增強
     * @param classVisitor
     * @param flags
     */
    public void accept(final ClassVisitor classVisitor, final int flags) {
        accept(classVisitor, new Attribute[0], flags);
    }
    
}
Opcodes
public interface Opcodes {
    //這裡面的內容就是前面講到的JVM指令集合和各種訪問標識等常量
    // access flags
    int ACC_PUBLIC = 0x0001; // class, field, method
    int ACC_PRIVATE = 0x0002; // class, field, method
    int ACC_PROTECTED = 0x0004; // class, field, method
    int ACC_STATIC = 0x0008; // field, method
    int ACC_FINAL = 0x0010; // class, field, method
    int ACC_SUPER = 0x0020; // class
    int ACC_SYNCHRONIZED = 0x0020; // method
    int ACC_VOLATILE = 0x0040; // field
    int ACC_BRIDGE = 0x0040; // method
    int ACC_VARARGS = 0x0080; // method
    int ACC_TRANSIENT = 0x0080; // field
    int ACC_NATIVE = 0x0100; // method
    int ACC_INTERFACE = 0x0200; // class
    int ACC_ABSTRACT = 0x0400; // class, method
    int ACC_STRICT = 0x0800; // method
    int ACC_SYNTHETIC = 0x1000; // class, field, method
    int ACC_ANNOTATION = 0x2000; // class
    int ACC_ENUM = 0x4000; // class(?) field inner
    
    int NOP = 0; // visitInsn
    int ACONST_NULL = 1; // -
    int ICONST_M1 = 2; // -
    int ICONST_0 = 3; // -
    int ICONST_1 = 4; // -
    int ICONST_2 = 5; // -
    int ICONST_3 = 6; // -
    int ICONST_4 = 7; // -
    int ICONST_5 = 8; // -
    int LCONST_0 = 9; // -
    int LCONST_1 = 10; // -
    int FCONST_0 = 11; // -
    int FCONST_1 = 12; // -
    int FCONST_2 = 13; // -
    int DCONST_0 = 14; // -
    int DCONST_1 = 15; // -
    int BIPUSH = 16; // visitIntInsn
    int SIPUSH = 17; // -
    int LDC = 18; // visitLdcInsn
   
}

以上這些類都只是擷取其中一部分,旨在講解思路。

舉個例子

廢話不多說,直接獻上程式碼:

package com.qty.classloader;

import java.io.File;
import java.io.FileOutputStream;
import java.lang.reflect.Method;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class AsmDemo {

	public static void main(String[] args) throws Exception {
		// 生成一個類只需要ClassWriter元件即可
		ClassWriter cw = new ClassWriter(0);
		// 通過visit方法確定類的頭部資訊
        //相當於 public class Custom 編譯版本1.7
		cw.visit(Opcodes.V1_7, Opcodes.ACC_PUBLIC, "com/qty/classloader/Custom", null, "java/lang/Object", null);
		// 生成預設的構造方法
		MethodVisitor mw = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);

		// 生成構造方法的位元組碼指令
        // aload_0 載入0位置的區域性變數,即this
		mw.visitVarInsn(Opcodes.ALOAD, 0);
        // 呼叫初始化函式
		mw.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
		mw.visitInsn(Opcodes.RETURN);
        //maxs編輯的是最大棧深度和最大區域性變數個數
		mw.visitMaxs(1, 1);

		// 生成方法 public void doSomeThing(String value)
		mw = cw.visitMethod(Opcodes.ACC_PUBLIC, "doSomeThing", "(Ljava/lang/String;)V", null, null);

		// 生成方法中的位元組碼指令
        //相當於 System.out.println(value);
		mw.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
		mw.visitVarInsn(Opcodes.ALOAD, 1);
		mw.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V");
		mw.visitInsn(Opcodes.RETURN);
		mw.visitMaxs(2, 2);

		cw.visitEnd(); // 使cw類已經完成
		// 將cw轉換成位元組陣列寫到檔案裡面去
		byte[] data = cw.toByteArray();
        //這裡需要輸出到對應專案的classes的目錄下
		File file = new File("./target/classes/com/qty/classloader/Custom.class");
		FileOutputStream fout = new FileOutputStream(file);
		fout.write(data);
		fout.close();
        //class生成了,試一下能不能正確執行
		Class<?> exampleClass = Class.forName("com.qty.classloader.Custom");
		Method method = exampleClass.getDeclaredMethod("doSomeThing", String.class);
		Object o = exampleClass.newInstance();
		method.invoke(o, "this is a test!");
	}
}

以上程式碼在我本地跑通沒有問題,且能夠正確輸出this is a test!.

使用命令看一下反編譯效果:

  Last modified 2020-10-7; size 320 bytes
  MD5 checksum eed71ac57da1174f4adf0910a9fa338a
public class com.qty.classloader.Custom
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC
Constant pool:
   #1 = Utf8               com/qty/classloader/Custom
   #2 = Class              #1             // com/qty/classloader/Custom
   #3 = Utf8               java/lang/Object
   #4 = Class              #3             // java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = NameAndType        #5:#6          // "<init>":()V
   #8 = Methodref          #4.#7          // java/lang/Object."<init>":()V
   #9 = Utf8               doSomeThing
  #10 = Utf8               (Ljava/lang/String;)V
  #11 = Utf8               java/lang/System
  #12 = Class              #11            // java/lang/System
  #13 = Utf8               out
  #14 = Utf8               Ljava/io/PrintStream;
  #15 = NameAndType        #13:#14        // out:Ljava/io/PrintStream;
  #16 = Fieldref           #12.#15        // java/lang/System.out:Ljava/io/PrintStream;
  #17 = Utf8               java/io/PrintStream
  #18 = Class              #17            // java/io/PrintStream
  #19 = Utf8               println
  #20 = NameAndType        #19:#10        // println:(Ljava/lang/String;)V
  #21 = Methodref          #18.#20        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #22 = Utf8               Code
{
  public com.qty.classloader.Custom();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #8                  // Method java/lang/Object."<init>":()V
         4: return

  public void doSomeThing(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: getstatic     #16                 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: aload_1
         4: invokevirtual #21                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         7: return
}

ASM除了可以動態生成新的Class檔案,還可以修改原有Class檔案的功能或者在原Class檔案新增方法欄位等,這裡不再舉例子,有興趣的可以自己研究一下。不過大家已經發現,使用ASM動態修改Class檔案,難度還是有的,需要使用者對JVM指令、Class格式相當熟悉,

除了ASM,還有其他第三方工具也提供了對位元組碼的動態修改,包括CGLib,Javassisit,AspectJ等,而這些框架相比於ASM,則是將JVM指令級別的編碼封裝起來,讓使用者直接使用Java程式碼編輯,使用更加方便。

想要詳細瞭解ASM,可以參考ASM官方文件.

IDEA外掛 ASM byteCode Outline 可以直接看到程式碼的JVM操作指令.

Javassisit

Javassisit官方文件

再舉個例子

public class SsisitDemo {
	public static void main(String[] args) throws Exception {
		ClassPool pool = ClassPool.getDefault();
		CtClass ct = pool.makeClass("com.qty.GenerateClass");// 建立類
		ct.setInterfaces(new CtClass[] { pool.makeInterface("java.lang.Cloneable") });// 讓類實現Cloneable介面
		try {
			CtField f = new CtField(CtClass.intType, "id", ct);// 獲得一個型別為int,名稱為id的欄位
			f.setModifiers(AccessFlag.PUBLIC);// 將欄位設定為public
			ct.addField(f);// 將欄位設定到類上
			// 新增建構函式
			CtConstructor constructor = CtNewConstructor.make("public GeneratedClass(int pId){this.id=pId;}", ct);
			ct.addConstructor(constructor);
			// 新增方法
			CtMethod helloM = CtNewMethod.make("public void hello(String des){ System.out.println(des+this.id);}", ct);
			ct.addMethod(helloM);
			ct.writeFile("./target/classes");// 將生成的.class檔案儲存到磁碟

			// 下面的程式碼為驗證程式碼
			Class<?> clazz = Class.forName("com.qty.GenerateClass");
			Field[] fields = clazz.getFields();
			System.out.println("屬性名稱:" + fields[0].getName() + "  屬性型別:" + fields[0].getType());
			Constructor<?> con = clazz.getConstructor(int.class);
			Method me = clazz.getMethod("hello", String.class);
			me.invoke(con.newInstance(12), "this is a test-- ");
		} catch (CannotCompileException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

輸出如下:

屬性名稱:id  屬性型別:int
this is a test-- 12

使用javap -c檢視:

Compiled from "GenerateClass.java"
public class com.qty.GenerateClass implements java.lang.Cloneable {
  public int id;

  public com.qty.GenerateClass(int);
    Code:
       0: aload_0
       1: invokespecial #15                 // Method java/lang/Object."<init>":()V
       4: aload_0
       5: iload_1
       6: putfield      #17                 // Field id:I
       9: return

  public void hello(java.lang.String);
    Code:
       0: getstatic     #26                 // Field java/lang/System.out:Ljava/io/PrintStream;
       3: new           #28                 // class java/lang/StringBuffer
       6: dup
       7: invokespecial #29                 // Method java/lang/StringBuffer."<init>":()V
      10: aload_1
      11: invokevirtual #33                 // Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
      14: aload_0
      15: getfield      #35                 // Field id:I
      18: invokevirtual #38                 // Method java/lang/StringBuffer.append:(I)Ljava/lang/StringBuffer;
      21: invokevirtual #42                 // Method java/lang/StringBuffer.toString:()Ljava/lang/String;
      24: invokevirtual #47                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      27: return
}

Class載入

上面講到所有的內容都是Demo級別的例子,並沒有從專案使用層面來分析這些技術如何使用。比如,我們修改的位元組碼何時載入到JVM?執行中的專案如果動態修改某個類的實現,怎麼載入?

ClassLoader

ClassLoader雙親委託機制確保了每個Class只能被一個ClassLoader載入,每個ClassLoader關注自己的資源目錄:

  • BootStrapClassLoader -> <JAVA_HOME>/lib或者-Xbootclasspath指定的路徑
  • ExtClassLoader -> <JAVA_HOME>/lib/ext或者-Djava.ext.dir指定的路徑
  • AppClassLoader -> 專案classPath目錄,通常就是classes目錄和moven引用的jar包

上面的例子中,自動生成的Class檔案都是直接放到專案classpath下,可以直接被AppClassLoader獲取到,所以可以直接使用Class.forName獲取到class物件。但之前的例子都是直接生成新的class檔案,如果是修改已經載入好的class檔案會是什麼效果,我們接著看栗子:

package com.qty.first;
public class SsisitObj {
	private String name;
	public void sayMyName() {
		System.out.println("My name is " + name);
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
}

正常設定name之後,呼叫sayMyName會輸出自己的名字。現在要在專案執行中對這個class進行修改,使sayMyName除了列印出自己名字外,還要在列印之前輸出開始結束標記。

package com.qty.first;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

public class ClassDemo {

	public static void main(String[] args) throws Exception {
		SsisitObj obj = new SsisitObj();
		obj.setName("Jack");
		obj.sayMyName();
		addCutPoint();
		obj.sayMyName();
	}
	//對SsisitObj中方法進行修改
	private static void addCutPoint() {
		try {
			ClassPool pool = ClassPool.getDefault();
			pool.insertClassPath("target/classes/com/qty/first");
			CtClass cc = pool.get("com.qty.first.SsisitObj");
            //定位到方法
			CtMethod fMethod = cc.getDeclaredMethod("sayMyName");
            //覆蓋發放內容
			fMethod.setBody("{" + "System.out.println(\"Method start. \");"
					+ "System.out.println(\"My name is \" + name);" + "System.out.println(\"Method end.  \");}");
            //生成class並載入
			cc.toClass();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

上面這個例子一定回報錯,因為Classloader並沒有解除安裝class的方法,所以一旦class被載入到JVM之後,就不可以再次被載入,那是不是有其他方案?

上栗子:

package com.qty.first;

import java.io.File;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

public class ClassDemo {
	private static String url = "./com/qty/first/SsisitObj.class";

	public static void main(String[] args) throws Exception {
		ISaySomething obj = loadFile().newInstance();
		obj.setName("jack");
		obj.sayMyName();
		addCutPoint();
		System.out.println("-----------我是分割線-----------------");
		obj = loadFile().newInstance();
		obj.setName("jack");
		obj.sayMyName();
	}

	@SuppressWarnings("unchecked")
	private static Class<ISaySomething> loadFile() throws Exception {
		MyClassLoader loader = new MyClassLoader();
		File file = new File(url);
		loader.addURLFile(file.toURI().toURL());
		Class<ISaySomething> clazz = (Class<ISaySomething>) loader.createClass("com.qty.first.SsisitObj");
		return clazz;
	}

	private static void addCutPoint() {
		try {
			ClassPool pool = ClassPool.getDefault();
			pool.insertClassPath("target/classes/com/qty/first");
			CtClass cc = pool.get("com.qty.first.SsisitObj");
			CtMethod fMethod = cc.getDeclaredMethod("sayMyName");
			fMethod.setBody("{" + "System.out.println(\"Method start. \");"
					+ "System.out.println(\"My name is \" + name);" + "System.out.println(\"Method end.  \");}");
			cc.writeFile("./");
			url = "./com/qty/first/SsisitObj.class";
		} catch (Exception e) {
			e.printStackTrace();

		}
	}
}

package com.qty.first;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;

public class MyClassLoader extends URLClassLoader {

	public MyClassLoader() {
		super(new URL[] {}, findParentClassLoader());
	}

	/**
	 * 定位基於當前上下文的父類載入器
	 * 
	 * @return 返回可用的父類載入器.
	 */
	private static ClassLoader findParentClassLoader() {
		ClassLoader parent = MyClassLoader.class.getClassLoader();
		if (parent == null) {
			parent = MyClassLoader.class.getClassLoader();
		}
		if (parent == null) {
			parent = ClassLoader.getSystemClassLoader();
		}
		return parent;
	}

	private URLConnection cachedFile = null;

	/**
	 * 將指定的檔案url新增到類載入器的classpath中去,並快取jar connection,方便以後解除安裝jar
	 * 一個可想類載入器的classpath中新增的檔案url
	 * 
	 * @param
	 */
	public void addURLFile(URL file) {
		try {
			// 開啟並快取檔案url連線
			URLConnection uc = file.openConnection();
			uc.setUseCaches(true);
			cachedFile = uc;
		} catch (Exception e) {
			System.err.println("Failed to cache plugin JAR file: " + file.toExternalForm());
		}
		addURL(file);
	}

	public void unloadJarFile(String url) {
		URLConnection fileURLConnection = cachedFile;
		if (fileURLConnection == null) {
			return;
		}
		try {
			System.err.println("Unloading plugin file " + fileURLConnection.getURL().toString());
			fileURLConnection.getInputStream().close();
			cachedFile = null;
		} catch (Exception e) {
			System.err.println("Failed to unload JAR file\n" + e);
		}
	}

	/**
	 * 繞過雙親委派邏輯,直接獲取Class
	 */
	public Class<?> createClass(String name) throws Exception {
		byte[] data;
		data = readClassFile(name);
		return defineClass(name, data, 0, data.length);
	}

	// 獲取要載入 的class檔名
	private String getFileName(String name) {
		int index = name.lastIndexOf('.');
		if (index == -1) {
			return name + ".class";
		} else {
			return name.replace(".", "/")+".class";
		}
	}

	/**
	 * 讀取Class檔案
	 */
	private byte[] readClassFile(String name) throws Exception {
		String fileName = getFileName(name);
		File file = new File(fileName);
		FileInputStream is = new FileInputStream(file);
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		int len = 0;
		while ((len = is.read()) != -1) {
			bos.write(len);
		}
		byte[] data = bos.toByteArray();
		is.close();
		bos.close();
		return data;
	}

}

輸出:

My name is jack
-----------我是分割線-----------------
Method start. 
My name is jack
Method end.  

這個栗子只是示意,也就是說當使用自定義Classloader的時候,是可以通過更換Classloader來實現重新載入Class的需求。

Instrument

在 JDK 1.5 中,Java 引入了java.lang.Instrument包,該包提供了一些工具幫助開發人員在 Java 程式執行時,動態修改系統中的 Class 型別。其中,使用該軟體包的一個關鍵元件就是 Java agent。

相比classloader對未載入到JVM中的class進行修改,使用Instrument可以在執行時對已經載入的class檔案重定義。

最後的栗子:

package com.qty.second;

import java.lang.instrument.ClassDefinition;
import java.lang.instrument.UnmodifiableClassException;

import com.qty.MyAgent;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

public class ClassDemo {
	public static void main(String[] args) throws ClassNotFoundException, UnmodifiableClassException {
		SsisitObj obj = new SsisitObj();
		obj.setName("Tom");
		obj.sayMyName();
		ClassDefinition definition = new ClassDefinition(obj.getClass(), getEditClass());
		MyAgent.getIns().redefineClasses(definition);
		obj = new SsisitObj();
		obj.setName("Jack");
		obj.sayMyName();
	}
	
	private static byte[] getEditClass() {
		try {
			ClassPool pool = ClassPool.getDefault();
			pool.insertClassPath("target/classes/com/qty/second");
			CtClass cc = pool.get("com.qty.second.SsisitObj");
			CtMethod fMethod = cc.getDeclaredMethod("sayMyName");
			fMethod.setBody("{" + "System.out.println(\"Method start. \");"
					+ "System.out.println(\"My name is \" + name);" + "System.out.println(\"Method end.  \");}");
			return cc.toBytecode();
		} catch (Exception e) {
			e.printStackTrace();

		}
		return null;
	}
}

結語

本次分享的重點內容是位元組碼技術的入門介紹。在瞭解位元組碼結構等相關知識之後,通過舉例的方式瞭解一下位元組碼技術相關應用方法,以及如何將位元組碼技術運用到實際專案中。

本次分享就到此為止,謝謝支援。

引申

既然JVM執行時識別的只是.class檔案,而檔案格式我們也瞭解,那是不是隻要我們能夠正確生成.class檔案就可以直接執行,甚至可以不用Java語言?

答案大家肯定都知道了,當然可以。Kotlin,Scala,Groovy,Jython,JRuby…這些都是基於JVM的程式語言。

那如果我們想自己實現一款基於JVM的開發語言,怎麼搞?

  1. 定義語義,靜態,動態?強型別,弱型別?
  2. 定義語法,關鍵字(if,else,break,return…)
  3. 定義程式碼編譯器,如何將自己的程式碼編譯成.class

有興趣的大佬,可以試試

還可以繼續引申,語義語法都定義好了,是不是可以實現編譯器直接編譯成.exe檔案,或者linux下可以執行程式?

待續

  • Class載入詳細過程,如JVM如何將指令生成對應程式碼
  • 位元組碼技術相關框架詳解,ASM,CGLib,Javassisit,AspectJ,JDK Proxy
  • ClassLoader詳解
  • Java Agent

相關文章