字节码详解

文章目录

  • 字节码详解
    • 前言
      • 万事开头难
      • 什么是字节码
    • Class文件
      • hello world
      • class结构
        • 为什么是``CafeBabe``
        • ``Unsupported major.minor version 51.0``
        • 类的访问标识
        • 常量池
        • Field-字段
        • Methods-方法
        • Descriptors-描述
        • attribute-属性
      • javap
    • 字节码技术应用
      • ASM
        • 主要类介绍
          • ClassVisitor
          • ClassWriter
          • ClassReader
          • Opcodes
        • 举个栗子
      • Javassisit
        • 主要类介绍
          • ClassPool
          • CtClass
          • CtMethod
          • CtBehavior
        • 再举个栗子
    • Class加载
      • ClassLoader
      • Instrument
    • 结语
    • 引申
      • 待续

字节码详解

前言

万事开头难

字节码相关内容往深了挖其实东西很多,我就按照自己学习的一个心理历程去分享一下这块儿的内容,起个抛砖引玉的作用,很多地方没有特别深入的研究,有待大家补充。

什么是字节码

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进制方式打开:
字节码详解_第1张图片

已经有点可读的样子,跟代码比起来,可读性确实不高,但这就是接下来的任务,分析这些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继续往后看八个字节,分别是00000034,我本地环境使用的是JDK1.8

class文件中看到的是16进制,把0034转为10进制的数字就是52。我用JDK1.7编译之后,如下:
字节码详解_第2张图片
主版本号对应的两个字节,根据我们本地编译版本不同也会不同。

下面是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_ClassCONSTANT_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,01CONSTANT_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中包含codeexceptionattribute_info等信息,这里主要说下code中的内容。

code数组中的内容就是方法中编译后的代码:

         0: aload_0
         1: invokespecial #10                 // Method java/lang/Object."":()V
         4: return

这个就是我们上面那个类的无参构造函数编译后的效果,那这里面的aload_0invokespecialreturn学过JVM相关知识的话,大家已经很熟悉了.

  • aload_0就是变量0进栈
  • invokespecial调用实例的初始化方法,即构造方法
  • return 即方法结束,返回值为void

那这些aload_0invokespecialreturn相关的指令是如何存储在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

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               
   #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

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, "", "()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

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."":()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
}

Class加载

上面讲到所有的内容都是Demo级别的例子,并没有从项目使用层面来分析这些技术如何使用。比如,我们修改的字节码何时加载到JVM?运行中的项目如果动态修改某个类的实现,怎么加载?

ClassLoader

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的需求。

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的开发语言,怎么搞?

  1. 定义语义,静态,动态?强类型,弱类型?
  2. 定义语法,关键字(if,else,break,return…)
  3. 定义代码编译器,如何将自己的代码编译成.class

有兴趣的大佬,可以试试

还可以继续引申,语义语法都定义好了,是不是可以实现编译器直接编译成.exe文件,或者linux下可以运行程序?

待续

  • Class加载详细过程,如JVM如何将指令生成对应代码
  • 字节码技术相关框架详解,ASM,CGLib,Javassisit,AspectJ,JDK Proxy
  • ClassLoader详解
  • Java Agent

你可能感兴趣的:(Java)