字节码
虚拟机实现的方式主要有以下两种:
- 将输入的Java虚拟机代码在加载时或执行时翻译成另一种虚拟机的指令集;
- 将输入的Java虚拟机代码在加载时或执行时翻译成宿主机处理程序的本地指令集(即即时编译器代码生成技术)。
Class 文件
- JVM所执行的二进制文件,跨平台的基础
- 满足这种规范的class文件都会被JVM加载运行
- 可以由其他语言编译生成
- 不同版本的JDK生成的类文件略有不同
mindmap Class文件 魔数 Class文件的特征 小版本号 大版本号 常量池 各种常量 整数 字符串 Class 访问标记 static public 当前类 类的属性 类的方法 访问标记 名称 描述符 子主题 code属性 行号属性 局部变量表 栈映射帧 类的字段 访问标记 名称 描述符 属性 实现的接口 父类
构成
类型 | 名称 | 数量 |
---|---|---|
u4 | magic | 1 |
u2 | minor_version | 1 |
u2 | major_version | 1 |
u2 | constant_pool_count | 1 |
cp_info | constant_pool | constant_pool_count-1 |
u2 | access_flags | 1 |
u2 | this_class | 1 |
u2 | super_class | 1 |
u2 | interfaces_count | 1 |
u2 | interfaces | interfaces_count |
u2 | fields_count | 1 |
field_info | fields | fields_count |
u2 | methods_count | 1 |
method_info | methods | methods_count |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
Class 文件中的所有字节存储都是使用大端序
Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:
- 无符号数:以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数
- 以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值
- 表:由多个无符号数或者其他表作为数据项构成的复合数据类型
反编译:
javap -v classname
魔数与 Class 文件版本
前4个字节为魔数,十六进制表示为0xCAFEBABE,标识该文件为class文件
第5、6字节表示次版本号(小更新) 第7和第8个字节是主版本号(从45开始,一个大版本加1)
常量池
常量池的入口放置了一项u2类型的数据,代表常量池容量计数值(constant_pool_count),这个数是从1开始
- 字面量:接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等
- 符号引用
- 被模块导出或者开放的包(Package)
- 类和接口的全限定名(Fully Qualified Name)
- 字段的名称和描述符(Descriptor)
- 方法的名称和描述符
- 方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic
- 动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)
访问标志
常量池结束之后的两个字节,描述该Class是类还是接口,以及是否被public、abstract、final等修饰符修饰
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为public类型 |
ACC_FINAL | 0x0010 | 是否被声明为final,只有类可设置 |
AcC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令的新语义,invokespecial指令的语义在JDK1.0.2发生过改变,为了区别这条指令使用哪种语义,JDK1.0.2之后编译出来的类的这个标志都必须为真 |
ACC_INTERFACE | 0x0200 | 标识这是一个接口 |
AcC_ABSTRACT | Ox0400 | 是否为abstract类型,对于接口或者抽象类来说,此标志值为真,其他类型值为假 |
AcC_SYNTHETIC | Ox1000 | 标识这个类并非由用户代码产生的 |
Acc_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
AcC_MODULE | 0x8000 | 标识这是一个模块 |
类索引、父类索引与接口索引集合
- 类索引与父类索引都是一个u2类型的数据
- 接口索引入口的第一项u2类型的数据为接口计数器
字段表集合
- 用于描述类和接口中声明的变量(包括类级以及实例级别)
- 第一部分为两个字节,描述字段个数;第二部分是每个字段的详细信息fields_info。
字段表结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
字段访问标志,存放在access_flags里面:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | Ox0001 | 字段是否public |
ACC_PRIVATE | Ox0002 | 字段是否 private |
ACC_PROTECTED | ox0004 | 字段是否 protected |
ACC_STATIC | Ox0008 | 字段是否static |
ACC FINAL | 0x0010 | 字段是否final |
ACC_VOLATILE | Ox0040 | 字段是否 volatile |
ACC_TRANSIENT | ox0080 | 字段是否transient |
ACC_SYNTHETIC | ox1000 | 字段是否由编译器自动产生 |
ACC_ENUM | Ox4000 | 字段是否 enum |
name_index和descriptor_index分别代表着字段的简单名称以及字段和方法的描述符
- 简单名称则就是指没有类型和参数修饰的方法或者字段名称
描述符:
标志字符 | 含义 |
---|---|
B | 基本类型byte |
C | 基本类型char |
D | 基本类型double |
F | 基本类型float |
I | 基本类型int |
J | 基本类型1ong |
S | 基本类型short |
Z | 基本类型boolean |
V | 特殊类型void |
L | 对象类型,如Ljava/lang/Object; |
对于数组类型,每一维度将使用一个前置的[
字符来描述,,如一个定义为“java.lang.String[][]”类型的二维数组将被记录成“[[Ljava/lang/String;”
用描述符来描述方法时,按照先参数列表、后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”之内
方法int indexOf(char[]source,int sourceOffset,int sourceCount,char[]target,int targetOffset,int targetCount,int fromIndex)的描述符为“([CII[CIII)I”
方法表集合
方法表的结构同字段表
方法访问标志:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | Ox0001 | 方法是否为public |
ACC_PRIVATE | Ox0002 | 方法是否为private |
ACC_PROTECTED | Ox0004 | 方法是否为protected |
ACC_STATIC | 0x0008 | 方法是否为static |
ACC_FINAL | 0x0010 | 方法是否为final |
AcC_SYNCHRONIZED | Ox0020 | 方法是否为synchronized |
ACC_BRIDGE | Ox0040 | 方法是不是由编译器产生的桥接方法 |
AcC_VARARGS | Ox0080 | 方法是否接受不定参数 |
ACC_NATIVE | Ox0100 | 方法是否为native |
ACC_ABSTRACT | 0x0400 | 方法是否为abstract |
AcC_STRICT | Ox0800 | 方法是否为strictfp |
ACC_SYNTHETIC | Ox1000 | 方法是否由编译器自动产生 |
方法里的Java代码,经过Javac编译器编译成字节码指令之后,存放在方法属性表集合中一个名为“Code”的属性里面
属性表集合
- Class文件、字段表、方法表都可以携带自己的属性表集合,以描述某些场景专有的信息
属性名称 | 使用位置 | 含义 |
---|---|---|
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | 由final关键字定义的常量值 |
Deprecated | 类、方法表、字段表 | 被声明为deprecated 的方法和字段 |
Exceptions | 方法表 | 方法抛出的异常列表 |
EnclosingMethod | 类文件 | 仅当一个类为局部类或者匿名类时才能拥有这个属性,这个属性用于标示这个类所在的外围方法类文件 |
InncrClasses | Code属性 | 内部类列表 |
LineNumberTable | Codc属性 | Java 源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
StackMapTable | Code属性 | JDK6中新增的属性,供新的类型检查验证器(Type Checker)检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配 |
Signature | 类、方法表、字段表 | JDK 5中新增的属性,用于支持范型情况下的方法签名。在Java语言中,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(TypeVariables)或参数化类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。由于Java的范型采用擦除法实现,为了避免类型信息被擦除后导致签名混乱,需要这个属性记录范型中的相关信息 |
SourceFile | 类文件 | 记录源文件名称 |
SourceDebugExtension | 类文件 | JDK 5中新增的属性,用于存储额外的调试信息。譬如在进行JSP文件调试时,无法通过Java堆栈来定位到JSP文件的行号JSR 45提案为这些非Java类文件语言编写,却需要编译成字节码并运行在Java虚拟机中的程序提供了一个进行调试的标准机制,使用该属性就可以用于存储这个标准所新加人的调试信息 |
Synthetic | 类、方法表、字段表 | 标识方法或字段为编译器自动生成的 |
LocalVariablcTypeTable | 类 | JDK 5中新增的属性,它使用特征签名代替描述符,是为了引人泛型语法之后能描述泛型参数化类型而添加 |
RuntimeVisibleAnnotations | 类、方法表、字段表 | JDK 5中新增的属性,为动态注解提供支持。该属性用于指明哪些注解是运行时(实际上运行时就是进行反射调用)可见的 |
RuntimcInvisiblcAnnotations | 类、方法表、字段表 | JDK 5中新增的属性,与RuntimeVisibleAnnota-tions属性作用刚好相应,用于指明哪些注解是运行时不可见的 |
RuntimeVisibleParamcterAnnotations | 方法表 | JDK5中新增的属性,作用与RuntimeVisible-Annotations属性类似,只不过作用对象为方法参数 |
RuntimelnvisibleParameterAnnotations | 方法表 | JDK 5中新增的属性,作用与 RuntimelnvisiblcAnnotations属性类似,只不过作用对象为方法参数 |
AnnotationDefault | 方法表 | JDK 5中新增的属性,用于记录注解类元素的默认值 |
BootstrapMethods | 类文件 | JDK 7中新增的属性,用于保存invokedynamic指令引用的引导方法限定符 |
RuntimeVisibleTypeAnnotations | 类、方法表、字段表,Code属性 | JDK 8中新增的属性,为实现JSR 308中新增的类型注解提供的支持,用于指明哪些类注解是运行时(实际上运行时就是进行反射调用)可见的 |
RuntimelnvisibleTypeAnnotations | 类、方法表、字段表,Code属性 | JDK 8中新增的属性,为实现JSR 308中新增的类型注解提供的支持,与RuntimeVisibleTypeAnnotations属性作用刚好相反,用于指明哪些注解是运行时不可见的 |
MethodParameters | 方法表 | JDK 8中新增的属性,用于支持(编译时加上-parameters参数)将方法名称编译进 Class文件中,并可运行时获取。此前要获取方法名称(典型的如IDE的代码提示)只能通过JavaDoc中得到 |
Module | 类 | JDK 9中新增的属性,用于记录一个Module的名称以及相关信息(requires.exports.opens, uses .provides) |
ModulePackages | 类 | JDK9中新增的属性,用于记录一个模块中所有被exports或者opens 的包 |
ModuleMainClass | 类 | JDK9中新增的属性,用于指定一个模块的主类 |
NestHost | 类 | JDK 11中新增的属性,用于支持嵌套类(Java中类的内部类)的反射和访问控制的API,一个内部类通过该属性得知自己的宿主类 |
NestMembers | 类 | JDK 11中新增的属性,用于支持嵌套类(Java中的内部类)的反射和访问控制的API,一个宿主类通过该属性得知自已己有哪些内部类 |
属性表结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u1 | info | attribute_length |
1.Code属性
类型 | 名称 | 数量 | 说明 |
---|---|---|---|
u2 | attribute_name_index | 1 | attribute_name_index是一项指向CONSTANT_Utf8_info型常量的索引,此常量值固定为"Code",它代表了该属性的属性名称 |
u4 | attribute_length | l | 属性值的长度 |
u2 | max_stack | l | 代表了操作数栈(Operand Stack)深度的最大值 |
u2 | max_locals | 1 | 代表了局部变量表所需的存储空间(32位以下(包含)的变量占用一个槽) |
u4 | code_length | 1 | 执行的字节码长度(《Java虚拟机规范》中明确限制了一个方法不允许超过65535条字节码指令) |
u1 | code | code_length | 存放执行的字节码 |
u2 | exception_table_length | 1 | |
exception_info | exception_table | exception_table_length | |
u2 | attributes_count | 1 | |
u2 | attributes_count | 1 | |
attribute_info | attributes | attributes_count |
public int inc() {return m + 1;}
一个方法编译后:
public int inc(); descriptor: ()I flags: (0x0001) ACC_PUBLIC Code: stack=2, locals=1, args_size=1 //这里的方法虽然没有参数,但是参数数量为1,1就是this 0: aload_0 1: getfield #2 // Field m:I 4: iconst_1 5: iadd 6: ireturn LineNumberTable: line 4: 0
异常表:
类型 | 名称 | 数量 | 说明 |
---|---|---|---|
u2 | start_pc | 1 | 异常捕获起始行 |
u2 | end_pc | 1 | 异常捕获结束行(不包含本行) |
u2 | handler_pc | 1 | 发生异常后跳转的位置 |
u2 | catch_type | 1 | 异常的类型 |
2.Exceptions属性
列举出方法中可能抛出的受查异常(Checked Excepitons)
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | number_of_exceptions | 1 |
u2 | exception_index_table | number_of_exceptions |
3.LineNumberTable属性
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | line_number_table_length | 1 |
line_number_info | line_number_table | line_number_table_length |
4.LocalVariableTable及LocalVariableTypeTable属性
LocalVariableTable属性用于描述栈帧中局部变量表的变量与Java源码中定义的变量之间的关系
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | local_variable_table_length | 1 |
local_variable_info | local_variable_table | local_variable_table_length |
local_variable_info项目代表了一个栈帧与源码中的局部变量的关联:
类型 | 名称 | 数量 | 说明 |
---|---|---|---|
u2 | start_pc | 1 | 生命周期开始的字节码偏移量 |
u2 | length | 1 | 作用范围覆盖的长度 |
u2 | name_index | 1 | 局部变量的名称 |
u2 | descriptor_index | 1 | 局部变量的描述符 |
u2 | index | 1 | 局部变量在栈帧的局部变量表中变量槽的位置 |
LocalVariableTypeTable。这个新增的属性结构与LocalVariableTable非常相似,仅仅是把记录的字段描述符的descriptor_index替换成了字段的特征签名(Signature)
5.SourceFile及SourceDebugExtension属性
类型 | 名称 | 数量 | 说明 |
---|---|---|---|
u2 | attribute_name_index | 1 | |
u4 | attribute_length | 1 | |
u2 | sourcefile_index | 1 | 指向常量池中CONSTANT_Utf8_info型常量的索引,常量值是源码文件的文件名 |
SourceDebugExtension属性用于存储额外的代码调试信息:
类型 | 名称 | 数量|说明-- | --------------------------------- | --u2 | attribute_name_index | 1u4 | attribute_length | 1u1 | debug_extension[attribute_length] | 1|额外的debug信息
6.ConstantValue属性
通知虚拟机自动为静态变量赋值。
目前Oracle公司实现的Javac编译器的选择是,如果同时使用final和static来修饰一个变量(按照习惯,这里称“常量”更贴切),并且这个变量的数据类型是基本类型或者java.lang.String的话,就将会生成ConstantValue属性来进行初始化
类型 | 名称 | 数量 | 说明 |
---|---|---|---|
u2 | attribute_name_index | 1 | |
u4 | attribute_length | 1 | |
u2 | constantvalue_index | 1 | 所以这里的常量最多只能为64bit |
7.InnerClasses属性
用于记录内部类与宿主类之间的关联
类型 | 名称 | 数量 | 说明 |
---|---|---|---|
u2 | attribute_name_index | 1 | |
u4 | attribute_length | 1 | |
u2 | number_of_classes | 1 | 表需要记录多少个内部类信息 |
inner_class_info | inner_classes | number_of_classes | 记录的内部类信息 |
类型 | 名称 | 数量 | 说明 |
---|---|---|---|
u2 | inner_class_info_index | 1 | 内部类的符号引用 |
u2 | outer_class_info_index | 1 | 宿主类的符号引用 |
u2 | inner_name_index | 1 | 代表这个内部类的名称,如果是匿名内部类,这项值为0 |
u2 | inner_class_access | flags | 1 |
8.Deprecated及Synthetic属性
Deprecated属性用于表示某个类、字段或者方法,已经被程序作者定为不再推荐使用,它可以通过代码中使用“@deprecated”注解进行设置
Synthetic属性代表此字段或者方法并不是由Java源码直接产生的
属性结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
9.StackMapTable属性
在编译阶段将一系列的验证类型(Verification Type)直接记录在Class文件之中,通过检查这些验证类型代替了类型推导过程,从而大幅提升了字节码验证的性能
类型 | 名称 | 数量 | 说明 |
---|---|---|---|
u2 | attribute_name_index | 1 | |
u4 | attribute_length | 1 | |
u2 | number_of_entries | 1 | |
stack_map_frame | stack_map_frame_entries | number_of_entries |
10.Signature属性
一个可选的定长属性,可以出现于类、字段表和方法表结构的属性表中,用来记录泛型信息
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | signature_index | 1 |
11.BootstrapMethods属性
位于类文件的属性表中。这个属性用于保存invokedynamic指令引用的引导方法限定符
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | num_bootstrap_methods | 1 |
bootstrap_method | bootstrap_methods | num_bootstrap_methods |
12.MethodParameters属性
一个用在方法表中的变长属性。MethodParameters的作用是记录方法的各个形参名称和信息
13.模块化相关属性
14.运行时注解相关属性
字节码指令
不考虑异常处理的字节码执行:
do {自动计算PC寄存器的值加1;根据PC寄存器指示的位置,从字节码流中取出操作码;if (字节码存在操作数) 从字节码流中取出操作数;执行操作码所定义的操作;} while (字节码流长度 > 0);
大部分与数据类型相关的字节码指令,它们的操作码助记符中都有特殊的字符来表明专门为哪种数据类型服务:i代表对int类型的数据操作,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference
- 基于寄存器的指令集
- 基于栈的指令集Hotspot中的Local Variable Table = JVM中的寄存器
加载和存储指令
用于将数据在栈帧中的局部变量表和操作数栈之间来回传输
- 将一个局部变量加载到操作栈:`iload、iload_
、lload、lload_ 、fload、fload_ 、dload、dload_ 、aload、aload_ ` - 将一个数值从操作数栈存储到局部变量表:`istore、istore_
、lstore、lstore_ 、fstore、fstore_ 、dstore、dstore_ 、astore、astore_ ` - 将一个常量加载到操作数栈:`bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_、lconst_
、fconst_ 、dconst_ `
iload_<n>
代表了iload_0、iload_1、iload_2和iload_3这几条指令 这些指令都是iload的特殊形式,这些特殊的指令省略掉了操作数,但是语义同iload一样
运算指令
用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶
- 加法指令:iadd、ladd、fadd、dadd
- 减法指令:isub、lsub、fsub、dsub
- 乘法指令:imul、lmul、fmul、dmul
- 除法指令:idiv、ldiv、fdiv、ddiv
- 求余指令:irem、lrem、frem、drem
- 取反指令:ineg、lneg、fneg、dneg
- 位移指令:ishl、ishr、iushr、lshl、lshr、lushr
- 按位或指令:ior、lor
- 按位与指令:iand、land
- 按位异或指令:ixor、lxor
- 局部变量自增指令:iinc
- 比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp
类型转换指令
Java虚拟机直接支持(即转换时无须显式的转换指令)的宽化类型转换
窄化转换:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f
对象/数组创建与访问指令
- 创建类实例的指令:new
- 创建数组的指令:newarray、anewarray、multianewarray
- 访问类字段(static字段,或者称为类变量)和实例字段(非static字段,或者称为实例变量)的指令:getfield、putfield、getstatic、putstatic
- 把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、daload、aaload
- 将一个操作数栈的值储存到数组元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore
- 取数组长度的指令:arraylength
- 检查类实例类型的指令:instanceof、checkcast
操作数栈管理指令
直接操作操作数栈的指令
- 将操作数栈的栈顶一个或两个元素出栈:pop、pop2
- 复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2
- 将栈最顶端的两个数值互换:swap
控制转移指令
有条件或无条件地从指定位置指令(而不是控制转移指令)的下一条指令继续执行程序,概念模型上理解,可以认为控制指令就是在有条件或无条件地修改PC寄存器的值
- 条件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne
- 复合条件分支:tableswitch、lookupswitch
- 无条件分支:goto、goto_w、jsr、jsr_w、ret
方法调用和返回指令
- invokevirtual指令:用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式。
- invokeinterface指令:用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。
- invokespecial指令:用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。
- invokestatic指令:用于调用类静态方法(static方法)。
- invokedynamic指令:用于在运行时动态解析出调用点限定符所引用的方法。并执行该方法。前面四条调用指令的分派逻辑都固化在Java虚拟机内部,用户无法改变,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的
方法返回指令是根据返回值的类型区分的,包括ireturn(当返回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn和areturn,另外还有一条return指令供声明为void的方法、实例初始化方法、类和接口的类初始化方法使用
异常处理指令
- 显式抛出异常的操作(throw语句)都由athrow指令来实现
- 异常捕获现在则是使用了异常表的来完成
同步控制指令
- ACC_SYNCHRONIZED 标志同步方法
- MONITORENTER MONITOREXIT 标记临界区
ASM
ASM是一个通用的Java字节码操作和分析框架
- Core API
- 不需要读取类的整个结构,使用流式的方法来处理字节码文件
- 编程难度较大
- Tree API
- 消耗内存多,但是编程比较简单
- 通过各种Node类来映射字节码的各个区域
字节码增强
- java agent
- premain:支持在main函数之前,对类的字节码进行修改/替换
- agentmain:支持在程序运行过程中,对字节码进行替换
字节码混淆
- 使反编译的代码可读性变差
- ProGuard
javassist
- 生成类
ClassPool pool = ClassPool.getDefault();// 创建类CtClass userClass = pool.makeClass("wang.ismy.test.User");// 添加属性userClass.addField(CtField.make("private String name;",userClass));userClass.addField(CtField.make("private Integer age;",userClass));// 添加方法userClass.addMethod(CtMethod.make("public String getName(){return name;}",userClass));userClass.addConstructor(new CtConstructor(new CtClass[]{pool.get("java.lang.String"),pool.get("java.lang.Integer")},userClass));userClass.writeFile("./User.class");
- 修改类
ClassPool pool = ClassPool.getDefault();pool.appendClassPath(new ClassClassPath(Main.class));CtClass userClass = pool.get("wang.ismy.assist.User");userClass.getDeclaredMethod("getName").setBody("{return name + \"123\";}");Class<?> aClass = userClass.toClass();Object obj = aClass.newInstance();System.out.println(aClass.getMethod("getName").invoke(obj));
实例-动态代理字节码生成
public class DynamicProxyTest { public static void main(String[] args) { System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); Run origin = new Run(); Runnable hello = (Runnable)Proxy.newProxyInstance(Run.class.getClassLoader(), new Class[]{Runnable.class}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("hello"); return method.invoke(origin); } }); hello.run(); }}class Run implements Runnable{ @Override public void run() { System.out.println("world"); }}
这里newProxyInstance会生成这么样的一个代理类:
public final class $Proxy0 extends Proxy implements Runnable { ... public final void run() throws { try { super.h.invoke(this, m3, (Object[])null); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } ...}
生成的代理类run方法会调用我们写的InvocationHandler 我们的InvocationHandler又会执行一些附带逻辑并最后执行真实对象的方法
实例-Backport:回到未来
把高版本JDK中编写的代码放到低版本JDK环境中去部署使用。为了解决这个问题,一种名为“Java逆向移植”的工具(Java Backporting Tools)应运而生,Retrotranslator和Retrolambda是这类工具中的杰出代表
这些工具可以很好地移植下面两种情况:
- 对Java类库API的代码增强
- 在前端编译器层面做的改进。这种改进被称作语法糖
对于第一种情况 一些诸如只有高版本才有的类库移植工具可以很方便移植
但对于第二种情况,移植工具就需要通过修改字节码的方式来实现