精华内容
下载资源
问答
  • 字节码程序java-agent-and-instrument java-agent-and-instrument-test APM ,追踪, 方法增强, 热模块替代品 完毕 空中漫步查明拉链框架开发,例如:dubbo,spring, arthas,jrebel等。 连接池 连接池 德鲁伊,...
  • 初学Java,总结了三个问题——Java编程可以发展的方向,JDK是什么和Java源文件和字节码文件的关系

            Java编程能有几个发展方向?

           Java编程有三个发展方向:Java se(又称J2se,Java 2 Standard Edition),Java ee(又称J2ee,Java 2 Enterprise Edition)和Java me(又称J2me,Java 2 Platform Micro Edition)。其中Java se是做桌面开发,是这三个发展方向的中最基础的部分,而Java ee是做Web开发,是这三个发展方向最重要的部分,而Java me是做手机开发。

           JDK是什么?

          JDK全称为Java Development Kit(Java中文开发工具包),JDK是sun公司开发的,其中包括JRE(Java Runtime Environmen,Java运行环境),一堆Java工具和Java基础类库。

                                                               

           可以这样说,JDK是开发环境,而JRE是Java运行环境。假如张三是一个程序员,他要编写一个Java程序,那么他的电脑是上必须安装JDK,而若李四得到了张三的编写的Java程序,李四只需在其电脑上安装JRE就行了,无须安装JDK。

          Java源文件(以.java为后缀的文件)和Java字节码文件(以.class为后缀的文件)的区别?

         Java源文件需要经过编译器(即javac.exe)才能生成Java字节码文件,然后解释器(即java.exe)将Java字节码文件加载到JRE(Java虚拟机)中,这样Java程序就能够在装载了Java虚拟机的各种平台上运行了。其中的关系如下图:

                                     

            所以,如果你是一个程序员的话,在给客户交付Java项目时,如果你想保护你自己的Java项目代码不泄露,你只要给客户你项目的Java源文件就可以了,无须给Java项目源文件。而客户只需在一台安装有JRE计算机上就可以运行你交付的Java项目。

    展开全文
  • Java字节码

    千次阅读 2019-01-14 13:23:40
    Java最黑科技的玩法就是字节码编程,也就是动态修改或是动态生成 Java 字节码。使用字节码可以玩出很多高级的玩法,最高级的还是在 Java 程序运行时进行字节码修改和代码注入。听起来是不是一些很黑客,也很黑科技的...

    Java最黑科技的玩法就是字节码编程,也就是动态修改或是动态生成 Java 字节码。使用字节码可以玩出很多高级的玩法,最高级的还是在 Java 程序运行时进行字节码修改和代码注入。听起来是不是一些很黑客,也很黑科技的事?是的,这个方式使用 Java 这门静态语言在运行时可以进行各种动态的代码修改,而且可以进行无侵入的编程。
    比如,我们不需要在代码中埋点做统计或监控,可以使用这种技术把我们的监控代码直接以字节码的方式注入到别人的代码中,从而实现对实际程序运行情况进行统计和监控。但是要做到这个事,还需要学习一个叫 Java Agent 的技术(可以参考我的这篇文章:Java Agent)。

    1、Class类文件的结构

    根据 Java 虚拟机规范的规定,Class 文件格式采用一种类似 C 语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表
    无符号数属于基本的数据类型,以 u1、u2、u4、u8 来分别代表 1 个字节、2 个字节、4个字节和 8 个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成字符串值。
    是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个 Class 文件本质上就是一张表,它由下表所示的数据项构成。

    类型名称数量描述
    u4magic1魔数,占用4个字节,offset 0-3
    u2minor_version1次版本号,offset 4-5
    u2major_version1主版本号 ,offset 7-8
    u2constant_pool_count1常量池数量,offset 8-9
    cp_infoconstant_pollconstant_pool_count-1常量池,Class 文件之中的资源仓库。数量不固定
    u2access_flags1访问标志,用于识别一些类或者接口层次的访问信息
    u2this_class1类索引,用于确定这个类的全限定名
    u2super_class1父类索引,用于确定这个类的父类的全限定名
    u2interfaces_count1接口计数器,表示索引表的容量
    u2interfacesinterfaces_count接口索引集合,用来描述这个类实现了哪些接口
    u2fields_count1字段容量计数器,记录这个表含有多少个字段
    field_infofieldsfields_count字段表集合,用于描述接口或者类中声明的变量
    u2methods_count1方法表计数器,记录这个表含有多少个方法
    method_infomethodsmethods_count方法表集合,存放 Class 的方法集合
    u2attributes_count1属性表计数器,表示字段表或方法表有多少个属性
    attributeattributesattributes_count属性表集合,在 Class 文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息

    为方便讲解,在这里准备了一段最简单的代码,也希望大家能跟着实际操作一遍:

    package org.clazz;
    
    public class TestClazz {
        private int m;
    
        public int inc() {
            return m + 1;
        }
    }
    

    使用 javac 将这个文件转换成 Class,然后用十六进制编辑器 WinHex 打开这个 Class 文件:
    在这里插入图片描述
    有了以上的知识准备,现在我们一起分析上面的 Class 分别代表什么意思。揭开这层神秘的面纱!

    1.1、魔数

    每个 Class 文件的头 4 个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的 Class 文件。
    很多文件存储标准中都使用魔数来进行身份识别,譬如图片格式,如 gif 或者 jpeg 等在文件头中都存有魔数。
    文件格式的制定者可以自由地选择魔数值,只要这个魔数值还没有被广泛采用过同时又不会引起混淆即可。
    我们看到 TestClazz.class 的魔数也就是头 4 个字节为 CA FE BA BE,用十六进制表示是 0xCAFEBABE(咖啡宝贝?这个名称也太浪漫了吧)。这也意味着每个 Class 文件的魔数值都必须为 0xCAFEBABE。
    在这里插入图片描述

    1.2、Class 文件的版本

    紧接着魔数的 4 个字节存储的是 Class 文件的版本号:5-6 个字节是次版本号(Minor Version),7-8 个字节是主版本号(Major Version)。Java 的版本号是从 45 开始的,JDK 1.1 之后的每个JDK 大版本发布主版本号加 1,高版本的 JDK 能向下兼容以前版本的 Class 文件,但不能运行以后版本的 Class 文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过起把那本号的 Class 文件。
    Class 文件版本号:

    JDK版本号10进制版本号16进制版本号
    1.145.000 00 00 2D
    1.246.000 00 00 2E
    1.347.000 00 00 2F
    1.448.000 00 00 30
    1.549.000 00 00 31
    1.650.000 00 00 32
    1.751.000 00 00 33
    1.852.000 00 00 34

    再看看文件对应的值:
    在这里插入图片描述
    我们看到代表主版本号的 7-8 个字节的值为 0x0034,也即十进制的 52,该版本号说明这个文件是可以被 JDK 1.8 或以上版本虚拟机执行的 Class 文件。

    1.3、常量池

    紧接着主次版本号之后的是常量池入口,常量池可以理解为 Class 文件之中的资源仓库,它是占用 Class 文件空间最大的数据项目之一。
    由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项 u2 类型的数据,代表常量池容量计数值(constant_pool_count)。
    在这里插入图片描述
    如上图所示,常量池容量为十六进制数 0x0016,即十进制的 19,结合上面的 Class 表,我们能知道常量池中有 19 - 1 = 18 项常量。
    常量池容量计数值之后就是常量池,常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。
    字面量比较接近于 Java 语言层面的常量概念,如文本字符串、声明为 final 的常量值等。
    符号引用则属于编译原理方面的概念,包括了下面三类常量:

    • 类和接口的全限定名(Fully Qualified Name)
    • 字段的名称和描述符(Descriptor)
    • 方法的名称和描述符

    常量池中每一项常量都是一个表,总共 14 种表:

    类型标志描述
    CONSTANT_Utf8_info1UTF-8 编码的字符串
    CONSTANT_Integer_info3整型字面量
    CONSTANT_Float_info4浮点型字面量
    CONSTANT_Long5长整型字面量
    CONSTANT_Double_info6双精度浮点型字面量
    CONSTANT_Class_info7类或接口的符号引用
    CONSTANT_String_info8字符串类型字面量
    CONSTANT_Fieldref_info9字段的符号引用
    CONSTANT_Methidref_info10类中方法的符号引用
    CONSTANT_InterfaceMethodref_info11接口中方法的符号引用
    CONSTANT_NameAndType_info12字段或方法的部分符号引用
    CONSTANT_MethodHandle_info15表示方法句柄
    CONSTANT_MethodType_info16标识方法类型
    CONSTANT_InvokeDynamic_info18标识一个动态方法调用点

    之所以说常量池是最烦琐的数据,是因为这 14 中常量类型各自均有自己的结构。
    我们再来看图中常量池的第一项常量,它的标志位(偏移地hi:0x0000000A)是 0x0A,转换为十进制的值为 10,查常量表中对应的标志为 10 的常量属于 CONSTANT_Methodref_info 类型。
    在这里插入图片描述
    我们看一下 CONSTANT_Methodref_info 类型常量的结构:

    名称类型描述
    tagu1值为10
    indexu2指向声明方法的类描述符 CONSTANT_Class_info 的索引项
    indexu2指向名称及类型描述符 CONSTANT_NameAndType 的索引项

    上图中的第一个 index 十六进制为 0x0004,即十进制的 4,表示指向常量池中第 4 个常量。
    第二个 index 十六进制为 0x000F,即十进制的 15,表示指向常量吃中的第 15 个常量。
    (先不管第4、15 常量表示什么)
    上面分析的是第一个常量值,接着分析第二个常量值,它的标志位(地址:0x0000000F)是 0x09,即十进制的 9,表示这个常量属于 CONSTANT_Fieldref_info 类型,此常量代表字段的符号引用。
    在这里插入图片描述
    CONSTANT_Fieldref_info 型常量的结构:

    名称类型描述
    tagu1值为9
    indexu2指向声明字段的类或者接口描述符 CONSTANT_Class_info 的索引项
    indexu2指向字段描述符 CONSTANT_NameAndType 的索引项

    以上分析了 TestClazz.class 常量池中 18 个常量中的前两个,其余的 16 个依次类推:
    在这里插入图片描述
    需要注意的是第 18 个常量,tag 标志为 0x01 表示 CONSTANT_Utf8_info :

    名称类型描述
    tagu1数量 1
    lengthu2长度,表示占用几个字节
    bytesu1占用 length 个字节

    注意 bytes 字段的长度,是根据 length 计算的,length 为 0x0010 转换十进制为 16,所以后面的 bytes 占用 16 个字节。
    最后将 14 中常量项的结构定义总结为下表,供大家参考:
    在这里插入图片描述

    1.4、访问标志

    在常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口;是否定义为 public 类型;是否定义为 abstract 类型;如果是类的话,是否被声明为 final 等。具体标志位以及标志的含义见下表:

    标志名称标志值含义
    ACC_PUBLIC0x0001是否为 public 类型
    ACC_FINAL0x0010是否被声明为 final,只有类可设置
    ACC_SUPER0x0020是否允许使用 invokespecial 字节码指令的新语意,invokespecial指令的语意在 JDK 1.0.2 发生过改变,为了区别这条指令使用哪种语意,JDK 1.0.2 之后编译出来的类的这个标志都必须为真
    ACC_INTERFACE0x0200标识这是一个接口
    ACC_ABSTRACT0x0400是否为 abstract 类型,对于接口或者抽象类来说,此标志值为真,其他类值为假
    ACC_SYNTHETIC0x1000标识这个类并非由用户代码产生的
    ACC_ANNOTATION0x2000标识这是一个注解
    ACC_ENUM0x4000标识这是一个枚举

    在这里插入图片描述
    TestClazz.class 是一个普通的 java 类,不是接口、枚举,因此它的ACC_PUBLIC、ACC_SUPER标志为真,其他标志为假,因此它的 access_flags 的值为:0x0001|0x0020 = 0x0021。

    1.5、类索引、父类索引与接口索引集合

    类索引(this_class)和父类索引(super_class)都是一个 u2 类型的数据,而接口索引结合(interfaces)是一组 u2 类型的数据的集合,Class 文件中由这三项数据来确定这个类的继承关系。
    类索引和父类索引都是指向一个类型为 CONSTANT_Class_info 的类描述符常量。
    在这里插入图片描述
    图中看到,TestClazz中的类索引指向的是第 3 个常量,父类索引指向的是第 4 个常量。
    对于接口索引集合,入口的第一项——u2 类型的数据为接口计数器(interfaces_count),表示索引表的容量。如果该类没有实现任何接口,则该计数器值为 0 ,后面接口的索引表不再占用任何字节。

    1.6、字段表集合

    字段表用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例变量,但不包括在方法内部声明的局部变量。
    字段表结构:

    类型名称数量
    u2access_flags1
    u2name_index1
    u2descriptor_index1
    u2attributes_count1
    attribute_infoattributesattributes_count

    字段修饰符放在 access_flags 项目中,它与类中的 access_flags 项目是非常类似的,都是一个 u2 的数据类型,其中可以设置的标志位和含义见表:

    标志名称标志值含义
    ACC_PUBLIC0x0001字段是否为 public
    ACC_PRIVATE0x0002字段是否为 private
    ACC_PROTECTED0x0004字段是否为 proctected
    ACC_STATIC0x0008字段是否为 static
    ACC_FINAL0x0010字段是否为 final
    ACC_VOLATILE0x0040字段是否为 volatile
    ACC_TRANSIENT0x0080字段是否为 transient
    ACC_SYNTHETIC0x1000字段是否由编译器自动产生的
    ACC_ENUM0x4000字段是否为 enum

    跟随 access_flags 标志的是两项索引值:name_index 和 descriptor_index。它们都是对常量池的引用,分别代表着字段的简单名称以及字段和方法的描述符。
    在这里插入图片描述

    1.7、方法表集合

    方法表的内容和字段表几乎采用了完全一致的方式,方法表结构:

    类型名称数量
    u2access_flags1
    u2name_index1
    u2descriptor_index1
    u2attributes_count1
    attribute_infoattributesattributes_count

    access_flags 方法访问标志:

    标志名称标志值含义
    ACC_PUBLIC0x0001方法是否为 public
    ACC_PRIVATE0x0002方法是否为 private
    ACC_PROTECTED0x0004方法是否为 proctected
    ACC_STATIC0x0008方法是否为 static
    ACC_FINAL0x0010方法是否为 final
    ACC_SYNCHRONIZED0x0020方法是否为 synchronized
    ACC_BRIDGE0x0040方法是否是由编译器产生的桥接方法
    ACC_VARARGS0x0080方法是否接受不定参数
    ACC_NATIVE0x0100方法是否为 native
    ACC_ABSTRACT0x0400方法是否为 abstract
    ACC_STRICTFP0x0800方法是否为 strictfp
    ACC_SYNTHETIC0x1000方法是否由编译器自动产生的

    TestClazz对应的位置:
    在这里插入图片描述
    注意,方法表集合只存放了方法名称,索引等,方法里的代码存放在方法属性表集合中一个名为“Code”的属性里面,这就是下面需要将到的属性表集合。

    1.8、属性表集合

    属性表(attribute_info)在 Class 文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。
    与 Class 文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松些,不再要求各个属性表具有严格顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java 虚拟机运行时会忽略掉它不认识的属性。
    虚拟机规范预定义的属性:

    属性名称使用位置含义
    Code方法表Java 代码编译成的字节码指令
    ConstantValue字段表final 关键字定义的常量值
    Deprecated类、方法表、字段表被声明为 deprecated 的方法和字段
    Exceptions方法表方法抛出的异常
    EnclosingMethod类文件仅当一个类为局部类或者匿名类时才能拥有这个属性,这个属性用于标识这个类所在的外围方法
    InnerClasses类文件内部类列表
    LineNumberTableCode 属性Java 源码的行号与字节码指令的对应关系
    LocalVariableTableCode 属性方法的局部变量描述
    StackMapTableCode 属性JDK 1.6 中新增的属性,供新的类型检查验证器(Tyoe Checker)检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配
    Signature类、方法表、字段表这个属性用于支持泛型情况下的方法签名,在 Java 语言中,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则 Signature 属性会为它记录泛型签名信息。由于 Java 的泛型采用擦除法实现,在为了避免类型信息被擦除后导致签名混乱,需要这个属性记录泛型中的相关信息
    SourceFile类文件记录源文件名称
    SourceDebugExtension类文件JDK 1.6 中新增的属性,SourceDebugExtension 属性用于存储额外的调试信息。譬如在进行 JSP 文件调试时,无法通过 Java 堆栈来定位到 JSP 文件的行号,JSR-45 规范为这些非 Java 语言编写,却需要编异常字节码并运行在 Java 虚拟机中的程序提供了一个进行调试的标准机制,使用 SourceDebugExtension 属性就可以用于存储这个标准所新加入的调试信息
    Synthetic类、方法表、字段表标识方法或字段为编译器自动生成的
    LocalVariableTypeTable它使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加
    RuntimeVisibleAnnotations类、方法表、字段表为动态注解提供支持。RuntimeVisibleAnnotations 属性用于指明哪些注解是运行时(实际上运行时就是进行反射调用)可见的
    RuntimeInvisibleAnnotations类、方法表、字段表与 RuntimeVisibaleAnnotations 属性作用刚好相反,用于指明哪些注解是运行时不可见的
    RuntimeVisibleParameterAnnotations方法表作用与 RuntimeVisibleAnnotations 属性类似,只不过作用对象为方法参数
    RuntimeInvisibleParameterAnnotations方法表作用与 RuntimeInvisibleAnnotations 属性类似,只不过作用对象为方法参数
    AnnotationDefault方法表用于记录注解类元素的默认值
    BootstrapMethods类文件JDK 1.7 中新增的属性,用于保存 invokedynamic 指令引用的引导方法限定符

    对于每个属性,它的名称需要从常量池中引用一个 CONSTANT_Utf8_info 类型的常量来表示,而属性值的结构则是完全自定义的,只需要通过一个 u4 的长度属性去说明属性值所占用的位数即可,一个符合规则的属性表应该满足下表所定义的结构。
    属性表结构:

    类型名称数量
    u2attribute_name_index1
    u4attribute_length1
    u1infoattribute_length

    1.8.1、Code 属性

    Java 程序方法体中的代码经过 Javac 编译器处理后,最终变为字节码指令存储在 Code 属性内。
    Code 属性表结构:

    类型名称数量描述
    u2attibute_name_index1属性名称,指向 CONSTANT_Utf8_info 型常量的索引,常量值固定为“Code”
    u4attribute_length1属性值长度,由于属性名称索引与属性长度一共为 6 字节,所以属性值的长度固定为整个属性表长度减去 6 个字节
    u2max_stack1操作数栈深度最大值,在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机运行的时候需要根据这个值来分配栈帧中的操作栈深度
    u2max_locals1局部变量表所需的存储空间,max_locals 的单位是 Slot,Slot 是虚拟机为局部变量分配内存所使用的最小单位
    u4code_length1字节码长度
    u1codecode_length字节码指令字节流,用于存储字节码指令的一系列字节流。文章的末尾会给出“虚拟机字节码指令表”。
    u2exception_table_length1
    exception_infoexception_tableexception_table_length
    u2attributes_count1
    attribute_infoattributesattributes_count

    Code 属性是 Class 文件中最重要的一个属性,如果把一个 Java 程序中的信息分为代码(java代码)和元数据(类、字段、方法定义及其他信息)两部分,那么在整个 Class 文件中,Code 属性用于描述代码,所有其他数据项目都用于描述元数据。
    继续以 TestClazz.class 文件为例
    在这里插入图片描述
    它的操作数栈的最大深度和本地变量表的容量都为 0x0001,字节码区域所占空间的长度为 0x0005。
    虚拟机读取到字节码长度后,按照顺序依次读入紧随的 5 个字节,并根据字节码指令表翻译出所对应的字节码指令。
    翻译 “2A B7 00 0A B1” 的过程:
    1.读入 2A,查表得 0x2A 对应得指令为 aload_0,这个指令得含义是将第 0 个 Slot 中为 reference 类型得本地变量推送到操作数栈顶。
    2.读入 B7,查表得 0xB7 对应得指令为 invokespecial,这条指令的作用是以栈顶的 reference 类型的数据所指向的对象作为方法接收者,调用此对象的实例构造器方法、private 方法或者它的父类的方法。
    3.读入 00 0A,这是 invokespecial 的参数,查常量池得 0x000A 对应的常量为实例构造器“”方法的符号引用。
    4.读入 B1,查表得 0xB1 对应得指令为 return,含义是返回此方法,这条指令执行后,当前方法结束。
    属性表集合除了 Code 属性,还有 Exceptions 属性、LineNumberTable 属性等等,这里就不一一介绍了。有兴趣得童鞋可以自行了解。

    2、javap 工具分析 Class

    在 JDK 的 bin 目录中,Oracle 公司已经为我们准备好一个专门用于分析 Class 文件字节码的工具:javap
    使用命令:

    javap -verbose TestClazz.class
    

    代码清单:

     Last modified 2019-1-14; size 285 bytes
      MD5 checksum c434da45f0fff84f21348a725448f2f5
      Compiled from "TestClazz.java"
    public class org.clazz.TestClazz
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
       #1 = Methodref          #4.#15         // java/lang/Object."<init>":()V
       #2 = Fieldref           #3.#16         // org/clazz/TestClazz.m:I
       #3 = Class              #17            // org/clazz/TestClazz
       #4 = Class              #18            // java/lang/Object
       #5 = Utf8               m
       #6 = Utf8               I
       #7 = Utf8               <init>
       #8 = Utf8               ()V
       #9 = Utf8               Code
      #10 = Utf8               LineNumberTable
      #11 = Utf8               inc
      #12 = Utf8               ()I
      #13 = Utf8               SourceFile
      #14 = Utf8               TestClazz.java
      #15 = NameAndType        #7:#8          // "<init>":()V
      #16 = NameAndType        #5:#6          // m:I
      #17 = Utf8               org/clazz/TestClazz
      #18 = Utf8               java/lang/Object
    {
      public org.clazz.TestClazz();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 3: 0
    
      public int inc();
        descriptor: ()I
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=1, args_size=1
             0: aload_0
             1: getfield      #2                  // Field m:I
             4: iconst_1
             5: iadd
             6: ireturn
          LineNumberTable:
            line 7: 0
    }
    SourceFile: "TestClazz.java"
    

    结束

    到此,相信大家能对字节码有一个较深的认识,Java 语言中的各种变量、关键字和运算符号的语义最终都是由多条字节码命令组合而成的,因此字节码命令所能提供的语义描述能力肯定会比 Java 语言本身更强大。

    附录

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    展开全文
  • 小傅哥的《字节码编程》- 文章涉及源码 前言 初识字节码编程是从使用非入侵的全链路监控开始,在这之前我所了解的如果需要监控系统的运行状况,通常需要硬编码埋点或者AOP的方式采集方法执行信息;耗时、异常、出入...
  • JavaAgent Javassist 与 Asm JavaAgent 字节码动态编程项目
  • 字节码编程,提供了字节码的基础知识和操作系统等知识,学习完成后可能对字节码有一个很好的认识,在java中很多的一些也能用到字节码的知识
  •  ASM是一个java字节码操纵框架,它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .

             原文链接:http://blog.csdn.net/qq_27376871/article/details/51613066


    一、什么是ASM


      ASM是一个java字节码操纵框架,它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。asm字节码增强技术主要是用来反射的时候提升性能的,如果单纯用jdk的反射调用,性能是非常低下的,而使用字节码增强技术后反射调用的时间已经基本可以与直接调用相当了

      使用ASM框架需要导入asm的jar包,下载链接:asm-3.2.jar


    二、如何使用ASM


      ASM框架中的核心类有以下几个:

      ①  ClassReader:该类用来解析编译过的class字节码文件。

      ②  ClassWriter:该类用来重新构建编译后的类,比如说修改类名、属性以及方法,甚至可以生成新的类的字节码文件。

      ③  ClassAdapter:该类也实现了ClassVisitor接口,它将对它的方法调用委托给另一个ClassVisitor对象。


    三、 ASM字节码处理框架是用Java开发的而且使用基于访问者模式生成字节码及驱动类到字节码的转换。


          通俗的讲,它就是对class文件的CRUD,经过CRUD后的字节码可以转换为类。ASM的解析方式类似于SAX解析XML文件,它综合运用了访问者模式、职责链模式、桥接模式等多种设计模式,相对于其他类似工具如BCEL、SERP、Javassist、CGLIB,它的最大的优势就在于其性能更高,其jar包仅30K。Hibernate和Spring都使用了cglib代理,而cglib本身就是使用的ASM,可见ASM在各种开源框架都有广泛的应用。
       ASM是一个强大的框架,利用它我们可以做到:
       1、获得class文件的详细信息,包括类名、父类名、接口、成员名、方法名、方法参数名、局部变量名、元数据等
       2、对class文件进行动态修改,如增加、删除、修改类方法、在某个方法中添加指令等

       3、CGLIB(动态代理)是对ASM的封装,简化了ASM的操作,降低了ASM的使用门槛,

       其中,hibernate的懒加载使用到了asm,spring的AOP也使用到了。你建立一个hibernate映射对象并使用懒加载配置的时候,在内存中生成的对象使用的不再是你实现的那个类了,而是hibernate根据字节码技术已你的类为模板构造的一个新类,证明就是当你获得那个对象输出类名是,不是你自己生成的类名了。spring可能是proxy$xxx,hibernate可能是<你的类名>$xxx$xxx之类的名字。


     AOP 的利器:ASM 3.0 介绍


           随着 AOP(Aspect Oriented Programming)的发展,代码动态生成已然成为 Java 世界中不可或缺的一环。本文将介绍一种小巧轻便的 Java 字节码操控框架 ASM,它能方便地生成和改造 Java 代码。著名的框架,如 Hibernate 和 Spring 在底层都用到了 ASM。比起传统的 Java 字节码操控框架,BCEL 或者 SERP,它具有更符合现代软件模式的编程模型和更迅捷的性能。

    本文主要分为四个部分:首先将 ASM 和其他 Java 类生成方案作对比,然后大致介绍 Java 类文件的组织,最后针对最新的 ASM 3.0,描述其编程框架,并给出一个使用 ASM 进行 AOP 的例子,介绍调整函数内容,生成派生类,以及静态和动态生成类的方法


       引言

    什么是 ASM ?


    ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。

    与 BCEL 和 SERL 不同,ASM 提供了更为现代的编程模型。对于 ASM 来说,Java class 被描述为一棵树;使用 “Visitor” 模式遍历整个二进制结构;事件驱动的处理方式使得用户只需要关注于对其编程有意义的部分,而不必了解 Java 类文件格式的所有细节:ASM 框架提供了默认的 “response taker”处理这一切。


    为什么要动态生成 Java 类?


    动态生成 Java 类与 AOP 密切相关的。AOP 的初衷在于软件设计世界中存在这么一类代码,零散而又耦合:零散是由于一些公有的功能(诸如著名的 log 例子)分散在所有模块之中;同时改变 log 功能又会影响到所有的模块。出现这样的缺陷,很大程度上是由于传统的 面向对象编程注重以继承关系为代表的“纵向”关系,而对于拥有相同功能或者说方面 (Aspect)的模块之间的“横向”关系不能很好地表达。例如,目前有一个既有的银行管理系统,包括 Bank、Customer、Account、Invoice 等对象,现在要加入一个安全检查模块, 对已有类的所有操作之前都必须进行一次安全检查。


    图 1. ASM – AOP


          然而 Bank、Customer、Account、Invoice 是代表不同的事务,派生自不同的父类,很难在高层上加入关于 Security Checker 的共有功能。对于没有多继承的 Java 来说,更是如此。传统的解决方案是使用 Decorator 模式,它可以在一定程度上改善耦合,而功能仍旧是分散的 —— 每个需要 Security Checker 的类都必须要派生一个 Decorator,每个需要 Security Checker 的方法都要被包装(wrap)。下面我们以Account类为例看一下 Decorator:

    首先,我们有一个 SecurityChecker类,其静态方法 checkSecurity执行安全检查功能:

    public class SecurityChecker { 
    	 public static void checkSecurity() { 
    		 System.out.println("SecurityChecker.checkSecurity ..."); 
    		 //TODO real security check 
    	 } 	
     }


    另一个是 Account类:


    public class Account { 
    	 public void operation() { 
    		 System.out.println("operation..."); 
    		 //TODO real operation 
    	 } 
     }

    若想对  operation 加入对  SecurityCheck.checkSecurity() 调用,标准的 Decorator 需要先定义一个 Account 类的接口:


    public interface Account { 
    	 void operation(); 
     }

    然后把原来的  Account 类定义为一个实现类:


    <pre name="code" class="java">public class AccountImpl extends Account{ 
    	 public void operation() { 
    		 System.out.println("operation..."); 
    		 //TODO real operation 
    	 } 
     }

     
    

    定义一个 Account类的 Decorator,并包装 operation方法:

    public class AccountWithSecurityCheck implements Account { 	
    	 private  Account account; 
    	 public AccountWithSecurityCheck (Account account) { 
    		 this.account = account; 
    	 } 
    	 public void operation() { 
    		 SecurityChecker.checkSecurity(); 
    		 account.operation(); 
    	 } 
     }

    这个简单的例子里,改造一个类的一个方法还好,如果是变动整个模块,Decorator 很快就会演化成另一个噩梦。动态改变 Java 类就是要解决 AOP 的问题,提供一种得到系统支持的可编程的方法,自动化地生成或者增强 Java 代码。这种技术已经广泛应用于最新的 Java 框架内,如 Hibernate,Spring 等。


    为什么选择 ASM ?


          最直接的改造 Java 类的方法莫过于直接改写 class 文件。Java 规范详细说明了 class 文件的格式,直接编辑字节码确实可以改变 Java 类的行为。直到今天,还有一些 Java 高手们使用最原始的工具,如 UltraEdit 这样的编辑器对 class 文件动手术。是的,这是最直接的方法,但是要求使用者对 Java class 文件的格式了熟于心:小心地推算出想改造的函数相对文件首部的偏移量,同时重新计算 class 文件的校验码以通过 Java 虚拟机的安全机制。

    Java 5 中提供的 Instrument 包也可以提供类似的功能:启动时往 Java 虚拟机中挂上一个用户定义的 hook 程序,可以在装入特定类的时候改变特定类的字节码,从而改变该类的行为。但是其缺点也是明显的:

    • Instrument 包是在整个虚拟机上挂了一个钩子程序,每次装入一个新类的时候,都必须执行一遍这段程序,即使这个类不需要改变。
    • 直接改变字节码事实上类似于直接改写 class 文件,无论是调用 ClassFileTransformer. transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer),还是Instrument.redefineClasses(ClassDefinition[] definitions),都必须提供新 Java 类的字节码。也就是说,同直接改写 class 文件一样,使用 Instrument 也必须了解想改造的方法相对类首部的偏移量,才能在适当的位置上插入新的代码。

          尽管 Instrument 可以改造类,但事实上,Instrument 更适用于监控和控制虚拟机的行为。

    一种比较理想且流行的方法是使用 java.lang.ref.proxy。我们仍旧使用上面的例子,给 Account类加上 checkSecurity 功能 :

    首先,Proxy 编程是面向接口的。下面我们会看到,Proxy 并不负责实例化对象,和 Decorator 模式一样,要把 Account定义成一个接口,然后在AccountImpl里实现Account接口,接着实现一个 InvocationHandlerAccount方法被调用的时候,虚拟机都会实际调用这个InvocationHandlerinvoke方法:


    lass SecurityProxyInvocationHandler implements InvocationHandler { 
    	 private Object proxyedObject; 
    	 public SecurityProxyInvocationHandler(Object o) { 
    		 proxyedObject = o; 
    	 } 
    		
    	 public Object invoke(Object object, Method method, Object[] arguments) 
    		 throws Throwable { 			
    		 if (object instanceof Account && method.getName().equals("opertaion")) { 
    			 SecurityChecker.checkSecurity(); 
    		 } 
    		 return method.invoke(proxyedObject, arguments); 
    	 } 
     }

    最后,在应用程序中指定  InvocationHandler 生成代理对象:


    public static void main(String[] args) { 
    	 Account account = (Account) Proxy.newProxyInstance( 
    		 Account.class.getClassLoader(), 
    		 new Class[] { Account.class }, 
    		 new SecurityProxyInvocationHandler(new AccountImpl()) 
    	 ); 
    	 account.function(); 
     }



    其不足之处在于:

    • Proxy 是面向接口的,所有使用 Proxy 的对象都必须定义一个接口,而且用这些对象的代码也必须是对接口编程的:Proxy 生成的对象是接口一致的而不是对象一致的:例子中Proxy.newProxyInstance生成的是实现Account接口的对象而不是 AccountImpl的子类。这对于软件架构设计,尤其对于既有软件系统是有一定掣肘的。
    • Proxy 毕竟是通过反射实现的,必须在效率上付出代价:有实验数据表明,调用反射比一般的函数开销至少要大 10 倍。而且,从程序实现上可以看出,对 proxy class 的所有方法调用都要通过使用反射的 invoke 方法。因此,对于性能关键的应用,使用 proxy class 是需要精心考虑的,以避免反射成为整个应用的瓶颈。

          ASM 能够通过改造既有类,直接生成需要的代码。增强的代码是硬编码在新生成的类文件内部的,没有反射带来性能上的付出。同时,ASM 与 Proxy 编程不同,不需要为增强代码而新定义一个接口,生成的代码可以覆盖原来的类,或者是原始类的子类。它是一个普通的 Java 类而不是 proxy 类,甚至可以在应用程序的类框架中拥有自己的位置,派生自己的子类。

    相比于其他流行的 Java 字节码操纵工具,ASM 更小更快。ASM 具有类似于 BCEL 或者 SERP 的功能,而只有 33k 大小,而后者分别有 350k 和 150k。同时,同样类转换的负载,如果 ASM 是 60% 的话,BCEL 需要 700%,而 SERP 需要 1100% 或者更多。

    ASM 已经被广泛应用于一系列 Java 项目:AspectWerkz、AspectJ、BEA WebLogic、IBM AUS、OracleBerkleyDB、Oracle TopLink、Terracotta、RIFE、EclipseME、Proactive、Speedo、Fractal、EasyBeans、BeanShell、Groovy、Jamaica、CGLIB、dynaop、Cobertura、JDBCPersistence、JiP、SonarJ、Substance L&F、Retrotranslator 等。Hibernate 和 Spring 也通过 cglib,另一个更高层一些的自动代码生成工具使用了 ASM。


    Java 类文件概述


    所谓 Java 类文件,就是通常用 javac 编译器产生的 .class 文件。这些文件具有严格定义的格式。为了更好的理解 ASM,首先对 Java 类文件格式作一点简单的介绍。Java 源文件经过 javac 编译器编译之后,将会生成对应的二进制文件(如下图所示)。每个合法的 Java 类文件都具备精确的定义,而正是这种精确的定义,才使得 Java 虚拟机得以正确读取和解释所有的 Java 类文件。



    图 2. ASM – Javac 流程


          Java 类文件是 8 位字节的二进制流。数据项按顺序存储在 class 文件中,相邻的项之间没有间隔,这使得 class 文件变得紧凑,减少存储空间。在 Java 类文件中包含了许多大小不同的项,由于每一项的结构都有严格规定,这使得 class 文件能够从头到尾被顺利地解析。下面让我们来看一下 Java 类文件的内部结构,以便对此有个大致的认识。

    例如,一个最简单的 Hello World 程序:


    public class HelloWorld { 
    	 public static void main(String[] args) { 
    		 System.out.println("Hello world"); 
    	 } 
     }


    经过 javac 编译后,得到的类文件大致是:





    从上图中可以看到,一个 Java 类文件大致可以归为 10 个项:

    • Magic:该项存放了一个 Java 类文件的魔数(magic number)和版本信息。一个 Java 类文件的前 4 个字节被称为它的魔数。每个正确的 Java 类文件都是以 0xCAFEBABE 开头的,这样保证了 Java 虚拟机能很轻松的分辨出 Java 文件和非 Java 文件。
    • Version:该项存放了 Java 类文件的版本信息,它对于一个 Java 文件具有重要的意义。因为 Java 技术一直在发展,所以类文件的格式也处在不断变化之中。类文件的版本信息让虚拟机知道如何去读取并处理该类文件。
    • Constant Pool:该项存放了类中各种文字字符串、类名、方法名和接口名称、final 变量以及对外部类的引用信息等常量。虚拟机必须为每一个被装载的类维护一个常量池,常量池中存储了相应类型所用到的所有类型、字段和方法的符号引用,因此它在 Java 的动态链接中起到了核心的作用。常量池的大小平均占到了整个类大小的 60% 左右。
    • Access_flag:该项指明了该文件中定义的是类还是接口(一个 class 文件中只能有一个类或接口),同时还指名了类或接口的访问标志,如 public,private, abstract 等信息。
    • This Class:指向表示该类全限定名称的字符串常量的指针。
    • Super Class:指向表示父类全限定名称的字符串常量的指针。
    • Interfaces:一个指针数组,存放了该类或父类实现的所有接口名称的字符串常量的指针。以上三项所指向的常量,特别是前两项,在我们用 ASM 从已有类派生新类时一般需要修改:将类名称改为子类名称;将父类改为派生前的类名称;如果有必要,增加新的实现接口。
    • Fields:该项对类或接口中声明的字段进行了细致的描述。需要注意的是,fields 列表中仅列出了本类或接口中的字段,并不包括从超类和父接口继承而来的字段。
    • Methods:该项对类或接口中声明的方法进行了细致的描述。例如方法的名称、参数和返回值类型等。需要注意的是,methods 列表里仅存放了本类或本接口中的方法,并不包括从超类和父接口继承而来的方法。使用 ASM 进行 AOP 编程,通常是通过调整 Method 中的指令来实现的。
    • Class attributes:该项存放了在该文件中类或接口所定义的属性的基本信息。

    事实上,使用 ASM 动态生成类,不需要像早年的 class hacker 一样,熟知 class 文件的每一段,以及它们的功能、长度、偏移量以及编码方式。ASM 会给我们照顾好这一切的,我们只要告诉 ASM 要改动什么就可以了 —— 当然,我们首先得知道要改什么:对类文件格式了解的越多,我们就能更好地使用 ASM 这个利器。


    ASM 3.0 编程框架


          ASM 通过树这种数据结构来表示复杂的字节码结构,并利用 Push 模型来对树进行遍历,在遍历过程中对字节码进行修改。所谓的 Push 模型类似于简单的 Visitor 设计模式,因为需要处理字节码结构是固定的,所以不需要专门抽象出一种 Vistable 接口,而只需要提供 Visitor 接口。所谓 Visitor 模式和 Iterator 模式有点类似,它们都被用来遍历一些复杂的数据结构。Visitor 相当于用户派出的代表,深入到算法内部,由算法安排访问行程。Visitor 代表可以更换,但对算法流程无法干涉,因此是被动的,这也是它和 Iterator 模式由用户主动调遣算法方式的最大的区别。

    在 ASM 中,提供了一个 ClassReader类,这个类可以直接由字节数组或由 class 文件间接的获得字节码数据,它能正确的分析字节码,构建出抽象的树在内存中表示字节码。它会调用accept方法,这个方法接受一个实现了ClassVisitor接口的对象实例作为参数,然后依次调用ClassVisitor接口的各个方法。字节码空间上的偏移被转换成 visit 事件时间上调用的先后,所谓 visit 事件是指对各种不同 visit 函数的调用,ClassReader知道如何调用各种 visit 函数。在这个过程中用户无法对操作进行干涉,所以遍历的算法是确定的,用户可以做的是提供不同的 Visitor 来对字节码树进行不同的修改。ClassVisitor会产生一些子过程,比如visitMethod会返回一个实现MethordVisitor接口的实例,visitField会返回一个实现FieldVisitor接口的实例,完成子过程后控制返回到父过程,继续访问下一节点。因此对于ClassReader来说,其内部顺序访问是有一定要求的。实际上用户还可以不通过ClassReader类,自行手工控制这个流程,只要按照一定的顺序,各个 visit 事件被先后正确的调用,最后就能生成可以被正确加载的字节码。当然获得更大灵活性的同时也加大了调整字节码的复杂度。

    各个 ClassVisitor通过职责链 (Chain-of-responsibility) 模式,可以非常简单的封装对字节码的各种修改,而无须关注字节码的字节偏移,因为这些实现细节对于用户都被隐藏了,用户要做的只是覆写相应的 visit 函数。

    ClassAdaptor类实现了 ClassVisitor接口所定义的所有函数,当新建一个 ClassAdaptor对象的时候,需要传入一个实现了 ClassVisitor接口的对象,作为职责链中的下一个访问者 (Visitor),这些函数的默认实现就是简单的把调用委派给这个对象,然后依次传递下去形成职责链。当用户需要对字节码进行调整时,只需从ClassAdaptor类派生出一个子类,覆写需要修改的方法,完成相应功能后再把调用传递下去。这样,用户无需考虑字节偏移,就可以很方便的控制字节码。

    每个 ClassAdaptor类的派生类可以仅封装单一功能,比如删除某函数、修改字段可见性等等,然后再加入到职责链中,这样耦合更小,重用的概率也更大,但代价是产生很多小对象,而且职责链的层次太长的话也会加大系统调用的开销,用户需要在低耦合和高效率之间作出权衡。用户可以通过控制职责链中 visit 事件的过程,对类文件进行如下操作:

    1. 删除类的字段、方法、指令:只需在职责链传递过程中中断委派,不访问相应的 visit 方法即可,比如删除方法时只需直接返回 null,而不是返回由visitMethod方法返回的MethodVisitor对象。

    class DelLoginClassAdapter extends ClassAdapter { 
    	 public DelLoginClassAdapter(ClassVisitor cv) { 
    		 super(cv); 
    	 } 
    
    	 public MethodVisitor visitMethod(final int access, final String name, 
    		 final String desc, final String signature, final String[] exceptions) { 
    		 if (name.equals("login")) { 
    			 return null; 
    		 } 
    		 return cv.visitMethod(access, name, desc, signature, exceptions); 
    	 } 
     }


    2、修改类、字段、方法的名字或修饰符:在职责链传递过程中替换调用参数。


     class AccessClassAdapter extends ClassAdapter { 
    	 public AccessClassAdapter(ClassVisitor cv) { 
    		 super(cv); 
    	 } 
    
    	 public FieldVisitor visitField(final int access, final String name, 
            final String desc, final String signature, final Object value) { 
            int privateAccess = Opcodes.ACC_PRIVATE; 
            return cv.visitField(privateAccess, name, desc, signature, value); 
        } 
     }

    3、 增加新的类、方法、字段


    ASM 的最终的目的是生成可以被正常装载的 class 文件,因此其框架结构为客户提供了一个生成字节码的工具类 —— ClassWriter。它实现了ClassVisitor接口,而且含有一个toByteArray()函数,返回生成的字节码的字节流,将字节流写回文件即可生产调整后的 class 文件。一般它都作为职责链的终点,把所有 visit 事件的先后调用(时间上的先后),最终转换成字节码的位置的调整(空间上的前后),如下例:

    ClassWriter  classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS); 
     ClassAdaptor delLoginClassAdaptor = new DelLoginClassAdapter(classWriter); 
     ClassAdaptor accessClassAdaptor = new AccessClassAdaptor(delLoginClassAdaptor); 
    	
     ClassReader classReader = new ClassReader(strFileName); 
     classReader.accept(classAdapter, ClassReader.SKIP_DEBUG);

    综上所述,ASM 的时序图如下:


    图 4. ASM – 时序图

    使用 ASM3.0 进行 AOP 编程


          我们还是用上面的例子,给 Account类加上 security check 的功能。与 proxy 编程不同,ASM 不需要将 Account声明成接口,Account可以仍旧是一个实现类。ASM 将直接在 Account类上动手术,给Account类的operation方法首部加上对 SecurityChecker.checkSecurity的调用。

    首先,我们将从 ClassAdapter继承一个类。ClassAdapter是 ASM 框架提供的一个默认类,负责沟通ClassReaderClassWriter。如果想要改变ClassReader处读入的类,然后从ClassWriter处输出,可以重写相应的ClassAdapter函数。这里,为了改变 Account类的operation 方法,我们将重写visitMethdod方法。

    class AddSecurityCheckClassAdapter extends ClassAdapter {
    
        public AddSecurityCheckClassAdapter(ClassVisitor cv) {
            //Responsechain 的下一个 ClassVisitor,这里我们将传入 ClassWriter,
            // 负责改写后代码的输出
            super(cv); 
        } 
        
        // 重写 visitMethod,访问到 "operation" 方法时,
        // 给出自定义 MethodVisitor,实际改写方法内容
        public MethodVisitor visitMethod(final int access, final String name, 
            final String desc, final String signature, final String[] exceptions) { 
            MethodVisitor mv = cv.visitMethod(access, name, desc, signature,exceptions);
            MethodVisitor wrappedMv = mv; 
            if (mv != null) { 
                // 对于 "operation" 方法
                if (name.equals("operation")) { 
                    // 使用自定义 MethodVisitor,实际改写方法内容
                    wrappedMv = new AddSecurityCheckMethodAdapter(mv); 
                } 
            } 
            return wrappedMv; 
        } 
    }


    下一步就是定义一个继承自 MethodAdapter的 AddSecurityCheckMethodAdapter,在“operation”方法首部插入对SecurityChecker.checkSecurity()的调用。


    class AddSecurityCheckMethodAdapter extends MethodAdapter { 
    	 public AddSecurityCheckMethodAdapter(MethodVisitor mv) { 
    		 super(mv); 
    	 } 
    
    	 public void visitCode() { 
    		 visitMethodInsn(Opcodes.INVOKESTATIC, "SecurityChecker", 
    			"checkSecurity", "()V"); 
    	 } 
     }


    其中,ClassReader读到每个方法的首部时调用 visitCode(),在这个重写方法里,我们用 visitMethodInsn(Opcodes.INVOKESTATIC, "SecurityChecker","checkSecurity", "()V");插入了安全检查功能。

    最后,我们将集成上面定义的 ClassAdapterClassReader和 ClassWriter产生修改后的Account类文件 :

    <span style="font-family: "microsoft yahei"; font-size: 15px;">i</span>mport java.io.File; 
     import java.io.FileOutputStream; 
     import org.objectweb.asm.*; 
        
     public class Generator{ 
    	 public static void main() throws Exception { 
    		 ClassReader cr = new ClassReader("Account"); 
    		 ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); 
    		 ClassAdapter classAdapter = new AddSecurityCheckClassAdapter(cw); 
    		 cr.accept(classAdapter, ClassReader.SKIP_DEBUG); 
    		 byte[] data = cw.toByteArray(); 
    		 File file = new File("Account.class"); 
    		 FileOutputStream fout = new FileOutputStream(file); 
    		 fout.write(data); 
    		 fout.close(); 
    	 } 
     }<span style="font-family:microsoft yahei;"><span style="font-size: 15px;">
    执行完这段程序后,我们会得到一个新的 Account.class 文件,如果我们使用下面代码:</span></span>
    <span style="font-family:microsoft yahei;"><span style="font-size: 15px;">
     </span></span>public class Main { 
    	 public static void main(String[] args) { 
    		 Account account = new Account(); 
    		 account.operation(); 
    	 } 
     }

    使用这个 Account,我们会得到下面的输出:

    SecurityChecker.checkSecurity ... 
     operation...

    也就是说,在 Account原来的 operation内容执行之前,进行了 SecurityChecker.checkSecurity()检查。


    将动态生成类改造成原始类 Account 的子类


    上面给出的例子是直接改造 Account类本身的,从此 Account类的 operation方法必须进行 checkSecurity 检查。但事实上,我们有时仍希望保留原来的Account类,因此把生成类定义为原始类的子类是更符合 AOP 原则的做法。下面介绍如何将改造后的类定义为Account的子类Account$EnhancedByASM。其中主要有两项工作 :

    • 改变 Class Description, 将其命名为 Account$EnhancedByASM,将其父类指定为 Account
    • 改变构造函数,将其中对父类构造函数的调用转换为对 Account构造函数的调用。

    在 AddSecurityCheckClassAdapter类中,将重写 visit方法:


    public void visit(final int version, final int access, final String name, 
    		 final String signature, final String superName, 
    		 final String[] interfaces) { 
    	 String enhancedName = name + "$EnhancedByASM";  // 改变类命名
    	 enhancedSuperName = name; // 改变父类,这里是”Account”
    	 super.visit(version, access, enhancedName, signature, 
    	 enhancedSuperName, interfaces); 
     }

    改进  visitMethod 方法,增加对构造函数的处理:


    public MethodVisitor visitMethod(final int access, final String name, 
    	 final String desc, final String signature, final String[] exceptions) { 
    	 MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions); 
    	 MethodVisitor wrappedMv = mv; 
    	 if (mv != null) { 
    		 if (name.equals("operation")) { 
    			 wrappedMv = new AddSecurityCheckMethodAdapter(mv); 
    		 } else if (name.equals("<init>")) { 
    			 wrappedMv = new ChangeToChildConstructorMethodAdapter(mv, 
    				 enhancedSuperName); 
    		 } 
    	 } 
    	 return wrappedMv; 
     }


    这里  ChangeToChildConstructorMethodAdapter 将负责把  Account 的构造函数改造成其子类 Account$EnhancedByASM 的构造函数:


    class ChangeToChildConstructorMethodAdapter extends MethodAdapter { 
    	 private String superClassName; 
    
    	 public ChangeToChildConstructorMethodAdapter(MethodVisitor mv, 
    		 String superClassName) { 
    		 super(mv); 
    		 this.superClassName = superClassName; 
    	 } 
    
    	 public void visitMethodInsn(int opcode, String owner, String name, 
    		 String desc) { 
    		 // 调用父类的构造函数时
    		 if (opcode == Opcodes.INVOKESPECIAL && name.equals("<init>")) { 
    			 owner = superClassName; 
    		 } 
    		 super.visitMethodInsn(opcode, owner, name, desc);// 改写父类为 superClassName 
    	 } 
     }

    最后演示一下如何在运行时产生并装入产生的  Account$EnhancedByASM 。 我们定义一个  Util  类,作为一个类工厂负责产生有安全检查的 Account 类:

    public class SecureAccountGenerator { 
    
        private static AccountGeneratorClassLoader classLoader = 
            new AccountGeneratorClassLoade(); 
        
        private static Class secureAccountClass; 
        
        public Account generateSecureAccount() throws ClassFormatError, 
            InstantiationException, IllegalAccessException { 
            if (null == secureAccountClass) {            
                ClassReader cr = new ClassReader("Account"); 
                ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); 
                ClassAdapter classAdapter = new AddSecurityCheckClassAdapter(cw);
                cr.accept(classAdapter, ClassReader.SKIP_DEBUG); 
                byte[] data = cw.toByteArray(); 
                secureAccountClass = classLoader.defineClassFromClassFile( 
                   "Account$EnhancedByASM",data); 
            } 
            return (Account) secureAccountClass.newInstance(); 
        } 
        
        private static class AccountGeneratorClassLoader extends ClassLoader {
            public Class defineClassFromClassFile(String className, 
                byte[] classFile) throws ClassFormatError { 
                return defineClass("Account$EnhancedByASM", classFile, 0, 
    	        classFile.length());
            } 
        } 
    }

    静态方法 SecureAccountGenerator.generateSecureAccount()在运行时动态生成一个加上了安全检查的Account子类。著名的 Hibernate 和 Spring 框架,就是使用这种技术实现了 AOP 的“无损注入”。

    小结

    最后,我们比较一下 ASM 和其他实现 AOP 的底层技术:


    表 1. AOP 底层技术比较

    AOP 底层技术功能性能面向接口编程编程难度
    直接改写 class 文件完全控制类无明显性能代价不要求高,要求对 class 文件结构和 Java 字节码有深刻了解
    JDK Instrument完全控制类无论是否改写,每个类装入时都要执行 hook 程序不要求高,要求对 class 文件结构和 Java 字节码有深刻了解
    JDK Proxy只能改写 method反射引入性能代价要求
    ASM几乎能完全控制类无明显性能代价不要求中,能操纵需要改写部分的 Java 字节码




    展开全文
  • Java字节码增强探秘

    千次阅读 2019-09-10 14:03:58
    1.字节码 ...因此,也可以看出字节码对于Java生态的重要性。之所以被称之为字节码,是因为字节码文件由十六进制值组成,而JVM以两个十六进制值为一组,即以字节为单位进行读取。在Java中一般是用ja...

    1.字节码

    1.1 什么是字节码?

    Java之所以可以“一次编译,到处运行”,一是因为JVM针对各种操作系统、平台都进行了定制,二是因为无论在什么平台,都可以编译生成固定格式的字节码(.class文件)供JVM使用。因此,也可以看出字节码对于Java生态的重要性。之所以被称之为字节码,是因为字节码文件由十六进制值组成,而JVM以两个十六进制值为一组,即以字节为单位进行读取。在Java中一般是用javac命令编译源代码为字节码文件,一个.java文件从编译到运行的示例如图1所示。

    图1 Java运行示意图

    对于开发人员,了解字节码可以更准确、直观地理解Java语言中更深层次的东西,比如通过字节码,可以很直观地看到Volatile关键字如何在字节码上生效。另外,字节码增强技术在Spring AOP、各种ORM框架、热部署中的应用屡见不鲜,深入理解其原理对于我们来说大有裨益。除此之外,由于JVM规范的存在,只要最终可以生成符合规范的字节码就可以在JVM上运行,因此这就给了各种运行在JVM上的语言(如Scala、Groovy、Kotlin)一种契机,可以扩展Java所没有的特性或者实现各种语法糖。理解字节码后再学习这些语言,可以“逆流而上”,从字节码视角看它的设计思路,学习起来也“易如反掌”。

    本文重点着眼于字节码增强技术,从字节码开始逐层向上,由JVM字节码操作集合到Java中操作字节码的框架,再到我们熟悉的各类框架原理及应用,也都会一一进行介绍。

    1.2 字节码结构

    .java文件通过javac编译后将得到一个.class文件,比如编写一个简单的ByteCodeDemo类,如下图2的左侧部分:

    图2 示例代码(左侧)及对应的字节码(右侧)

    编译后生成ByteCodeDemo.class文件,打开后是一堆十六进制数,按字节为单位进行分割后展示如图2右侧部分所示。上文提及过,JVM对于字节码是有规范要求的,那么看似杂乱的十六进制符合什么结构呢?JVM规范要求每一个字节码文件都要由十部分按照固定的顺序组成,整体结构如图3所示。接下来我们将一一介绍这十部分:

    图3 JVM规定的字节码结构

    (1) 魔数(Magic Number)

    所有的.class文件的前四个字节都是魔数,魔数的固定值为:0xCAFEBABE。魔数放在文件开头,JVM可以根据文件的开头来判断这个文件是否可能是一个.class文件,如果是,才会继续进行之后的操作。

    有趣的是,魔数的固定值是Java之父James Gosling制定的,为CafeBabe(咖啡宝贝),而Java的图标为一杯咖啡。

    (2) 版本号

    版本号为魔数之后的4个字节,前两个字节表示次版本号(Minor Version),后两个字节表示主版本号(Major Version)。上图2中版本号为“00 00 00 34”,次版本号转化为十进制为0,主版本号转化为十进制为52,在Oracle官网中查询序号52对应的主版本号为1.8,所以编译该文件的Java版本号为1.8.0。

    (3) 常量池(Constant Pool)

    紧接着主版本号之后的字节为常量池入口。常量池中存储两类常量:字面量与符号引用。字面量为代码中声明为Final的常量值,符号引用如类和接口的全局限定名、字段的名称和描述符、方法的名称和描述符。常量池整体上分为两部分:常量池计数器以及常量池数据区,如下图4所示。

    图4 常量池的结构

    • 常量池计数器(constant_pool_count):由于常量的数量不固定,所以需要先放置两个字节来表示常量池容量计数值。图2中示例代码的字节码前10个字节如下图5所示,将十六进制的24转化为十进制值为36,排除掉下标“0”,也就是说,这个类文件中共有35个常量。

    图5 前十个字节及含义

    • 常量池数据区:数据区是由(constant_pool_count-1)个cp_info结构组成,一个cp_info结构对应一个常量。在字节码中共有14种类型的cp_info(如下图6所示),每种类型的结构都是固定的。

    图6 各类型的cp_info

    具体以CONSTANT_utf8_info为例,它的结构如下图7左侧所示。首先一个字节“tag”,它的值取自上图6中对应项的Tag,由于它的类型是utf8_info,所以值为“01”。接下来两个字节标识该字符串的长度Length,然后Length个字节为这个字符串具体的值。从图2中的字节码摘取一个cp_info结构,如下图7右侧所示。将它翻译过来后,其含义为:该常量类型为utf8字符串,长度为一字节,数据为“a”。

    图7 CONSTANT_utf8_info的结构(左)及示例(右)

    其他类型的cp_info结构在本文不再赘述,整体结构大同小异,都是先通过Tag来标识类型,然后后续n个字节来描述长度和(或)数据。先知其所以然,以后可以通过javap -verbose ByteCodeDemo命令,查看JVM反编译后的完整常量池,如下图8所示。可以看到反编译结果将每一个cp_info结构的类型和值都很明确地呈现了出来。

    图8 常量池反编译结果

    (4) 访问标志

    常量池结束之后的两个字节,描述该Class是类还是接口,以及是否被Public、Abstract、Final等修饰符修饰。JVM规范规定了如下图9的访问标志(Access_Flag)。需要注意的是,JVM并没有穷举所有的访问标志,而是使用按位或操作来进行描述的,比如某个类的修饰符为Public Final,则对应的访问修饰符的值为ACC_PUBLIC | ACC_FINAL,即0x0001 | 0x0010=0x0011。

    图9 访问标志

    (5) 当前类名

    访问标志后的两个字节,描述的是当前类的全限定名。这两个字节保存的值为常量池中的索引值,根据索引值就能在常量池中找到这个类的全限定名。

    (6) 父类名称

    当前类名后的两个字节,描述父类的全限定名,同上,保存的也是常量池中的索引值。

    (7) 接口信息

    父类名称后为两字节的接口计数器,描述了该类或父类实现的接口数量。紧接着的n个字节是所有接口名称的字符串常量的索引值。

    (8) 字段表

    字段表用于描述类和接口中声明的变量,包含类级别的变量以及实例变量,但是不包含方法内部声明的局部变量。字段表也分为两部分,第一部分为两个字节,描述字段个数;第二部分是每个字段的详细信息fields_info。字段表结构如下图所示:

    图10 字段表结构

    以图2中字节码的字段表为例,如下图11所示。其中字段的访问标志查图9,0002对应为Private。通过索引下标在图8中常量池分别得到字段名为“a”,描述符为“I”(代表int)。综上,就可以唯一确定出一个类中声明的变量private int a。

    图11 字段表示例

    (9)方法表

    字段表结束后为方法表,方法表也是由两部分组成,第一部分为两个字节描述方法的个数;第二部分为每个方法的详细信息。方法的详细信息较为复杂,包括方法的访问标志、方法名、方法的描述符以及方法的属性,如下图所示:

    图12 方法表结构

    方法的权限修饰符依然可以通过图9的值查询得到,方法名和方法的描述符都是常量池中的索引值,可以通过索引值在常量池中找到。而“方法的属性”这一部分较为复杂,直接借助javap -verbose将其反编译为人可以读懂的信息进行解读,如图13所示。可以看到属性中包括以下三个部分:

    • “Code区”:源代码对应的JVM指令操作码,在进行字节码增强时重点操作的就是“Code区”这一部分。
    • “LineNumberTable”:行号表,将Code区的操作码和源代码中的行号对应,Debug时会起到作用(源代码走一行,需要走多少个JVM指令操作码)。
    • “LocalVariableTable”:本地变量表,包含This和局部变量,之所以可以在每一个方法内部都可以调用This,是因为JVM将This作为每一个方法的第一个参数隐式进行传入。当然,这是针对非Static方法而言。

    图13 反编译后的方法表

    (10)附加属性表

    字节码的最后一部分,该项存放了在该文件中类或接口所定义属性的基本信息。

    1.3 字节码操作集合

    在上图13中,Code区的红色编号0~17,就是.java中的方法源代码编译后让JVM真正执行的操作码。为了帮助人们理解,反编译后看到的是十六进制操作码所对应的助记符,十六进制值操作码与助记符的对应关系,以及每一个操作码的用处可以查看Oracle官方文档进行了解,在需要用到时进行查阅即可。比如上图中第一个助记符为iconst_2,对应到图2中的字节码为0x05,用处是将int值2压入操作数栈中。以此类推,对0~17的助记符理解后,就是完整的add()方法的实现。

    1.4 操作数栈和字节码

    JVM的指令集是基于栈而不是寄存器,基于栈可以具备很好的跨平台性(因为寄存器指令集往往和硬件挂钩),但缺点在于,要完成同样的操作,基于栈的实现需要更多指令才能完成(因为栈只是一个FILO结构,需要频繁压栈出栈)。另外,由于栈是在内存实现的,而寄存器是在CPU的高速缓存区,相较而言,基于栈的速度要慢很多,这也是为了跨平台性而做出的牺牲。

    我们在上文所说的操作码或者操作集合,其实控制的就是这个JVM的操作数栈。为了更直观地感受操作码是如何控制操作数栈的,以及理解常量池、变量表的作用,将add()方法的对操作数栈的操作制作为GIF,如下图14所示,图中仅截取了常量池中被引用的部分,以指令iconst_2开始到ireturn结束,与图13中Code区0~17的指令一一对应:

    图14 控制操作数栈示意图

    1.5 查看字节码工具

    如果每次查看反编译后的字节码都使用javap命令的话,好非常繁琐。这里推荐一个Idea插件:jclasslib。使用效果如图15所示,代码编译后在菜单栏"View"中选择"Show Bytecode With jclasslib",可以很直观地看到当前字节码文件的类信息、常量池、方法区等信息。

    图15 jclasslib查看字节码

    2. 字节码增强

    在上文中,着重介绍了字节码的结构,这为我们了解字节码增强技术的实现打下了基础。字节码增强技术就是一类对现有字节码进行修改或者动态生成全新字节码文件的技术。接下来,我们将从最直接操纵字节码的实现方式开始深入进行剖析。

    图16 字节码增强技术

    2.1 ASM

    对于需要手动操纵字节码的需求,可以使用ASM,它可以直接生产 .class字节码文件,也可以在类被加载入JVM之前动态修改类行为(如下图17所示)。ASM的应用场景有AOP(Cglib就是基于ASM)、热部署、修改其他jar包中的类等。当然,涉及到如此底层的步骤,实现起来也比较麻烦。接下来,本文将介绍ASM的两种API,并用ASM来实现一个比较粗糙的AOP。但在此之前,为了让大家更快地理解ASM的处理流程,强烈建议读者先对访问者模式进行了解。简单来说,访问者模式主要用于修改或操作一些数据结构比较稳定的数据,而通过第一章,我们知道字节码文件的结构是由JVM固定的,所以很适合利用访问者模式对字节码文件进行修改。

    图17 ASM修改字节码

    2.1.1 ASM API

    2.1.1.1 核心API

    ASM Core API可以类比解析XML文件中的SAX方式,不需要把这个类的整个结构读取进来,就可以用流式的方法来处理字节码文件。好处是非常节约内存,但是编程难度较大。然而出于性能考虑,一般情况下编程都使用Core API。在Core API中有以下几个关键类:

    • ClassReader:用于读取已经编译好的.class文件。
    • ClassWriter:用于重新构建编译后的类,如修改类名、属性以及方法,也可以生成新的类的字节码文件。
    • 各种Visitor类:如上所述,CoreAPI根据字节码从上到下依次处理,对于字节码文件中不同的区域有不同的Visitor,比如用于访问方法的MethodVisitor、用于访问类变量的FieldVisitor、用于访问注解的AnnotationVisitor等。为了实现AOP,重点要使用的是MethodVisitor。

    2.1.1.2 树形API

    ASM Tree API可以类比解析XML文件中的DOM方式,把整个类的结构读取到内存中,缺点是消耗内存多,但是编程比较简单。TreeApi不同于CoreAPI,TreeAPI通过各种Node类来映射字节码的各个区域,类比DOM节点,就可以很好地理解这种编程方式。

    2.1.2 直接利用ASM实现AOP

    利用ASM的CoreAPI来增强类。这里不纠结于AOP的专业名词如切片、通知,只实现在方法调用前、后增加逻辑,通俗易懂且方便理解。首先定义需要被增强的Base类:其中只包含一个process()方法,方法内输出一行“process”。增强后,我们期望的是,方法执行前输出“start”,之后输出"end"。

    public class Base {
        public void process(){
            System.out.println("process");
        }
    }
    

    为了利用ASM实现AOP,需要定义两个类:一个是MyClassVisitor类,用于对字节码的visit以及修改;另一个是Generator类,在这个类中定义ClassReader和ClassWriter,其中的逻辑是,classReader读取字节码,然后交给MyClassVisitor类处理,处理完成后由ClassWriter写字节码并将旧的字节码替换掉。Generator类较简单,我们先看一下它的实现,如下所示,然后重点解释MyClassVisitor类。

    import org.objectweb.asm.ClassReader;
    import org.objectweb.asm.ClassVisitor;
    import org.objectweb.asm.ClassWriter;
    
    public class Generator {
        public static void main(String[] args) throws Exception {
    				//读取
            ClassReader classReader = new ClassReader("meituan/bytecode/asm/Base");
            ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
            //处理
            ClassVisitor classVisitor = new MyClassVisitor(classWriter);
            classReader.accept(classVisitor, ClassReader.SKIP_DEBUG);
            byte[] data = classWriter.toByteArray();
            //输出
            File f = new File("operation-server/target/classes/meituan/bytecode/asm/Base.class");
            FileOutputStream fout = new FileOutputStream(f);
            fout.write(data);
            fout.close();
            System.out.println("now generator cc success!!!!!");
        }
    }
    

    MyClassVisitor继承自ClassVisitor,用于对字节码的观察。它还包含一个内部类MyMethodVisitor,继承自MethodVisitor用于对类内方法的观察,它的整体代码如下:

    import org.objectweb.asm.ClassVisitor;
    import org.objectweb.asm.MethodVisitor;
    import org.objectweb.asm.Opcodes;
    
    public class MyClassVisitor extends ClassVisitor implements Opcodes {
        public MyClassVisitor(ClassVisitor cv) {
            super(ASM5, cv);
        }
        @Override
        public void visit(int version, int access, String name, String signature,
                          String superName, String[] interfaces) {
            cv.visit(version, access, name, signature, superName, interfaces);
        }
        @Override
        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
            MethodVisitor mv = cv.visitMethod(access, name, desc, signature,
                    exceptions);
            //Base类中有两个方法:无参构造以及process方法,这里不增强构造方法
            if (!name.equals("<init>") && mv != null) {
                mv = new MyMethodVisitor(mv);
            }
            return mv;
        }
        class MyMethodVisitor extends MethodVisitor implements Opcodes {
            public MyMethodVisitor(MethodVisitor mv) {
                super(Opcodes.ASM5, mv);
            }
    
            @Override
            public void visitCode() {
                super.visitCode();
                mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                mv.visitLdcInsn("start");
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            }
            @Override
            public void visitInsn(int opcode) {
                if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)
                        || opcode == Opcodes.ATHROW) {
                    //方法在返回之前,打印"end"
                    mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                    mv.visitLdcInsn("end");
                    mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
                }
                mv.visitInsn(opcode);
            }
        }
    }
    

    利用这个类就可以实现对字节码的修改。详细解读其中的代码,对字节码做修改的步骤是:

    • 首先通过MyClassVisitor类中的visitMethod方法,判断当前字节码读到哪一个方法了。跳过构造方法""后,将需要被增强的方法交给内部类MyMethodVisitor来进行处理。
    • 接下来,进入内部类MyMethodVisitor中的visitCode方法,它会在ASM开始访问某一个方法的Code区时被调用,重写visitCode方法,将AOP中的前置逻辑就放在这里。
    • MyMethodVisitor继续读取字节码指令,每当ASM访问到无参数指令时,都会调用MyMethodVisitor中的visitInsn方法。我们判断了当前指令是否为无参数的“return”指令,如果是就在它的前面添加一些指令,也就是将AOP的后置逻辑放在该方法中。
    • 综上,重写MyMethodVisitor中的两个方法,就可以实现AOP了,而重写方法时就需要用ASM的写法,手动写入或者修改字节码。通过调用methodVisitor的visitXXXXInsn()方法就可以实现字节码的插入,XXXX对应相应的操作码助记符类型,比如mv.visitLdcInsn(“end”)对应的操作码就是ldc “end”,即将字符串“end”压入栈。

    完成这两个visitor类后,运行Generator中的main方法完成对Base类的字节码增强,增强后的结果可以在编译后的target文件夹中找到Base.class文件进行查看,可以看到反编译后的代码已经改变了(如图18左侧所示)。然后写一个测试类MyTest,在其中new Base(),并调用base.process()方法,可以看到下图右侧所示的AOP实现效果:

    图18 ASM实现AOP的效果

    2.1.3 ASM工具

    利用ASM手写字节码时,需要利用一系列visitXXXXInsn()方法来写对应的助记符,所以需要先将每一行源代码转化为一个个的助记符,然后通过ASM的语法转换为visitXXXXInsn()这种写法。第一步将源码转化为助记符就已经够麻烦了,不熟悉字节码操作集合的话,需要我们将代码编译后再反编译,才能得到源代码对应的助记符。第二步利用ASM写字节码时,如何传参也很令人头疼。ASM社区也知道这两个问题,所以提供了工具ASM ByteCode Outline

    安装后,右键选择“Show Bytecode Outline”,在新标签页中选择“ASMified”这个tab,如图19所示,就可以看到这个类中的代码对应的ASM写法了。图中上下两个红框分别对应AOP中的前置逻辑于后置逻辑,将这两块直接复制到visitor中的visitMethod()以及visitInsn()方法中,就可以了。

    图19 ASM Bytecode Outline

    2.2 Javassist

    ASM是在指令层次上操作字节码的,阅读上文后,我们的直观感受是在指令层次上操作字节码的框架实现起来比较晦涩。故除此之外,我们再简单介绍另外一类框架:强调源代码层次操作字节码的框架Javassist。

    利用Javassist实现字节码增强时,可以无须关注字节码刻板的结构,其优点就在于编程简单。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构或者动态生成类。其中最重要的是ClassPool、CtClass、CtMethod、CtField这四个类:

    • CtClass(compile-time class):编译时类信息,它是一个class文件在代码中的抽象表现形式,可以通过一个类的全限定名来获取一个CtClass对象,用来表示这个类文件。
    • ClassPool:从开发视角来看,ClassPool是一张保存CtClass信息的HashTable,key为类名,value为类名对应的CtClass对象。当我们需要对某个类进行修改时,就是通过pool.getCtClass(“className”)方法从pool中获取到相应的CtClass。
    • CtMethod、CtField:这两个比较好理解,对应的是类中的方法和属性。

    了解这四个类后,我们可以写一个小Demo来展示Javassist简单、快速的特点。我们依然是对Base中的process()方法做增强,在方法调用前后分别输出"start"和"end",实现代码如下。我们需要做的就是从pool中获取到相应的CtClass对象和其中的方法,然后执行method.insertBefore和insertAfter方法,参数为要插入的Java代码,再以字符串的形式传入即可,实现起来也极为简单。

    import com.meituan.mtrace.agent.javassist.*;
    
    public class JavassistTest {
        public static void main(String[] args) throws NotFoundException, CannotCompileException, IllegalAccessException, InstantiationException, IOException {
            ClassPool cp = ClassPool.getDefault();
            CtClass cc = cp.get("meituan.bytecode.javassist.Base");
            CtMethod m = cc.getDeclaredMethod("process");
            m.insertBefore("{ System.out.println(\"start\"); }");
            m.insertAfter("{ System.out.println(\"end\"); }");
            Class c = cc.toClass();
            cc.writeFile("/Users/zen/projects");
            Base h = (Base)c.newInstance();
            h.process();
        }
    }
    

    3. 运行时类的重载

    3.1 问题引出

    上一章重点介绍了两种不同类型的字节码操作框架,且都利用它们实现了较为粗糙的AOP。其实,为了方便大家理解字节码增强技术,在上文中我们避重就轻将ASM实现AOP的过程分为了两个main方法:第一个是利用MyClassVisitor对已编译好的class文件进行修改,第二个是new对象并调用。这期间并不涉及到JVM运行时对类的重加载,而是在第一个main方法中,通过ASM对已编译类的字节码进行替换,在第二个main方法中,直接使用已替换好的新类信息。另外在Javassist的实现中,我们也只加载了一次Base类,也不涉及到运行时重加载类。

    如果我们在一个JVM中,先加载了一个类,然后又对其进行字节码增强并重新加载会发生什么呢?模拟这种情况,只需要我们在上文中Javassist的Demo中main()方法的第一行添加Base b=new Base(),即在增强前就先让JVM加载Base类,然后在执行到c.toClass()方法时会抛出错误,如下图20所示。跟进c.toClass()方法中,我们会发现它是在最后调用了ClassLoader的native方法defineClass()时报错。也就是说,JVM是不允许在运行时动态重载一个类的。

    图20 运行时重复load类的错误信息

    显然,如果只能在类加载前对类进行强化,那字节码增强技术的使用场景就变得很窄了。我们期望的效果是:在一个持续运行并已经加载了所有类的JVM中,还能利用字节码增强技术对其中的类行为做替换并重新加载。为了模拟这种情况,我们将Base类做改写,在其中编写main方法,每五秒调用一次process()方法,在process()方法中输出一行“process”。

    我们的目的就是,在JVM运行中的时候,将process()方法做替换,在其前后分别打印“start”和“end”。也就是在运行中时,每五秒打印的内容由"process"变为打印"start process end"。那如何解决JVM不允许运行时重加载类信息的问题呢?为了达到这个目的,我们接下来一一来介绍需要借助的Java类库。

    import java.lang.management.ManagementFactory;
    
    public class Base {
        public static void main(String[] args) {
            String name = ManagementFactory.getRuntimeMXBean().getName();
            String s = name.split("@")[0];
            //打印当前Pid
            System.out.println("pid:"+s);
            while (true) {
                try {
                    Thread.sleep(5000L);
                } catch (Exception e) {
                    break;
                }
                process();
            }
        }
    
        public static void process() {
            System.out.println("process");
        }
    }
    

    3.2 Instrument

    instrument是JVM提供的一个可以修改已加载类的类库,专门为Java语言编写的插桩服务提供支持。它需要依赖JVMTI的Attach API机制实现,JVMTI这一部分,我们将在下一小节进行介绍。在JDK 1.6以前,instrument只能在JVM刚启动开始加载类时生效,而在JDK 1.6之后,instrument支持了在运行时对类定义的修改。要使用instrument的类修改功能,我们需要实现它提供的ClassFileTransformer接口,定义一个类文件转换器。接口中的transform()方法会在类文件被加载时调用,而在transform方法里,我们可以利用上文中的ASM或Javassist对传入的字节码进行改写或替换,生成新的字节码数组后返回。

    我们定义一个实现了ClassFileTransformer接口的类TestTransformer,依然在其中利用Javassist对Base类中的process()方法进行增强,在前后分别打印“start”和“end”,代码如下:

    import java.lang.instrument.ClassFileTransformer;
    
    public class TestTransformer implements ClassFileTransformer {
        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
            System.out.println("Transforming " + className);
            try {
                ClassPool cp = ClassPool.getDefault();
                CtClass cc = cp.get("meituan.bytecode.jvmti.Base");
                CtMethod m = cc.getDeclaredMethod("process");
                m.insertBefore("{ System.out.println(\"start\"); }");
                m.insertAfter("{ System.out.println(\"end\"); }");
                return cc.toBytecode();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    }
    

    现在有了Transformer,那么它要如何注入到正在运行的JVM呢?还需要定义一个Agent,借助Agent的能力将Instrument注入到JVM中。我们将在下一小节介绍Agent,现在要介绍的是Agent中用到的另一个类Instrumentation。在JDK 1.6之后,Instrumentation可以做启动后的Instrument、本地代码(Native Code)的Instrument,以及动态改变Classpath等等。我们可以向Instrumentation中添加上文中定义的Transformer,并指定要被重加载的类,代码如下所示。这样,当Agent被Attach到一个JVM中时,就会执行类字节码替换并重载入JVM的操作。

    import java.lang.instrument.Instrumentation;
    
    public class TestAgent {
        public static void agentmain(String args, Instrumentation inst) {
            //指定我们自己定义的Transformer,在其中利用Javassist做字节码替换
            inst.addTransformer(new TestTransformer(), true);
            try {
                //重定义类并载入新的字节码
                inst.retransformClasses(Base.class);
                System.out.println("Agent Load Done.");
            } catch (Exception e) {
                System.out.println("agent load failed!");
            }
        }
    }
    

    3.3 JVMTI & Agent & Attach API

    上一小节中,我们给出了Agent类的代码,追根溯源需要先介绍JPDA(Java Platform Debugger Architecture)。如果JVM启动时开启了JPDA,那么类是允许被重新加载的。在这种情况下,已被加载的旧版本类信息可以被卸载,然后重新加载新版本的类。正如JDPA名称中的Debugger,JDPA其实是一套用于调试Java程序的标准,任何JDK都必须实现该标准。

    JPDA定义了一整套完整的体系,它将调试体系分为三部分,并规定了三者之间的通信接口。三部分由低到高分别是Java 虚拟机工具接口(JVMTI),Java 调试协议(JDWP)以及 Java 调试接口(JDI),三者之间的关系如下图所示:

    图21 JPDA

    现在回到正题,我们可以借助JVMTI的一部分能力,帮助动态重载类信息。JVM TI(JVM TOOL INTERFACE,JVM工具接口)是JVM提供的一套对JVM进行操作的工具接口。通过JVMTI,可以实现对JVM的多种操作,它通过接口注册各种事件勾子,在JVM事件触发时,同时触发预定义的勾子,以实现对各个JVM事件的响应,事件包括类文件加载、异常产生与捕获、线程启动和结束、进入和退出临界区、成员变量修改、GC开始和结束、方法调用进入和退出、临界区竞争与等待、VM启动与退出等等。

    而Agent就是JVMTI的一种实现,Agent有两种启动方式,一是随Java进程启动而启动,经常见到的java -agentlib就是这种方式;二是运行时载入,通过attach API,将模块(jar包)动态地Attach到指定进程id的Java进程内。

    Attach API 的作用是提供JVM进程间通信的能力,比如说我们为了让另外一个JVM进程把线上服务的线程Dump出来,会运行jstack或jmap的进程,并传递pid的参数,告诉它要对哪个进程进行线程Dump,这就是Attach API做的事情。在下面,我们将通过Attach API的loadAgent()方法,将打包好的Agent jar包动态Attach到目标JVM上。具体实现起来的步骤如下:

    • 定义Agent,并在其中实现AgentMain方法,如上一小节中定义的代码块7中的TestAgent类;
    • 然后将TestAgent类打成一个包含MANIFEST.MF的jar包,其中MANIFEST.MF文件中将Agent-Class属性指定为TestAgent的全限定名,如下图所示;

    图22 Manifest.mf

    • 最后利用Attach API,将我们打包好的jar包Attach到指定的JVM pid上,代码如下:
    import com.sun.tools.attach.VirtualMachine;
    
    public class Attacher {
        public static void main(String[] args) throws AttachNotSupportedException, IOException, AgentLoadException, AgentInitializationException {
            // 传入目标 JVM pid
            VirtualMachine vm = VirtualMachine.attach("39333"); 
            vm.loadAgent("/Users/zen/operation_server_jar/operation-server.jar");
        }
    }
    
    • 由于在MANIFEST.MF中指定了Agent-Class,所以在Attach后,目标JVM在运行时会走到TestAgent类中定义的agentmain()方法,而在这个方法中,我们利用Instrumentation,将指定类的字节码通过定义的类转化器TestTransformer做了Base类的字节码替换(通过javassist),并完成了类的重新加载。由此,我们达成了“在JVM运行时,改变类的字节码并重新载入类信息”的目的。

    以下为运行时重新载入类的效果:先运行Base中的main()方法,启动一个JVM,可以在控制台看到每隔五秒输出一次"process"。接着执行Attacher中的main()方法,并将上一个JVM的pid传入。此时回到上一个main()方法的控制台,可以看到现在每隔五秒输出"process"前后会分别输出"start"和"end",也就是说完成了运行时的字节码增强,并重新载入了这个类。

    图23 运行时重载入类的效果

    3.4 使用场景

    至此,字节码增强技术的可使用范围就不再局限于JVM加载类前了。通过上述几个类库,我们可以在运行时对JVM中的类进行修改并重载了。通过这种手段,可以做的事情就变得很多了:

    • 热部署:不部署服务而对线上服务做修改,可以做打点、增加日志等操作。
    • Mock:测试时候对某些服务做Mock。
    • 性能诊断工具:比如bTrace就是利用Instrument,实现无侵入地跟踪一个正在运行的JVM,监控到类和方法级别的状态信息。

    4. 总结

    字节码增强技术相当于是一把打开运行时JVM的钥匙,利用它可以动态地对运行中的程序做修改,也可以跟踪JVM运行中程序的状态。此外,我们平时使用的动态代理、AOP也与字节码增强密切相关,它们实质上还是利用各种手段生成符合规范的字节码文件。综上所述,掌握字节码增强后可以高效地定位并快速修复一些棘手的问题(如线上性能问题、方法出现不可控的出入参需要紧急加日志等问题),也可以在开发中减少冗余代码,大大提高开发效率。

    5. 参考文献

    作者简介

    泽恩,美团点评研发工程师。

    团队信息

    美团到店住宿业务研发团队负责美团酒店核心业务系统建设,致力于通过技术践行“帮大家住得更好”的使命。美团酒店屡次刷新行业记录,最近12个月酒店预订间夜量达到3个亿,单日入住间夜量峰值突破280万。团队的愿景是:建设打造旅游住宿行业一流的技术架构,从质量、安全、效率、性能多角度保障系统高速发展。
    美团到店事业群住宿业务研发团队现诚聘后台开发工程师/技术专家,欢迎有兴趣的同学加入。

    展开全文
  • 文章目录前言从AOP说起静态代理动态代理JavaProxyCGLIB字节码增强实现AOPASMJavaAssist运行时类加载...在上篇文章Java字节码技术(一)中已经介绍了Java中字节码相关的基础概念。我们知道,Java代码转换后的JVM指令...
  • Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至...
  • 作者:小傅哥 ... 沉淀、分享、成长,让自己和他人都能有所收获!...讲道理,市面上以及网络搜索中都基本很少有成体系的关于字节码编程的知识,这主要由于大部分开发人员其实很少接触这部分内容,包括;ASM、Javassi
  • 要理解java字节码的运行情况,首先要了解有关JVM的一些知识,这些是java字节码运行的先决条件。 JVM数据类型 Java是静态类型的,它会影响字节码指令的设计,这样指令就会期望自己对特定类型的值进行操作...
  • Javassist(JAVA编程ASSISTant)使Java字节码操作变得简单。 它是一个用Java编辑字节码的类库。 它使Java程序可以在运行时定义新类,并在JVM加载它时修改类文件。 与其他类似的字节码编辑器不同,Javassist提供了两...
  • Java 字节码概述

    2009-11-30 14:31:25
    Java 字节码概述ppt说明文档,欢迎交流
  • javassist, Java字节码工程工具包 Java字节码工程工具包 版本 3版权所有( C ) 1999 -2017按 Shigeru Chiba,保留所有权利。Javassist ( Java编程助手) 使Java字节码操作简单。 它是一个类库,用于在Java中编辑字节码
  • Java字节码文件

    千次阅读 2019-07-02 20:27:34
    字节码文件信息 常量池 方法表集合 二、分析try-catch-finally的demo 计算机只认识0和1。这意味着任何语言编写的程序最终都需要经过编译器编译成机器码才能被计算机执行。所以,我们所编写的程序在不同的平台上...
  • Java字节码操纵框架ASM小试

    千次阅读 2015-05-29 17:06:11
    Java字节码操纵框架ASM小试 转自:http://www.oseye.net/user/kevin/blog/304 本文主要内容: ASM是什么 JVM指令 Java字节码文件 ASM编程模型 ASM示例 参考资料汇总 JVM详细指令 ASM是什么 ...
  • 跟随本篇文章深入研究JVM内部结构和java字节码,你将会知道如何分解你的文件进行深入检查。 对于一个经验丰富的开发人员阅读java字节码也是非常枯燥的事情。首先我们要弄清楚我们为什么需要知道如此底层的东西?...
  • JAVA字节码文件操作技巧

    千次阅读 2013-09-23 10:23:39
    你知道如何操作JAVA字节码文件吗,这里将介绍与操作Java字节码有关的基本知识和操作Java字节码的方法及Demo,首先我们来看一下AOP的概念,AOP是OOP的延续,是AspectOrientedProgramming的缩写,意思是面向方面编程。...
  • idea (mac 版本)查看java字节码背景:学习并发编程的时候,查询字节码的频率也是蛮高的,最传统的方式 java -c编译后查看.class文件效率非常的低,因此,这篇文章可以帮助到你。1、进入setting->External Tools...
  • java动态编程-操作字节码

    千次阅读 2016-05-24 18:31:18
    1.动态编译一个类,动态编译可以参考http://blog.csdn.net/yingxiake/article/details/51487730还有http://blog.csdn.net/yingxiake/article/details/514877542.操作字节码,可以利用开源库操作字节码,例如CGLib、...
  • Java 字节码指令是 JVM 体系中非常难啃的一块硬骨头,我估计有些读者会有这样的疑惑,“Java 字节码难学吗?我能不能学会啊?” 讲良心话,不是我谦虚,一开始学 Java 字节码Java 虚拟机方面的知识我也感觉头大!...
  • Java字节码与反射机制

    2021-01-23 17:16:08
    字节码(Byte Code)是Java语言跨平台特性的重要保障,也是反射机制的重要基础。通过反射机制,我们不仅能看到一个类的属性和方法,还能在一个类里调用另外一个类的方法,但前提是我们得有相关类的字节码文件(也...
  • 字节码编程插桩这种技术常与 Javaagent 技术结合用在系统的非入侵监控中,这样就可以替代在方法中进行硬编码操作。比如,你需要监控一个方法,包括;方法信息、执行耗时、出入参数、执行链路以及异常等。那么就非常...
  • java字节码增强技术实现过程

    千次阅读 2017-08-04 08:31:23
    什么是Instrumentation?...监测的机制是对方法的字节码的修改。   包规范 在启动 JVM 时,通过指示代理类及其代理选项 启动一个代理程序。 该代理类必须实现公共的静态premain 方法,该方法原理上类似于 mai...
  • Java字节码的文件结构解析

    千次阅读 2018-10-02 22:08:08
    Java字节码的文件结构解析1、字节码文件产生缘由2、class类文件结构2.1、魔数和class文件版本2.2、常量池2.3、访问标志2.4、类索引、父类索引和接口索引集合2.5、字段表集合2.6、方法表集合3、总结 1、字节码文件...
  • 修改Java字节码

    千次阅读 2019-03-24 17:04:20
    下载工具asmtools 下载链接:...提取:72ke 操作字节 public class Foo { public static void main(String[] args) { boolean flag = true; if (flag) { System.out.prin...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 154,019
精华内容 61,607
热门标签
关键字:

java字节码编程

java 订阅