高阶函数 flask io rxjs Plupload ACE vue添加class vue滑动事件 pr序列设置哪个好 android调试工具 mysql增删改查语句 java常用的包 nikto扫描web漏洞 wordpress本地建站 css鼠标悬浮样式 java接收数组 mysql重启 python环境 python类和对象 python正则表达式语法 python入门例子 java入门 java在线学习 java语法 java的random java日期转时间戳 linux远程 qq飞车剧情辅助 dll文件下载 字幕制作软件哪个好 数科阅读器 teraterm 狮子狗皮肤 ppt格式刷怎么用 脚本学习 网页之家 微信昵称特殊字符保存 ps索引怎么解锁 su镜像 汉仪旗黑字体下载
当前位置: 首页 > 学习教程  > 编程语言

字节码详解

2020/10/8 18:18:21 文章标签:

字节码详解 前言 万事开头难 字节码相关内容往深了挖其实东西很多,我就按照自己学习的一个心理历程去分享一下这块儿的内容,起个抛砖引玉的作用,很多地方没有特别深入的研究,有待大家补充。 什么是字节码 Java作为一款“一次…

字节码详解

前言

万事开头难

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

什么是字节码

Java作为一款“一次编译,到处运行”的编程语言,跨平台靠的是JVM实现对不同操作系统API的支持,而一次编译指的就是class字节码;即我们编写好的.java文件,通过编译器编译成.class文件,JVM负责加载解释字节码文件,并生成系统可识别的代码执行(具体解析本次不做深入研究).

Class文件

The class File Format

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继续往后看八个字节,分别是00000034,我本地环境使用的是JDK1.8

class文件中看到的是16进制,把0034转为10进制的数字就是52。我用JDK1.7编译之后,如下:
在这里插入图片描述
主版本号对应的两个字节,根据我们本地编译版本不同也会不同。

下面是JDK版本与版本号对应关系:

jdk版本major.minor version
1.145
1.246
1.347
1.448
549
650
751
852

类的访问标识

访问标识类型表:

Flag NameValueInterpretation
ACC_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 TypeValue
CONSTANT_Class7
CONSTANT_Fieldref9
CONSTANT_Methodref10
CONSTANT_InterfaceMethodref11
CONSTANT_String8
CONSTANT_Integer3
CONSTANT_Float4
CONSTANT_Long5
CONSTANT_Double6
CONSTANT_NameAndType12
CONSTANT_Utf81
CONSTANT_MethodHandle15
CONSTANT_MethodType16
CONSTANT_InvokeDynamic18

不同常量对应后续字节数不同,如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 NameValueInterpretation
ACC_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后面再讲。

Methods-方法

method_info的结构如下:

method_info {
    u2             access_flags; //访问标识
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

类、字段与方法的访问标识类型都不太相同,方法的访问标识如下:

Flag NameValueInterpretation
ACC_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规范.

Descriptors-描述

方法和字段都有自己的描述信息,方法的描述包括参数、返回值的类型,字段描述为字段的类型,下面是类型表:

FieldType termTypeInterpretation
Bbytesigned byte
CcharUnicode character code point in the Basic Multilingual Plane, encoded with UTF-16
Ddoubledouble-precision floating-point value
Ffloatsingle-precision floating-point value
Iintinteger
Jlonglong integer
L ClassName ;referencean instance of class ClassName
Sshortsigned short
Zbooleantrue or false
[referenceone 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."<init>":()V
         4: return

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

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

那这些aload_0invokespecialreturn相关的指令是如何存储在code数组中的,或者说是以什么形式存在的?

其实JVM有这样一个指令数组,code数组中的记录的就是指令数组的有效下标,下面是部分指令:

CodeFormsdescribe
return0xB1当前方法返回void
areturn0xB0从方法中返回一个对象的引用
ireturn0xAC当前方法返回int
iload_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,类型及其出现位置如下:

AttributeLocation
SourceFileClassFile
InnerClassesClassFile
EnclosingMethodClassFile
SourceDebugExtensionClassFile
BootstrapMethodsClassFile
ConstantValuefield_info
Codemethod_info
Exceptionsmethod_info
RuntimeVisibleParameterAnnotations, RuntimeInvisibleParameterAnnotationsmethod_info
AnnotationDefaultmethod_info
MethodParametersmethod_info
SyntheticClassFile, field_info, method_info
DeprecatedClassFile, field_info, method_info
SignatureClassFile, field_info, method_info
RuntimeVisibleAnnotations, RuntimeInvisibleAnnotationsClassFile, field_info, method_info
LineNumberTableCode
LocalVariableTableCode
LocalVariableTypeTableCode
StackMapTableCode
RuntimeVisibleTypeAnnotations, RuntimeInvisibleTypeAnnotationsClassFile, 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

Javassisit官方文档

再举个栗子

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();
		}
	}
}

上面这个例子一定回报错,因为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();
	}

	@SuppressWarnings("unchecked")
	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);
	}

	public void unloadJarFile(String url) {
		URLConnection fileURLConnection = cachedFile;
		if (fileURLConnection == null) {
			return;
		}
		try {
			System.err.println("Unloading plugin file " + fileURLConnection.getURL().toString());
			fileURLConnection.getInputStream().close();
			cachedFile = null;
		} catch (Exception e) {
			System.err.println("Failed to unload JAR file\n" + e);
		}
	}

	/**
	 * 绕过双亲委派逻辑,直接获取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

本文链接: http://www.dtmao.cc/news_show_250043.shtml

附件下载

相关教程

    暂无相关的数据...

共有条评论 网友评论

验证码: 看不清楚?