Class文件解析实战
java跨平台的实现是基于JVM虚拟机的,编写的java源码,编译后会生成一种.class
文件,称为字节码文件。java虚拟机就是负责将字节码文件翻译成特定平台下的机器码然后运行。为了保证Class文件在多个平台的通用性,java官方制定了严格的Class文件格式。了解Class文件结构,有利于我们反编译 .class
文件或在程序编译期间修改字节码做代码注入。
Class 文件结构概览
首先先创建一个 java 类:
public class HelloWorld {
private static int num = 0;
public String name = "HelloWorld";
public static void main(String[] args) {
String[] strs = {"bigkai1", "bigkai2"};
for (int i = 0; i < 10; i++) {
num++;
if(i == 5) continue;
System.out.println("HelloWorld!");
}
}
}
然后进去当前类目录下执行 javac
命令生成类文件:
$ javac HelloWorld.java
我们便可以看到在java文件下生成了一个 HelloWorld.class
文件,使用类文件解析器 classpy
打开该文件,可以看到文件的整体结构:
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];
}
我将 Class 文件的结构做了一个简单的图示:
在JVM中,Class文件使用的是类C语言进行描述的,统一使用无符号整数作为基本数据类型:单字节 u1
、2字节 u2
、4字节 u4
、8字节 u8
。
下面就对文件各部分一一进行解析。
魔数
魔数(Magic Number)是Class文件的标识符,它是一个4字节的整数,只有当前四个字节为 0xCAFEBABE
(可以记忆为咖啡宝贝
的英译)时,虚拟机才会认为这是一个Class文件。这种开头固定标识符的做法在很多地方用到过,比如 zip的压缩文件
。
查看我们的 Class 文件,是否有这个标识符:
当我人为地将 CA FE BA BE
修改为 CA FE BA BA
时,让虚拟机对类文件加载 ,虚拟机在校验文件时会抛出以下错误:
版本号
在魔数的后面,就是Class的版本号,它一共有两种:小版本号(minor_version
)和大版本号(major_version
)。它们组合起来表示当前Class文件是由哪个版本的JDK编译产生的。以下是截取自java官网的版本图:
对照此图,我们可以通过版本号查看对应的 jdk 版本:
在我的Class文件中,版本号为 0x0037
,换算为十进制为 55
,即对应jdk11。
对于 major_version 为 56 或以上的类文件,minor_version 必须为 0 或 65535。
对于 major_version 在 45 到 55 之间的类文件,minor_version 可以是任何值。
当我人为的将大版本号修改为 0x0039
,即对应jdk14版本,然后加载类文件,由于我的jdk版本是11,虚拟机只能向下兼容,所以会报错:
常量池
常量池是 Class 文件中内容最重要的组成之一,常量池大体分为静态常量池和运行时常量池,静态常量池存放在 Class 文件中,运行时常量池指的是将 Class 文件加载进内容后,保存了常量池的方法区。这里我们解析的是静态常量池。
静态常量池的每个表项的格式为:
cp_info {
u1 tag;
u1 info[];
}
tag 表示指示条目所表示的常量类型。共有 17 种常数:
我对生成的 Class 文件常量池第一项进行分析:
可以看出它的 tag
是 0A
,根据上表得出它是一个 CONSTANT_Methodref
,该结构为:
CONSTANT_Methodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
然后根据它后面的 0x000C
,得出 class_index
在常量池中第12项
class_index
的值为常量池的索引,表示具有字段或方法作为成员的类或接口类型。
- 在 CONSTANT_Fieldref_info 结构中,class_index 项可以是类类型或接口类型。
- 在 CONSTANT_Methodref_info 结构中,class_index 项必须是类类型,而不是接口类型。
- 在 CONSTANT_InterfaceMethodref_info 结构中,class_index 项必须是接口类型,而不是类类型。
然后又往后读取两个字节 0x001C
,它表示常量池中字段或方法的名称和描述符的索引值。
我们从 class_index
查看它所在的类:
可以看到它的 tag
是7,指示的是 CONOSTANT_CLASS
,结构为:
CONSTANT_Class_info {
u1 tag;
u2 name_index;
}
它的 name_index
指示的是类的名字,我们接着看 0x0028
对应的项:
可以看出它是 CONSTANT_Utf8
,结构为:
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}
它的长度为 0x0010
=16
,所以往后一直读16个字节,得出它的名字为:java/lang/Object
。
接着我们再看它的 name_and_type_index
指向,它对应表项28:
tag
=12表示这是 CONSTANT_NameAndType
类型,结构为:
CONSTANT_NameAndType_info {
u1 tag;
u2 name_index;
u2 descriptor_index;
}
name_index
已经知道是什么意思了,我们直接来看 descriptor_index
,它的作用是表示一个有效的字段描述符或方法描述符:
不同字母对应的字段描述符为:
对于方法描述符,它有参数描述符和返回描述符,对于返回描述符,只是增加了一个 V
,它对应返回值为 void
。
类访问标记
在常量池后面,就是类访问标记,它是一个 u2
类型的字节,表示该类的访问信息,映射的访问修饰符如下:
每一种类型的表示都是通过设置访问标记中的特定位来表示的从图中可以看出我的Class文件为 0x0021
:
那么可以知道类的访问修饰符为 ACC_PUBLIC|ACC_SUPER
(0x0021
=0x0020
+x0001
)。
类关系信息
在访问标记的后面,就是该类的类别 this_class
、父类类别(所有的类最上层父类都是 Object
)super_class
以及实现的接口数量 interface_count
、接口类别 interface_index
。
查看我的 Class 文件:
该类的类别是 11,父类是 12,接口数量为 0,然后根据索引在常量池中查找相关信息:
看到它们都是 CONSTANT_Class
类型,根据后两个字节查看它们的名字:
它们都是 CONSTANT_Utf8
类型,按照对应的结构读取之后分别是 HelloWorld
,java/lang/Object
。则可知该类名字为 HelloWorld
,它的父类是 java.lang.Object
,它并没有实现接口。
字段信息
在类信息后面就是字段信息,分别由字段数量(fields_count
)和字段表(fields_info
)组成,字段数量是一个 u2
类型,主要看下字段信息表的结构:
field_info {
u2 access_flags; // 字段访问标记
u2 name_index; // 字段名
u2 descriptor_index; // 描述符
u2 attributes_count; // 字段属性数量
attribute_info attributes[attributes_count]; // 字段属性表
}
字段访问标记:类似类的访问标记,计算方式也和类的差不多。
字段名:指向常量池索引。
描述符:用于描述字段类型,指向常量池索引,字段的类型有:
字段属性数量:记录字段的属性个数,属性是字段的额外信息,比如初始化值、注释等。
字段属性:存放属性的具体内容。
以我生成的 Class 文件为例:
一共有两个字段,第一个字段字节码表示为 00 0A 00 0D 0E 00 00
,访问标记为 ACC_PRIVATE | ACC_STATIC
(00 0A
= 00 02
+ 00 08
),字段名为 00 0D
,描述符是 00 0E
,属性数量是 00 00
。
关于属性表的内容放到方法中描述。
方法
Class文件的方法由方法数量和方法内容两部分组成,方法数量是一个 u2
类型的数据,后面就是方法信息,方法信息的结构为:
method_info {
u2 access_flags; // 访问标记
u2 name_index; // 方法名
u2 descriptor_index; // 描述符
u2 attributes_count; // 属性数量
attribute_info attributes[attributes_count]; // 属性内容
}
方法的访问标记就比字段的访问标记多得多了:
name_index
是方法名的索引,descriptor_index
表示方法的签名(参数、返回值等),方法描述符在常量池中的表现为 (参数1参数2)返回值
。
主要关注 attribute_info
,它的结构为:
attribute_info {
u2 attribute_name_index; // 属性名
u4 attribute_length; // 属性长度
u1 info[attribute_length]; // 属性
}
属性有多种:
简单看一下常用的属性:
对于下面属性,有些不是运行时必须的属性,可以在Javac中分别使用 -g : none
或 -g :vars
选项来取消或要求生成这项信息。
Code
Code 属性存放方法的字节码等信息,是方法的执行主体,Code 属性的结构体为:
Code_attribute {
u2 attribute_name_index; // 属性名——固定为Code
u4 attribute_length; // 属性长度(不包括前面6个字节)
u2 max_stack; // 操作数栈最大深度
u2 max_locals; // 局部变量最大个数
u4 code_length; // 方法字节码长度
u1 code[code_length]; // 字节码内容
u2 exception_table_length; // 异常处理表长度
/*
从方法字节码的start_pc偏移量开始到end_pc偏移量为止的代码中,如果遇到了catch_type所指定的异常,那么代码就跳转到handler_pc位置执行。
*/
{ 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]; // 属性内容
}
ConstantValue
ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。只有被 static
关键字修饰的变量(类变量)才可以使用这项属性。它的结构为:
ConstantValue_attribute {
u2 attribute_name_index; // 固定ConstantValue
u4 attribute_length; // 固定2
u2 constantvalue_index; // 常量池的有效索引
}
如果在
field_info
结构的access_flags
项中设置了ACC_STATIC
标志,那么field_info
结构所表示的字段将被赋给它的ConstantValue
属性所表示的值,作为声明该字段的类或接口的初始化的一部分,这以操作发生在调用类或接口的类或接口初始化方法之前。
Signature
Signature 是 JDK1.5 时发布的,出现于类、属性表和方法表结构的属性表中。任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则 Signature 属性会为他记录泛型签名信息。它的结构为:
Signature_attribute {
u2 attribute_name_index; // 固定Signature
u4 attribute_length; // 固定2
/*
如果该签名属性是类文件结构的属性,则该索引处的常量池项必须是表示类签名的常量信息结构);
如果该签名属性是方法信息结构的属性,则必须是方法签名;否则,必须是字段签名。
*/
u2 signature_index; // 常量池有效索引。
}
之所以要专门使用这样一个属性去记录泛型类型,是因为 Java 语言的泛型采用的是擦除法实现的伪泛型,在字节码(Code 属性)中,泛型信息编译(类型变量、参数化类型)之后都统统被擦除掉。使用擦除法的好处是实现简单(主要修改
Javac
编译器,虚拟机内部只做了很少的改动)、非常容易实现Backport
,运行期也能够节省一些类型所占的内存空间。但坏处是运行期就无法像 C# 等有真泛型支持的语言那样,将泛型类型与用户定义的普通类型同等对待,例如运行期做反射时无法获得到泛型信息。Signature
属性就是为了弥补这个缺陷而增设的,现在 java 的反射 API 能够获取泛型类型,最终的数据来源也就是这个属性。
LineNumberTable
LineNumberTable
用于记录字节码偏移量和行号的对应关系,不是运行时必须的属性,但默认生成到Class文件之中,如果选择不生成 LineNumberTable
属性,对程序运行产生的最主要的影响就是当抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候,也无法按照源码行来设置断点。LineNumberTable
属性的结构体为 :
LineNumberTable_attribute {
u2 attribute_name_index; // 固定为LineNumberTable
u4 attribute_length; // 属性长度
u2 line_number_table_length; // 表项长度
{ u2 start_pc; // 字节码偏移量
u2 line_number; // 行号
} line_number_table[line_number_table_length]; // 表项内容
}
LocalVariableTable
LocalVariableTable
属性为局部变量表,不是运行时必须的属性,但默认会生成到Class文件之中如果没有生成这项属性,当前其他人引用这个方法时,所有的参数名称都将会丢失,IDE将会使用如 arg0
、arg1
之类的占位符代替原有的参数名。它的结构体如下:
LocalVariableTable_attribute {
u2 attribute_name_index; // 固定LocalVariableTable
u4 attribute_length; // 表项长度
u2 local_variable_table_length;
{ u2 start_pc; // 字节码偏移量
u2 length; // 长度
u2 name_index; // 局部变量名
u2 descriptor_index; // 局部变量描述符
u2 index; // 局部变量在当前栈帧的局部变量表中的槽位
} local_variable_table[local_variable_table_length]; // 表项内容
}
StackMapTable
它是JDK1.6引入的一个属性,位于 Code
属性的属性表,该接口存在若干个栈映射帧的数据,这个属性不是运行时必需的,仅做Class的类型校验。它会在虚拟机类加载的字节码验证阶段被新类型检查验证器(Type Checker)使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。它的结构如下:
StackMapTable_attribute {
u2 attribute_name_index; // 固定StackMapTable
u4 attribute_length; // 表项长度
u2 number_of_entries; // 栈映射帧属性
stack_map_frame entries[number_of_entries]; // 栈映射帧具体内容
}
stack_map_frame
的结构为:
union stack_map_frame {
/*
same_frame {
u1 frame_type = SAME; // 0-63
}
*/
same_frame; // 表示当前代码所在位置和上一个比较位置的局部变量表是否相同,并且操作数栈为空
/*
same_locals_1_stack_item_frame {
u1 frame_type = SAME_LOCALS_1_STACK_ITEM; // 64-127
verification_type_info stack[1];
}
*/
same_locals_1_stack_item_frame; // 表示当前帧和上一帧有相同的局部变量,并且操作数栈中变量的数量为1
/*
same_locals_1_stack_item_frame_extended {
u1 frame_type = SAME_LOCALS_1_STACK_ITEM_EXTENDED; // 247-
u2 offset_delta;
verification_type_info stack[1];
}
*/
same_locals_1_stack_item_frame_extended; // 表示当前帧和上一帧有相同的局部变量,操作数栈中变量的数量为1,并且offset_delta超过same_locals_1_stack_item_frame
/*
chop_frame {
u1 frame_type = CHOP; // 248-250
u2 offset_delta;
}
*/
chop_frame; // 表示操作数栈为空,当前局部变量表比前一帧少K(K=2510frrame_type)个局部变量
/*
same_frame_extended {
u1 frame_type = SAME_FRAME_EXTENDED; // 251-
u2 offset_delta;
}
*/
same_frame_extended; // 表示当前代码所在位置和上一个比较位置的局部变量表是否相同,并且操作数栈为空,支持的offset_delta更大
/*
append_frame {
u1 frame_type = APPEND; // 252-254
u2 offset_delta;
verification_type_info locals[frame_type - 251];
}
*/
append_frame; // 表示当前帧比上一帧多了K(K=frame_type-251)个局部变量,且操作数栈为空
/*
full_frame {
u1 frame_type = FULL_FRAME; // 255
u2 offset_delta;
u2 number_of_locals; // 局部变量表的数量
verification_type_info locals[number_of_locals]; // 局部变量表的数据类型
u2 number_of_stack_items; // 操作数栈的数量
verification_type_info stack[number_of_stack_items]; // 操作数栈的类型
}
*/
full_frame; // 完整记录了局部变量表和操作数栈
}
Exceptions
除了Code属性 ,每个方法可以有一个 Exceptions
属性,用于保存该方法可能抛出的异常。它结构如下:
Exceptions_attribute {
u2 attribute_name_index; // 固定为Exceptions
u4 attribute_length; // 属性长度
u2 number_of_exceptions; // 表项数量,可能抛出的异常数
u2 exception_index_table[number_of_exceptions]; // 存储了所有异常,每一项为执行常量池的一个索引
}
注意:方法的 Exceptions 表示一个方法可能抛出的异常,通常是由
throws
关键字指定的,而Code
内的异常表是异常处理机制,由try-catch
语句生成的。
方法字节码分析
对我生成的 Class 文件方法部分做一个简单的分析:
对于 main
方法,可以看到它的访问标识为 00 09
(ACC_STATIC | ACC__PUBLIC),name_index对应常量池的 00 15
,描述符对应常量池的 00 16
,属性个数为 00 01
,然后查看它的属性表:
可以根据前面的内容进行一一对照,最大操作栈数量为4,最大局部变量数量为54,方法内长度为54,code
里面存放的是让虚拟机执行的指令,下面我们主要对它的 code
内容进行分析,在此之前,要先了解虚拟机中的一些指令。
指令
JVM 的指令集有很多,大体可以分为:
- const 系列:负责把简单的数值类型送到栈顶。比如对应 int 型才该方式只能把
-1,0,1,2,3,4,5
推送到栈顶,对于 int 型,其他的数值使用 push 系列命令。 - push 系列:该系列命令负责把一个整形数字(长度比较小)送到到栈顶。它需要一个参数,用于指定要送到栈顶的数字,对于超出范围的数据将使用 ldc 命令。
- ldc 系列:该系列命令负责把数值常量或 String 常量值从常量池中推送至栈顶。该命令后面需要给一个表示常量在常量池中位置 (编号) 的参数。
对于const系列命令和push系列命令操作范围之外的数值类型常量,以及所有不是通过new创建的String,都放在常量池中。
- load 系列:
- loadA 系列:负责把本地变量的送到栈顶。这里的本地变量不仅可以是数值类型,还可以是引用类型。
- loadB 系列:负责把数组的某项送到栈顶。该命令根据栈里内容来确定对哪个数组的哪项进行操作。
- store 系列:
- storeA 系列:负责把栈顶的值存入本地变量。这里的本地变量不仅可以是数值类型,还可以是引用类型。
- storeB 系列:负责把栈顶项的值存到数组里。该命令根据栈里内容来确定对哪个数组的哪项进行操作。
- pop 系列:将栈顶元素弹出(或将栈顶元素赋值并压入)。
- 类型转换系列:该指令专门用于类型转换;这类指令的助记符使用 x2y 的形式给出。其中 x 可能是
i,f,l,d,y
可能是i,f,l,d,c,s,b
。 - 运算系列:为虚拟机提供基本的加减乘除运算功能。
- 数组系列:对于对象的操作指令,可进一步细分为创建指令,字段访问指令,类型检查指令,数组操作指令。
- 控制系列:代表条件控制。大体上分为比较指令,条件跳转指令,比较条件跳转指令,多条件分支跳转,无条件跳转指令等。
- 函数系列:包括函数调用指令、函数返回指令。
- 同步控制系列:Java 虚拟机提供了 monitorenter,monitorexit 来完成临界区的进入和离开。达到多线程的同步。
具体的指令集命令可以查看该博客:CSDN JVM 指令集整理。
指令字节码分析
了解了字节码相关指令后,来对生成的 Class 文件进行一次实战吧:
实际的代码为:
private static int num = 0;
public static void main(String[] args) {
String[] strs = {"bigkai1", "bigkai2"};
for (int i = 0; i < 10; i++) {
num++;
if(i == 5) continue;
System.out.println("HelloWorld!");
}
}
首先用 iconst_2
存入一个 int
类型的2到栈顶,接着 anewarray
创建一个数组的引用并推送到栈顶(栈顶元素出栈作为数组长度),使用 dup
复制栈顶数值并将复制值压入栈顶,接着 iconst_0
将 int
类型的0入栈,使用 ldc
将 String
型常量值从常量池中推送至栈顶,此处指向的是常量池中的 05
——bigkai1
,调用 aastore
将栈顶引用型数值存入指定数组的指定索引位置,它是根据栈顶的引用数值、数组下标、数组引用出栈,将数值存入对应的数组元素。此时就将第一个元素 bigkai1
存入字符串数组 strs
。
接着调用 dup
将栈顶元素复制并再次压入栈顶,然后 iconst_1
压入 int
类型的1,ldc
从常量池中取出 bigkai2
,接着调用 aastore
弹出栈的两个值,给数组的第二个元素赋值为 bigkai2
,此时完成了给字符串数组的所有赋值。
接着进入 for
方法,iconst_0
压入 int
类型的0,然后 istore_2
将栈顶 int
型数值存入第2个本地变量,iload_2
将第2个 int
型本地变量推送至栈顶,bipush
将单字节的常量值(-128~127
)推送至栈顶,if_icmpge
比较栈顶两int型数值大小,当结果大于等于0时跳转到第53个指令(第53个执行是 return
,即结束函数,返回返回值),接着调用 getstatic
获取指定类的静态域(从常量池中获取 num
)并将其值压入栈顶,iconst_1
压入 int
类型的1。iadd
将栈顶两int型数值相加并将结果压入栈顶,然后 putstatic
为 num
赋值,此时是将 num++
执行完毕。
iload_2
将第2个 int
型本地变量推送至栈顶(将 i=0
推送),然后 iconst_5
压入5,使用 if_icmpne
比较栈顶两int型数值大小,当结果不等于0时跳转到第39条指令,否则调用 goto
跳转到第47条指令。第47条指令是 iinc
,是将指定的 int
型变量增加指定值,需要两个变量,分别表示 index
,const
,index
指第 index
个 int
型本地变量,const
表示增加的值,然后又 goto
到第17条指令。此处是实现 if(i == 5) continue
。
如果第33条指令 if_icmpne
不等于0,就跳转到第39条指令,第39条指令是 getstatic
,它获取的是常量池中的 java/lang/System.out
,然后执行 ldc
,将 HelloWorld!
压入栈顶,调用 invokevirtual
指令,它的作用是调用实例方法,根据对象的实际类型进行派发,支持多态。此处是实现 System.out.println("HelloWorld!")
。
Class 文件属性
Class 文件也自带一些属性,由属性长度和属性内容组成,主要的属性有:
SourceFile
SourceFile
属性用于描述当前这个Class文件是由哪个源代码文件编译出来的。
SourceFile_attribute {
u2 attribute_name_index; // 固定SourceFile
u4 attribute_length; // 属性长度,固定为2
u2 sourcefile_index; // 源代码文件名,指向常量池索引
}
BootstrapMethods
BootstrapMethods
属性用于支持 invokeDynamic
指令,它是描述和保存引导方法。
invokeDynamic 是 JDK1.7 支持动态类型语言开发的指令,所谓动态类型语言就是它的类型检查的主体过程是在运行期而不是编译期,典型的代表语言就是
Python
。引导方法可以简单地理解为一个查找方法的方法。
BootstrapMethods_attribute {
u2 attribute_name_index; // 固定BootstrapMethods
u4 attribute_length; // 属性总长度(不包含前6个字节)
u2 num_bootstrap_methods; // 这个类中抱哈的引导方法的个数
{ u2 bootstrap_method_ref; // 指明函数
u2 num_bootstrap_arguments; // 指明引导方法的参数个数
u2 bootstrap_arguments[num_bootstrap_arguments]; // 引导方法的参数
} bootstrap_methods[num_bootstrap_methods];
}
InnerClasses
它用来描述外部类和内部类之间的关系:
InnerClasses_attribute {
u2 attribute_name_index; // 固定InnerClasses
u4 attribute_length; // 属性长度
u2 number_of_classes; // 内部类格式
{ u2 inner_class_info_index; // 内部类类型
u2 outer_class_info_index; // 外部类类型
u2 inner_name_index; // 内部类名称
u2 inner_class_access_flags; // 内部类访问标识符
} classes[number_of_classes]; // 内部类内容
}
内部类的访问标识符支持以下:
Deprecated
Deprecated
可用于类、方法、字段等结构中,表示该类、方法、字段将在未来版本中被弃用。它的结构如下:
Deprecated_attribute {
u2 attribute_name_index; // 固定Deprecated
u4 attribute_length; // 固定为0
}
当一个类、方法、字段被标记为
Deprecated
时,就会产生这个属性。
总结
只要遵循 Class 文件的规范,通过 Class 文件,各种语言都可以由源代码被编译成 Class 文件,并最终得以在虚拟机上执行。
一、OverridingClassLoader 中的使用
OverridingClassLoader 是 Spring 自定义的类加载器,默认会先自己加载 (excludedPackages 或 excludedClasses 例外),只有加载不到才会委托给双亲加载,这就破坏了 JDK 的双亲委派模式。
@Test
public void testOverridingClassLoader() throws Exception {
ClassLoader appClassLoader = Thread.currentThread().getContextClassLoader();
// 添加到 excludedPackages 或 excludedClasses 的类就不会被代理的 ClassLoader 加载
// 而会使用 JDK 默认的双亲委派机制
// 因此 TestBean 不会被 OverridingClassLoader 重新加载,而 ITestBean 会重新加载
OverridingClassLoader overridingClassLoader = new OverridingClassLoader(appClassLoader);
overridingClassLoader.excludeClass(TestBean.class.getName());
Class<?> excludedClazz1 = appClassLoader.loadClass(TestBean.class.getName());
Class<?> excludedClazz2 = overridingClassLoader.loadClass(TestBean.class.getName());
Assert.assertTrue("TestBean will exclude from OverridingClassLoader, so no reload",
excludedClazz1 == excludedClazz2);
Class<?> nonExcludedClazz1 = appClassLoader.loadClass(ITestBean.class.getName());
Class<?> nonExcludedClazz2 = overridingClassLoader.loadClass(ITestBean.class.getName());
Assert.assertFalse("ITestBean will not exclude, so reload again",
nonExcludedClazz1 == nonExcludedClazz2);
}
可以看到,ITestBean 被 OverridingClassLoader 重新加载了一次,而 TestBean 添加到了 excludedClasses 中还是使用 JDK 的默认加载器,因此不会被重新加载。
二、OverridingClassLoader 源码分析
2.1 DecoratingClassLoader
DecoratingClassLoader 很简单,内部维护了两个集合,如果你不想你的类被自定义的类加载器管理,可以把它添加到这两个集合中,这样仍使用 JDK 的默认类加载机制。
private final Set<String> excludedPackages = Collections.newSetFromMap(new ConcurrentHashMap<>(8));
private final Set<String> excludedClasses = Collections.newSetFromMap(new ConcurrentHashMap<>(8));
// isExcluded 返回 true 时仍使用 JDK 的默认类加载机制,返回 false 时自定义的类加载器生效
protected boolean isExcluded(String className) {
if (this.excludedClasses.contains(className)) {
return true;
}
for (String packageName : this.excludedPackages) {
if (className.startsWith(packageName)) {
return true;
}
}
return false;
}
2.2 OverridingClassLoader
(1) loadClass
isEligibleForOverriding () 返回 true 时使用 OverridingClassLoader 先加载,只有加载不到才会双亲委派,否则直接进行双亲委派。代码很简单就不细看了。
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
if (this.overrideDelegate != null && isEligibleForOverriding(name)) {
return this.overrideDelegate.loadClass(name);
}
return super.loadClass(name);
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if (isEligibleForOverriding(name)) {
Class<?> result = loadClassForOverriding(name);
if (result != null) {
if (resolve) {
resolveClass(result);
}
return result;
}
}
return super.loadClass(name, resolve);
}
// isExcluded(className)=false 时说明没有添加到 excludedPackages 或 excludedClasses
// 此时可以使用自定义的类加载器加载
protected boolean isEligibleForOverriding(String className) {
return !isExcluded(className);
}
(2) loadClassForOverriding
loadClassForOverriding 也是从 classpath 直接找到对应的 .class 文件,然后重新加载。
protected Class<?> loadClassForOverriding(String name) throws ClassNotFoundException {
Class<?> result = findLoadedClass(name);
if (result == null) {
byte[] bytes = loadBytesForClass(name);
if (bytes != null) {
result = defineClass(name, bytes, 0, bytes.length);
}
}
return result;
}
protected byte[] loadBytesForClass(String name) throws ClassNotFoundException {
InputStream is = openStreamForClass(name);
if (is == null) {
return null;
}
try {
byte[] bytes = FileCopyUtils.copyToByteArray(is);
// transformIfNecessary 留给子类重写
return transformIfNecessary(name, bytes);
} catch (IOException ex) {
throw new ClassNotFoundException("Cannot load resource for class [" + name + "]", ex);
}
}
protected InputStream openStreamForClass(String name) {
String internalName = name.replace('.', '/') + CLASS_FILE_SUFFIX;
return getParent().getResourceAsStream(internalName);
}