严忠林
摘 要: 代码动态生成是指在程序运行时根据实际情况即时生成需要的类代码。它可以提高程序的灵活性,已被用于很多应用架构、脚本语言的实现中。为帮助学生掌握代码动态生成技术,探讨了相关技术的实现方法、工具的应用和教学思路。
关键词: 代码动态生成; Java虚拟机; Java类文件; Bytecode; ASM
中图分类号:TP311 文獻标志码:A 文章编号:1006-8228(2013)05-07-03
Using code dynamic generation to enhance the flexibility of Java programs
Yan Zhonglin
(College of information, mechanical and electrical engineering, Shanghai Normal University, Shanghai 200234, China)
Abstract: Code dynamic generation is defined as class code generated instantly according to the actual needs when the program is running. It enhances the flexibility of the program, so it has been used in many framework and scripting language implementations. Students who are familiar with Java, JVM mechanism and Java class file structure will master this technique easily. It is beneficial for them to learn new concepts and new programming models based on this technology, which help them build more efficient, flexible and innovative application projects. The implementation methods, tools and teaching considerations are discussed.
Key words: code dynamic generation; JVM; Java class files; Bytecode; ASM
0 引言
Java程序是通过JVM(Java虚拟机)运行的,JVM屏蔽了底层硬件和操作系统的差异,提供了一个统一的处理平台。JVM根据类文件执行运算,类文件含有数据定义和处理代码,是Java程序的基本表示形式。程序中各个类文件分开存储,运行时按需装载链接,这一点和C++等其他语言不同。C++在编译时就组合所有类,形成一个完整的可运行文件,而Java要直到运行时才动态组合,完成链接。
通常,类文件是由编译器根据源文件自动生成,由JVM在运行时直接装载的。但这不是获得和使用它的惟一方法,某些情况下可以进行更巧妙的处理。比如在运行时绕过源文件直接生成需要的代码,或者在装载时直接修改类文件,即时改变它的行为,这就是类代码动态生成技术。
类代码动态生成技术需要直接在JVM的汇编语言——bytecode上展开工作。由于JVM模型和指令系统相对简单,类文件有定义明确的格式和语义,成员描述、与其他类的关联都基于符号引用,非常易于理解和修改,这都降低了直接处理它们的难度。这种类文件分开存储、按需装载的机制,也易于在运行时根据具体情况动态生成、替换某个特殊代码段。这使我们有了在运行时改变程序行为的“魔力”,可以突破Java的某些限制,完成它本来无法实现的任务。
例如,作为静态语言的Java,所有的域名、方法名都必须在编程时确定,有时这会限制程序的灵活性。虽然Java引入了“反射”机制以弥补此缺陷,但它的运行效率与正常代码相差很多,将它应用于高频执行的核心部分是不可接受的,这时就希望用即时生成的、可高效执行的代码进行替换。再比如,面向方面编程的实现需要在方法调用前后“编织”入横切操作,这可以在编译时进行,但如果能在运行时动态地插入这些代码,无疑更具灵活性。对象/关系映射也与此类似,需要能即时生成与关系数据库结构相对应的数据对象。这些都离不开代码动态生成技术。
为适应技术的发展潮流,许多学校都开设有关Java高端应用的课程,如JavaEE,若干轻型架构,一些新型脚本语言等,它们会引入许多新概念和编程模式,如AOP、IOC、ORM等。要使学生切实领会和掌握这些抽象而微妙的内容,只作表面上的介绍往往是不够的,应更深入地讲解内部实现机制,使学生知其然,也知其所以然。如果做一些核查,可以发现很多内容都离不开代码动态生成,比较著名的就有AspectJ、Hibernate、Spring、Clojure、Groovy、JRuby、Jython、Eclipse等。由此可见,动态代码生成已被相当普遍地使用了,可以认为它是未来Java高端项目开发的一种基本手段。因此,对现在的学生适当地普及这方面知识是很有必要的,这既可以使他们对Java特有的底层运行机制有更深入的理解,又可以帮助掌握许多现在流行的热门技术,更重要的是为他们将来自己进行创新开发打下基础。
要掌握这门技术,需要了解JVM的运行机制,这看起来比较困难,但由于Java是一个经过认真设计、非常理想化的平台,相关内容从总体上说还是易于理解和掌握的。经过对讲授内容的规划、斟酌,通常使用少量课时就能让学生对此有较清晰的理解。接下来我们对相关的知识点和教学问题进行探讨。
1 了解类文件格式
代码动态生成技术要直接构造可装载执行的类文件,因此首先必须清楚Java类文件的格式。它有非常明确的定义[1](见图1),除了文件头部以外,还有以下各部分。
⑴ 类型和接口部分:说明类的名字、访问控制、父类以及所实现的所有接口。
⑵ 数据域池:罗列了该类本身定义的所有数据成员,说明了各自的名字、访问控制、类型、初始值等属性。
⑶ 方法域池:是它自身所有方法的集合,详细描述了每个方法的名字、调用限制、参数类型、返回值、抛出异常、执行代码等属性。
⑷ 类属性池:列出类相关的属性,如源文件名等。整个文件中还有多处可出现各种属性,用于表示各类专门信息。Java已定义了20种属性,用户也可以引入自己需要的新属性。其中Annotation属性比较值得关注,它用于支持各种元编程,结合这里介绍的代码动态生成技术,可以实现各种特殊功能。
⑸ 常量池:包含了所有常量,类名、域名、方法名等各种命名串,以及描述它们类型等属性的描述串,对其他类的引用信息也在其中。常量共有14种类型,信息都用数字和字符表示,其他部分通过索引使用它们。
⑹ 方法的“code”属性的格式:它提供对应方法的bytecode代码,try/catch块位置,运行时操作栈和局部变量区等信息。类文件格式见图1。
[类文件格式 { /* u1 u2 u4为使用的字节数*/
类文件标记:u4 0XCAFEBABE
JDK版本: u2 子版本号; u2 主版本号
常量池: u2 常量池项数; [常量信息]*
类型及接口:u2 访问标志; u2 本类索引; u2 父类索引; u2 接口项数; [u2 接口索引]*
数据域池: u2 数据域项数; [数据信息]*
方法域池: u2 方法域项数; [方法信息]*
类属性池: u2 类属性项数; [类属性信息]*
}
数据信息 {
命名及描述:u2 访问标志; u2 数据名索引; u2 数据描述串索引;
数据属性池:u2 数据属性项数; [数据属性信息]*
}
方法信息 {
命名及描述:u2 访问标志; u2 方法名索引; u2 方法描述串索引;
方法屬性池:u2 方法属性项数; [方法属性信息]*
}
属性信息 { u2 属性名索引; u4 属性叙述长度; 属性叙述 }
常量信息 { u1 常量类型号; 常量叙述 }
Code信息 {
运行帧: u2 操作堆栈区大小; u2 局部变量区大小;
代码块: u4 代码块长度; [bytecode代码]*
异常处理池:u2 处理块项数; [u2 起始点; u2终止点; u2处理点; u2 异常类型]*
代码属性池:u2 代码属性项数; [代码属性信息]*
}]
图1 类文件格式
类文件结构虽然有点复杂,但学生只需粗略了解,使用后面介绍的ASM进行处理,内部细节是可以忽略的。
2 使用ASM
类文件是一个二进制文件,要程序员直接阅读和编写,是很困难的,应使用一些辅助工具软件。目前这方面最成熟、最受欢迎的是ASM,它是一个开源软件,相比于其他类似软件,ASM 更小更快,也提供了更好的编程模型[3]。它还附有代码生成工具和eclipse插件,可以在良好的人机界面下开展工作。ASM提供了两套API,一套类似于处理XML文件的SAX,由扫描类文件产生的一系列事件驱动,采用访问者模式进行处理。另一套类似于DOM,基于扫描类文件获得的语法树进行处理,两套方法各有优缺点,前者速度快,所需内存少,而后者能进行更全面的控制。
以前套方法为例,ASM提供了类解析器ClassReader,能够扫描解析类文件,并发出各驱动事件;提供类生成器ClassWriter,按合适的次序调用能生成包含各种元素的二进制类文件。ASM还有ClassVisitor、FieldVisitor、MethodVisitor、AnnotationVisitor等抽象类,为访问类文件中各元素定义了相应的visit方法,也按类文件的格式要求规定了调用次序。用户只要制作子类,用自己的处理逻辑覆盖相应方法,无须关注字节偏移量、常量池索引号等底层细节,就可实现从相关信息的检测、获取到运行代码的生成、修改等各种功能。
典型的代码结构如图2所示,由若干ClassReader、ClassVisitor子类、ClasssWriter组成一处理链。每个ClassVisitor子类通过对事件的过滤、变换、转发,实现自己特定的功能。它可以原样传送事件,保持对应元素不变;也可以改变事件参数,导致相应元素的修改;或者忽略某一事件不作转发,完成元素删除;还可以引入新的事件,注入新增加的元素。复杂情况下也可将这些对象组成网状结构,以完成多个类文件间的相互参照引用、合并、拆分等处理[5]。学生只需了解类文件结构,熟悉设计模式中的访问者模式,这种修改、生成类文件的方法应是比较容易掌握的。
[ClasssWriter cw=new ClassWriter();
ClassVisitor c1=new SubClassVisitor1(cw), c2=new SubClassVisitor2(c1);
ClassReader cr=new ClassReader(类文件).accept(c2, 0);
byte[] b=cw.toByteArray();]
图2 处理代码基本结构
3 熟悉Bytecode
用ASM直接生成类文件的工作是在JVM的底层展开的,应尽可能用于简单处理。复杂的处理还是直接使用高级语言Java较为妥当,这样就能进行良好的隔离、封装,将它们组织成可组合使用的基本单元。在此基础上再动态生成一些指令,按需访问、调用它们,完成所希望的处理。这些指令需要按照某种特定逻辑在运行时添加、改变、删除处理对象和运算步骤,如果直接生成还是感觉困难,依然可以先用Java编写一程序模版,确定处理的基本框架和各核心元素,运行时再以此为基础在指令层面上进行简单的增删和替换。
除了简单的类型或名字修改,以及数据或方法整体的增、删、替换外,其他处理一般都牵涉到Bytecode代码,因此有必要熟悉它们。JVM是一个堆栈型机器,所有的局部变量存储和运算操作都在堆栈中实现,没有寄存器,也不需要复杂的寻址方式,因此指令系统相对简单,名义上它有200多条指令[1],但经过归纳整理,实际只有不到50条不同指令(见表1),多种变化只是根据数据类型、判断条件而有规则添加的前后缀,完全可以举一反三,学习起来比真实的CPU如8086等要容易得多。指令大体可以分为三类。
⑴ 堆栈操作:用于为后续操作或方法调用,在栈顶配置需要的数据,或取得运算结果。它包括局部变量、对象/类数据成员、数组元素的进出栈,常量的进栈,栈顶元素的复制、交换等。
⑵ 运算操作:指定需要完成的运算,包括算术和逻辑运算,类型转换等,对于对象还有构造、判定等操作。
⑶ 流程控制:用于改变执行流程,包括各种转移指令、方法调用和返回指令。
这些指令恐怕是相关内容中最繁杂的部分,教学的关键是要让学生理解JVM运行时的栈帧结构,指令也应着重介绍动态代码生成时常用的部分,主要是各种方法调用,数据访问,以及为此而必须进行的堆栈准备,表1中列出了堆栈和控制部分的相关指令。
表1 JVM指令系统
[堆栈\&各类变量入/出栈\&①load⑥、①store⑥、①aload、①astore、get②、put②\&常量和栈顶处理\&①const_⑥、bipush、sipush、ldc⑦、pop③、dup③、swap\&运算\&算术运算\&①add、①sub、①mul、①div、①rem、①neg、iinc、①cmp⑥\&位或逻辑运算\&①shl、①shr、①ushr、①and、①or、①xor\&转换、构造和判定\&①2①、checkcast、new、④newarray、instanceof、arraylength\&控制\&方法调用和其他\&invoke②、monitorenter、monitorexit、nop\&条件判断\&if⑤、if_icmp⑤、if_acmp⑤、ifnull、ifnonnull\&转移和返回\&goto⑦、tableswitch、lookupswitch、①return、athrow、jsr⑦、ret\&①按数据类型\&②按成员属性\&③按所需的堆栈要求\&④按创建的数组类型\&⑤按判断条件\&⑥按操作要求\&⑦按数据宽度\&在此处填入相应内容\&]
4 掌握类装载机制
二进制类文件只有通过装载机制装入JVM才能发挥作用。通常情况下JVM只能根据classpath等预先指定的路径搜索并载入已有文件。所以对于运行时动态生成的二进制代码,需要另行设法导入,才能正常使用。这就需要理解Java的类装载机制。
Java类是在运行时按需动态装载的。虚拟机中有多个类装载器,负责获取不同来源的类文件。除了启动类装载器(Bootstrap ClassLoader),其他都继承自抽象类ClassLoader[2]。一般情况下,多个类加载器之间采用双亲委派模型组成一装载器树,以保证类代码的惟一性。类文件从装载到可以使用要经过载入、链接、初始化等阶段[4],载入阶段获取需要的二进制数据文件,链接阶段包含了代码合法性验证、存储空间获取和可选的引用类解析等操作。
因此,要能在运行时载入动态生成的代码,可以通过定义自己的类装载器完成。作为父类的ClassLoader有几个关键方法:
⑴ defineClass(String name, byte[] b, int off, int len):可以将一个正确的字节数组转换为合法的Java类。
⑵ findClass(String name):按该装载器特定的方法获得需要的二进制数据,并将其转换为Class对象。每个自定义装载器都应该覆写该方法,提供自己的载入机制。
⑶ loadClass(String name):用于装载指定名字的类,它实现双亲委派模型,保证仅在必要时才调用自己的findClass方法完成装载。通常不用改写。
一般情况下自定义装载器只需定义findClass方法,在其中利用ASM的ClassWrite动态生成二进制代码,再通过defineClass方法将它转化为标准的Java类。其基本结构如图3的上面部分所示。
[ClassLoader myClassLoader=new ClassLoader() {
public Class<?> findClass(String name) {
byte[] classfile=用ASM的ClasssWriter的toByteArray()获得的字节数组;
return defineClass(name, classfile, 0, classfile.length);
} };
Class<?> c=myClassLoader.loadClass(name);
---------------------------------------------------------------------------------------------------------------------
public static void premain(String agentArgs, Instrumentation inst) {
ClassFileTransformer transform=new ClassFileTransformer() {
public byte[] transform(ClassLoader loader, String classname, Class<?> clazz,
ProtectionDomain domain, byte[] classfile) throws IllegalClassFormatException {
if (当前处理的是需要变换的类代码) {
byte[] newClassfile=用ASM对classfile变换后获得的新数组;
return newClassfile;
} else return null;
} };
inst.addTransformer(transform,false);
}]
图3 动态生成代码的装载
在不宜使用自定义类装载器的场合,也可以用java.lang.instrument包[2]完成代码的动态变换和生成。它有ClassFile-
Transformer和Instrumentation两个接口,ClassFileTransformer由用户实现,完成代码变换,它定义了transform方法,会在装载器装载了新类,对其进行合法性验证之前执行。通过覆写该方法,用户可以拦截新载入的类,对其进行分类、检测、增删、修改,然后将变换后的新代码交JVM执行。使用这个包时,用户要提供一个包含premain方法的类。通过适当的命令,使系统在main方法执行之前执行该方法,用JVM提供的instrumentation对象添加用户自定义的代码变换方法,使所有新装载的类都经过transform的处理,在实现了期望的变换后被JVM执行。图3的下半部分演示了这种方案。
学会这些内容的关键是理解Java的类装载过程和对应方法,相对来说,前一方案比较容易掌握,后一方案适用于内部已封装使用了自定义的、非常规的类装载器的复杂应用架构,教学时可根据实际情况进行选择。
5 结束语
学习这些知识,可以帮助学生了解JVM的内部运行机制,更好地掌握那些通过动态代码生成技术实现的新概念、新模式,使他们能透过语法层面,从本质上深入领会这些内容。学习这些知识也可使学生掌握这种能在运行时生成、修改类代码的编程技能。在教学中,除了集中一些课时讲述以上内容外,还可以布置一些课题,让学生自己实践,比如“反射”代码的替换、动态代理的生成、接口实现的按需配置等,要鼓励学生发挥自己的想象力和创造性,用这种技术实现静态的Java语言所无法完成的功能,培养起勇于开拓,善于创新的良好习惯。
参考文献:
[1] Tim Lindholm,Frank Yellin.The Java Virtual Machine Specification
Second Edition [M] .Addison-Wesley Professional,1999.4.
[2] Oracle co. Java Platform, Standard Edition 7 API Specification
[EB/OL].http:// docs.oracle.com/javase/7/docs/api/
[3] Eric Bruneton. ASM 4.0 A Java bytecode engineering library [EB/
OL].http:// download.forge.objectweb.org/asm/asm4-guide.pdf
[4] 周志明.深入理解Java虚拟机[M].机械工业出版社,2011.
[5] Eugene Kuleshov. Using the ASM framework to implement
common Java bytecode transformation patterns[J]. http://asm.ow2.org/current/asm-transformations.pdf