位元組碼詳解
文章目錄
位元組碼詳解
前言
萬事開頭難
位元組碼相關內容往深了挖其實東西很多,我就按照自己學習的一個心理歷程去分享一下這塊兒的內容,起個拋磚引玉的作用,很多地方沒有特別深入的研究,有待大家補充。
什麼是位元組碼
Java作為一款“一次編譯,到處執行”的程式語言,跨平臺靠的是JVM實現對不同作業系統API的支援,而一次編譯
指的就是class
位元組碼;即我們編寫好的.java
檔案,通過編譯器編譯成.class
檔案,JVM負責載入解釋位元組碼檔案,並生成系統可識別的程式碼執行(具體解析本次不做深入研究).
Class檔案
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
繼續往後看八個位元組,分別是0000
、0034
,我本地環境使用的是JDK1.8
class檔案中看到的是16進位制,把0034
轉為10進位制的數字就是52。我用JDK1.7編譯之後,如下:
主版本號對應的兩個位元組,根據我們本地編譯版本不同也會不同。
下面是JDK版本與版本號對應關係:
jdk版本 | major.minor version |
---|---|
1.1 | 45 |
1.2 | 46 |
1.3 | 47 |
1.4 | 48 |
5 | 49 |
6 | 50 |
7 | 51 |
8 | 52 |
類的訪問標識
訪問標識型別表:
Flag Name | Value | Interpretation |
---|---|---|
ACC_PUBLIC | 0x0001 | Declared public ; may be accessed from outside its package. |
ACC_FINAL | 0x0010 | Declared final ; no subclasses allowed. |
ACC_SUPER | 0x0020 | Treat superclass methods specially when invoked by the invokespecial instruction. |
ACC_INTERFACE | 0x0200 | Is an interface, not a class. |
ACC_ABSTRACT | 0x0400 | Declared abstract ; must not be instantiated. |
ACC_SYNTHETIC | 0x1000 | Declared synthetic; not present in the source code.這個關鍵字不是原始碼生成,而是編譯器生成的 |
ACC_ANNOTATION | 0x2000 | Declared as an annotation type. |
ACC_ENUM | 0x4000 | Declared 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 Type | Value |
---|---|
CONSTANT_Class | 7 |
CONSTANT_Fieldref | 9 |
CONSTANT_Methodref | 10 |
CONSTANT_InterfaceMethodref | 11 |
CONSTANT_String | 8 |
CONSTANT_Integer | 3 |
CONSTANT_Float | 4 |
CONSTANT_Long | 5 |
CONSTANT_Double | 6 |
CONSTANT_NameAndType | 12 |
CONSTANT_Utf8 | 1 |
CONSTANT_MethodHandle | 15 |
CONSTANT_MethodType | 16 |
CONSTANT_InvokeDynamic | 18 |
不同常量對應後續位元組數不同,如CONSTANT_Class
,CONSTANT_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
,01
即CONSTANT_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 Name | Value | Interpretation |
---|---|---|
ACC_PUBLIC | 0x0001 | Declared public ; may be accessed from outside its package. |
ACC_PRIVATE | 0x0002 | Declared private ; usable only within the defining class. |
ACC_PROTECTED | 0x0004 | Declared protected ; may be accessed within subclasses. |
ACC_STATIC | 0x0008 | Declared static . |
ACC_FINAL | 0x0010 | Declared final ; never directly assigned to after object construction (JLS §17.5). |
ACC_VOLATILE | 0x0040 | Declared volatile ; cannot be cached. |
ACC_TRANSIENT | 0x0080 | Declared transient ; not written or read by a persistent object manager. |
ACC_SYNTHETIC | 0x1000 | Declared synthetic; not present in the source code. |
ACC_ENUM | 0x4000 | Declared 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 Name | Value | Interpretation |
---|---|---|
ACC_PUBLIC | 0x0001 | Declared public ; may be accessed from outside its package. |
ACC_PRIVATE | 0x0002 | Declared private ; accessible only within the defining class. |
ACC_PROTECTED | 0x0004 | Declared protected ; may be accessed within subclasses. |
ACC_STATIC | 0x0008 | Declared static . |
ACC_FINAL | 0x0010 | Declared final ; must not be overridden (§5.4.5). |
ACC_SYNCHRONIZED | 0x0020 | Declared synchronized ; invocation is wrapped by a monitor use. |
ACC_BRIDGE | 0x0040 | A bridge method, generated by the compiler. |
ACC_VARARGS | 0x0080 | Declared with variable number of arguments. |
ACC_NATIVE | 0x0100 | Declared native ; implemented in a language other than Java. |
ACC_ABSTRACT | 0x0400 | Declared abstract ; no implementation is provided. |
ACC_STRICT | 0x0800 | Declared strictfp ; floating-point mode is FP-strict. |
ACC_SYNTHETIC | 0x1000 | Declared synthetic; not present in the source code. |
ACC_BRIDGE
也是由編譯器生成的,比如泛型的子類重寫父類方法, 就會有一個在子類生成一個新的方法用ACC_BRIDGE
標識
ACC_VARARGS
可變引數的方法會出現這個標記
ACC_STRICT
strictfp標識的方法中,所有float和double表示式都嚴格遵守FP-strict的限制,符合IEEE-754規範.
Descriptors-描述
方法和欄位都有自己的描述資訊,方法的描述包括引數、返回值的型別,欄位描述為欄位的型別,下面是型別表:
FieldType term | Type | Interpretation |
---|---|---|
B | byte | signed byte |
C | char | Unicode character code point in the Basic Multilingual Plane, encoded with UTF-16 |
D | double | double-precision floating-point value |
F | float | single-precision floating-point value |
I | int | integer |
J | long | long integer |
L ClassName ; | reference | an instance of class ClassName |
S | short | signed short |
Z | boolean | true or false |
[ | reference | one 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
中包含code
、exception
、attribute_info
等資訊,這裡主要說下code
中的內容。
code
陣列中的內容就是方法中編譯後的程式碼:
0: aload_0
1: invokespecial #10 // Method java/lang/Object."<init>":()V
4: return
這個就是我們上面那個類的無參建構函式編譯後的效果,那這裡面的aload_0
、invokespecial
、return
學過JVM相關知識的話,大家已經很熟悉了.
aload_0
就是變數0進棧invokespecial
呼叫例項的初始化方法,即構造方法return
即方法結束,返回值為void
那這些aload_0
、invokespecial
、return
相關的指令是如何儲存在code
陣列中的,或者說是以什麼形式存在的?
其實JVM有這樣一個指令陣列,code
陣列中的記錄的就是指令陣列的有效下標,下面是部分指令:
Code | Forms | describe |
---|---|---|
return | 0xB1 | 當前方法返回void |
areturn | 0xB0 | 從方法中返回一個物件的引用 |
ireturn | 0xAC | 當前方法返回int |
iload_0 | 0x1A | 第一個int型區域性變數進棧 |
lload_0 | 0x1E | 第一個long型區域性變數進棧 |
istore_0 | 0x3B | 將棧頂int型數值存入第一個區域性變數 |
lstore_0 | 0x3F | 將棧頂long型數值存入第一個區域性變數 |
getstatic | 0xB2 | 獲取指定類的靜態域,並將其值壓入棧頂 |
putstatic | 0xB3 | 為指定的類的靜態域賦值 |
invokespecial | 0xB7 | 呼叫超類構造方法、例項初始化方法、私有方法 |
invokevirtual | 0xB6 | 呼叫例項方法 |
iadd | 0x60 | 棧頂兩int型數值相加,並且結果進棧 |
iconst_0 | 0x03 | int型常量值0進棧 |
ldc | 0x12 | 將int、float或String型常量值從常量池中推送至棧頂 |
詳細指令列表可以檢視官方文件。
關於attribute_info
還有其他型別,有興趣的可以檢視Attribute,型別及其出現位置如下:
Attribute | Location |
---|---|
SourceFile | ClassFile |
InnerClasses | ClassFile |
EnclosingMethod | ClassFile |
SourceDebugExtension | ClassFile |
BootstrapMethods | ClassFile |
ConstantValue | field_info |
Code | method_info |
Exceptions | method_info |
RuntimeVisibleParameterAnnotations , RuntimeInvisibleParameterAnnotations | method_info |
AnnotationDefault | method_info |
MethodParameters | method_info |
Synthetic | ClassFile , field_info , method_info |
Deprecated | ClassFile , field_info , method_info |
Signature | ClassFile , field_info , method_info |
RuntimeVisibleAnnotations , RuntimeInvisibleAnnotations | ClassFile , field_info , method_info |
LineNumberTable | Code |
LocalVariableTable | Code |
LocalVariableTypeTable | Code |
StackMapTable | Code |
RuntimeVisibleTypeAnnotations , RuntimeInvisibleTypeAnnotations | ClassFile , 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
再舉個例子
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的開發語言,怎麼搞?
- 定義語義,
靜態,動態?
,強型別,弱型別?
… - 定義語法,關鍵字(if,else,break,return…)
- 定義程式碼編譯器,如何將自己的程式碼編譯成
.class
有興趣的大佬,可以試試
還可以繼續引申,語義語法都定義好了,是不是可以實現編譯器直接編譯成.exe
檔案,或者linux
下可以執行程式?
待續
- Class載入詳細過程,如
JVM如何將指令生成對應程式碼
- 位元組碼技術相關框架詳解,
ASM
,CGLib
,Javassisit
,AspectJ
,JDK Proxy
… - ClassLoader詳解
- Java Agent
相關文章
- 詳解Android Gradle生成位元組碼流程AndroidGradle
- 位元組碼檔案結構詳解
- 什麼是位元組碼?python位元組碼詳細介紹!Python
- 位元組碼
- Java 位元組碼Java
- 位元組碼指令
- 位元組碼基礎
- 位元組跳動視訊編解碼面經
- Android 位元組碼插樁Android
- Python 位元組碼介紹Python
- JAVA動態位元組碼Java
- 【Java】JVM位元組碼分析JavaJVM
- 位元組碼檔案解剖
- 位元組跳動極高可用 KV 儲存系統詳解
- 輕鬆看懂Java位元組碼Java
- 位元組碼底層分析String
- Java位元組碼指令表Java
- Java類轉位元組碼工具Java
- pyc位元組碼文字轉python程式碼Python
- 淺談位元組碼增強技術系列1-位元組碼增強概覽
- 例項分析理解Java位元組碼Java
- python反編譯之位元組碼Python編譯
- Java位元組碼增強技術Java
- 開啟java語言世界通往位元組碼世界的大門——ASM位元組碼操作類庫JavaASM
- ASM位元組碼操作類庫:開啟java語言世界通往位元組碼世界的大門ASMJava
- ASM位元組碼操作類庫(開啟java語言世界通往位元組碼世界的大門)ASMJava
- 編碼、摘要和加密(一)——位元組編碼加密
- 機器碼和位元組碼分別介紹機器碼
- Java 動態性(4) – 位元組碼操作Java
- 從 Java 位元組碼到 ASM 實踐JavaASM
- Class檔案結構&位元組碼指令
- 位元組碼指令分析 ++i 和 i++
- 學習 Java 之 位元組碼驗證Java
- Dalvik 和 Java 位元組碼的比較Java
- 【JVM原始碼解析】模板直譯器解釋執行Java位元組碼指令(上)JVM原始碼Java
- 【密碼學系列】|| 分組密碼的工作模式詳解密碼學模式
- Java程式碼如何檢視位元組碼及彙編碼Java
- 位元組流