字节码相关内容往深了挖其实东西很多,我就按照自己学习的一个心理历程去分享一下这块儿的内容,起个抛砖引玉的作用,很多地方没有特别深入的研究,有待大家补充。
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]; }其他地方的16进制没那么显眼,唯独开头的4个字节开起来像是个单词CAFEBABE.
为什么所有文件都要有一个魔数开头,其实就是让JVM有一个稳定快速的途径来确认这个文件是字节码文件。
为什么一定是CafeBabe,源于Java与咖啡的不解之缘。像是zip文件的PK.
这个报错大家应该都见过,出现这个报错的时候都知道是JDK版本不对,立马去IDE上修改JDK编译版本、运行版本,OK报错解决。不过为什么JDK不一致时会报错呢,JVM是怎么确定版本不一致的?
从字节码文件说,CafeBabe继续往后看八个字节,分别是0000、0034,我本地环境使用的是JDK1.8
class文件中看到的是16进制,把0034转为10进制的数字就是52。我用JDK1.7编译之后,如下: 主版本号对应的两个字节,根据我们本地编译版本不同也会不同。
下面是JDK版本与版本号对应关系:
jdk版本major.minor version1.1451.2461.3471.448549650751852访问标识类型表:
Flag NameValueInterpretationACC_PUBLIC0x0001Declared public; may be accessed from outside its package.ACC_FINAL0x0010Declared final; no subclasses allowed.ACC_SUPER0x0020Treat superclass methods specially when invoked by the invokespecial instruction.ACC_INTERFACE0x0200Is an interface, not a class.ACC_ABSTRACT0x0400Declared abstract; must not be instantiated.ACC_SYNTHETIC0x1000Declared synthetic; not present in the source code.这个关键字不是源码生成,而是编译器生成的ACC_ANNOTATION0x2000Declared as an annotation type.ACC_ENUM0x4000Declared 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 TypeValueCONSTANT_Class7CONSTANT_Fieldref9CONSTANT_Methodref10CONSTANT_InterfaceMethodref11CONSTANT_String8CONSTANT_Integer3CONSTANT_Float4CONSTANT_Long5CONSTANT_Double6CONSTANT_NameAndType12CONSTANT_Utf81CONSTANT_MethodHandle15CONSTANT_MethodType16CONSTANT_InvokeDynamic18不同常量对应后续字节数不同,如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_info { u2 access_flags; //访问标识 u2 name_index; u2 descriptor_index; u2 attributes_count; //属性个数 attribute_info attributes[attributes_count]; }field访问标识类型如下:
Flag NameValueInterpretationACC_PUBLIC0x0001Declared public; may be accessed from outside its package.ACC_PRIVATE0x0002Declared private; usable only within the defining class.ACC_PROTECTED0x0004Declared protected; may be accessed within subclasses.ACC_STATIC0x0008Declared static.ACC_FINAL0x0010Declared final; never directly assigned to after object construction (JLS §17.5).ACC_VOLATILE0x0040Declared volatile; cannot be cached.ACC_TRANSIENT0x0080Declared transient; not written or read by a persistent object manager.ACC_SYNTHETIC0x1000Declared synthetic; not present in the source code.ACC_ENUM0x4000Declared 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 NameValueInterpretationACC_PUBLIC0x0001Declared public; may be accessed from outside its package.ACC_PRIVATE0x0002Declared private; accessible only within the defining class.ACC_PROTECTED0x0004Declared protected; may be accessed within subclasses.ACC_STATIC0x0008Declared static.ACC_FINAL0x0010Declared final; must not be overridden (§5.4.5).ACC_SYNCHRONIZED0x0020Declared synchronized; invocation is wrapped by a monitor use.ACC_BRIDGE0x0040A bridge method, generated by the compiler.ACC_VARARGS0x0080Declared with variable number of arguments.ACC_NATIVE0x0100Declared native; implemented in a language other than Java.ACC_ABSTRACT0x0400Declared abstract; no implementation is provided.ACC_STRICT0x0800Declared strictfp; floating-point mode is FP-strict.ACC_SYNTHETIC0x1000Declared synthetic; not present in the source code.ACC_BRIDGE也是由编译器生成的,比如泛型的子类重写父类方法, 就会有一个在子类生成一个新的方法用ACC_BRIDGE标识
ACC_VARARGS可变参数的方法会出现这个标记
ACC_STRICT strictfp标识的方法中,所有float和double表达式都严格遵守FP-strict的限制,符合IEEE-754规范.
方法和字段都有自己的描述信息,方法的描述包括参数、返回值的类型,字段描述为字段的类型,下面是类型表:
FieldType termTypeInterpretationBbytesigned byteCcharUnicode character code point in the Basic Multilingual Plane, encoded with UTF-16Ddoubledouble-precision floating-point valueFfloatsingle-precision floating-point valueIintintegerJlonglong integerL ClassName ;referencean instance of class ClassNameSshortsigned shortZbooleantrue or false[referenceone 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."<init>":()V 4: return这个就是我们上面那个类的无参构造函数编译后的效果,那这里面的aload_0、invokespecial、return学过JVM相关知识的话,大家已经很熟悉了.
aload_0就是变量0进栈invokespecial调用实例的初始化方法,即构造方法return 即方法结束,返回值为void那这些aload_0、invokespecial、return相关的指令是如何存储在code数组中的,或者说是以什么形式存在的?
其实JVM有这样一个指令数组,code数组中的记录的就是指令数组的有效下标,下面是部分指令:
JVM指令指令下标描述return0xB1当前方法返回voidareturn0xB0从方法中返回一个对象的引用ireturn0xAC当前方法返回intiload_00x1A第一个int型局部变量进栈lload_00x1E第一个long型局部变量进栈istore_00x3B将栈顶int型数值存入第一个局部变量lstore_00x3F将栈顶long型数值存入第一个局部变量getstatic0xB2获取指定类的静态域,并将其值压入栈顶putstatic0xB3为指定的类的静态域赋值invokespecial0xB7调用超类构造方法、实例初始化方法、私有方法invokevirtual0xB6调用实例方法iadd0x60栈顶两int型数值相加,并且结果进栈iconst_00x03int型常量值0进栈ldc0x12将int、float或String型常量值从常量池中推送至栈顶详细指令列表可以查看官方文档。
关于attribute_info还有其他类型,有兴趣的可以查看Attribute,类型及其出现位置如下:
AttributeLocationSourceFileClassFileInnerClassesClassFileEnclosingMethodClassFileSourceDebugExtensionClassFileBootstrapMethodsClassFileConstantValuefield_infoCodemethod_infoExceptionsmethod_infoRuntimeVisibleParameterAnnotations, RuntimeInvisibleParameterAnnotationsmethod_infoAnnotationDefaultmethod_infoMethodParametersmethod_infoSyntheticClassFile, field_info, method_infoDeprecatedClassFile, field_info, method_infoSignatureClassFile, field_info, method_infoRuntimeVisibleAnnotations, RuntimeInvisibleAnnotationsClassFile, field_info, method_infoLineNumberTableCodeLocalVariableTableCodeLocalVariableTypeTableCodeStackMapTableCodeRuntimeVisibleTypeAnnotations, RuntimeInvisibleTypeAnnotationsClassFile, 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 <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 是一个 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() { } }以上这些类都只是截取其中一部分,旨在讲解思路。
废话不多说,直接献上代码:
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字节码的类库。
主要负责加载或者生产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{} }输出如下:
属性名称: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 }上面讲到所有的内容都是Demo级别的例子,并没有从项目使用层面来分析这些技术如何使用。比如,我们修改的字节码何时加载到JVM?运行中的项目如果动态修改某个类的实现,怎么加载?
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的需求。
在 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下可以运行程序?
