字节码详解
前言
万事开头难
字节码相关内容往深了挖其实东西很多,我就按照自己学习的一个心理历程去分享一下这块儿的内容,起个抛砖引玉的作用,很多地方没有特别深入的研究,有待大家补充。
什么是字节码
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
数组中的记录的就是指令数组的有效下标,下面是部分指令:
指令 | 指令下标 | 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
与ASM一样,Javassist也是一个处理Java字节码的类库。
主要类介绍
ClassPool
主要负责加载或者生产class文件
public class ClassPool { //新建一个class,classname为类的全限类名 public CtClass makeClass(String classname) throws RuntimeException { return makeClass(classname, null); } //增加一个jar包或者目录供搜索class使用 public ClassPath insertClassPath(String pathname) throws NotFoundException { return source.insertClassPath(pathname); } //从搜索目录中找到对应class并返回CtClass引用供后续功能使用 public CtClass get(String classname) throws NotFoundException { } }
CtClass
一个CtClass对象对应一个Class字节码对象。
public abstract class CtClass { //为class增加接口、字段、方法 public void addInterface(CtClass anInterface) {} public void addField(CtField f) throws CannotCompileException {} public void addMethod(CtMethod m) throws CannotCompileException {} //在指定目录生产class文件 public void writeFile(String directoryName) throws CannotCompileException, IOException{} //生成class对象到当前JVM中,即加载当前修改的Class对象 public Class<?> toClass() throws CannotCompileException {} }
CtMethod
对应class中的Method
public final class CtMethod extends CtBehavior { //修改方法名 public void setName(String newname) {} //修改方法体 public void setBody(CtMethod src, ClassMap map) throws CannotCompileException{} }
CtBehavior
public abstract class CtBehavior extends CtMember { //设置方法体 public void setBody(String src) throws CannotCompileException {} //在方法体前插入代码 public void insertBefore(String src) throws CannotCompileException {} //在方法体最后插入代码 public void insertAfter(String src) throws CannotCompileException {} }
再举个栗子
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(); } } }
上面这个例子一定会报错attempted duplicate class definition for name: "com/qty/first/SsisitObj"
因为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(); } //代码只是示意,如果真实需求需要使用自定义classLoader加载,那么会缓存当前ClassLoader //当Class对象更改时再进行更换 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); } /** * 绕过双亲委派逻辑,直接获取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