字节码相关内容往深了挖其实东西很多,我就按照自己学习的一个心理历程去分享一下这块儿的内容,起个抛砖引玉的作用,很多地方没有特别深入的研究,有待大家补充。
Java作为一款“一次编译,到处运行”的编程语言,跨平台靠的是JVM实现对不同操作系统API的支持,而一次编译
指的就是class
字节码;即我们编写好的.java
文件,通过编译器编译成.class
文件,JVM负责加载解释字节码文件,并生成系统可识别的代码执行(具体解析本次不做深入研究).
字节码官方文档
从代码开始:
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进制。
下面是官方文档给出的定义:
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
即常量池长度为33tag
,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_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
后面再讲。
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规范.
方法和字段都有自己的描述信息,方法的描述包括参数、返回值的类型,字段描述为字段的类型,下面是类型表:
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_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."":()V
4: return
这个就是我们上面那个类的无参构造函数编译后的效果,那这里面的aload_0
、invokespecial
、return
学过JVM相关知识的话,大家已经很熟悉了.
aload_0
就是变量0进栈invokespecial
调用实例的初始化方法,即构造方法return
即方法结束,返回值为void
那这些aload_0
、invokespecial
、return
相关的指令是如何存储在code
数组中的,或者说是以什么形式存在的?
其实JVM有这样一个指令数组,code
数组中的记录的就是指令数组的有效下标,下面是部分指令:
JVM指令 | 指令下标 | 描述 |
---|---|---|
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 |
熟悉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
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Methodref #3.#9 // java/lang/Object."":()V
#9 = NameAndType #5:#6 // "":()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."":()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 是一个 Java 字节码操控框架,它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。
说白了,ASM可以在不修改Java源码文件的情况下,直接对Class文件进行修改,改变或增强原有类功能。
在熟悉了字节码原理的情况下,理解动态修改字节码技术会更加容易,接下来我们只针对ASM框架中几个主要类进行分析,并举个栗子帮助大家理解。
提供各种对字节码操作的方法,包括对属性、方法、注解等内容的修改:
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继承了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() {
}
}
//读取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);
}
}
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, "" , "()V", null, null);
// 生成构造方法的字节码指令
// aload_0 加载0位置的局部变量,即this
mw.visitVarInsn(Opcodes.ALOAD, 0);
// 调用初始化函数
mw.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "" , "()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
#6 = Utf8 ()V
#7 = NameAndType #5:#6 // "":()V
#8 = Methodref #4.#7 // java/lang/Object."":()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."":()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字节码的类库。
主要负责加载或者生产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对象对应一个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 {}
}
对应class中的Method
public final class CtMethod extends CtBehavior {
//修改方法名
public void setName(String newname) {}
//修改方法体
public void setBody(CtMethod src, ClassMap map) throws CannotCompileException{}
}
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."":()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."":()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
}
上面讲到所有的内容都是Demo级别的例子,并没有从项目使用层面来分析这些技术如何使用。比如,我们修改的字节码何时加载到JVM?运行中的项目如果动态修改某个类的实现,怎么加载?
ClassLoader
双亲委托机制确保了每个Class只能被一个ClassLoader
加载,每个ClassLoader
关注自己的资源目录:
BootStrapClassLoader
-> /lib
或者-Xbootclasspath
指定的路径ExtClassLoader
-> /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的需求。
在 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的开发语言,怎么搞?
静态,动态?
,强类型,弱类型?
….class
有兴趣的大佬,可以试试
还可以继续引申,语义语法都定义好了,是不是可以实现编译器直接编译成.exe
文件,或者linux
下可以运行程序?
JVM如何将指令生成对应代码
ASM
,CGLib
,Javassisit
,AspectJ
,JDK Proxy
…