娄不夜 首都经济贸易大学信息学院 100070
Web环境下Java表达式的动态编译与计算
娄不夜 首都经济贸易大学信息学院 100070
针对Java Web应用程序需要在运行时从外部读得Java表达式并进行计算的要求,利用编译器API对源代码进行编译、采用自定义类装载器装载字节代码、基于反射机制执行字节代码,从而实现Java表达式的动态编译与计算。该方法不需要产生Java源文件或class文件。实际系统的具体应用验证了该方法的有效性。
Java表达式;编译器API;类装载器;反射机制
有些Java Web应用程序在运行时需要从外部(如数据库、XML文件)读入某些数据,以完成相应的处理功能。这些数据可能是一些简单的字符串、数值,也可能是一些需要计算才能得到结果的表达式。
大多数解释型语言都有诸如eval的函数,可以将一个字符串作为表达式进行计算求值,而作为半编译半解释的Java语言并不提供类似的功能。本文介绍一种利用Java SE6提供的编译器API实现类似功能的方法,可以在程序运行过程中完成Java表达式的编译和计算。
该方法涉及动态编译、自定义类装载器、反射机制等技术,其基本过程如下:
(1) 基于要计算的表达式字符串构建一个Java类,类的源代码存储在普通的字符串中。
(2) 利用编译器API实现对源代码的动态编译。其中源代码直接读自上述字符串,编译产生的字节代码保存在字节数组中。整个过程不涉及源代码文件和字节代码文件的创建。
(3) 使用自定义类装载器装入字节代码、产生Class对象。这里,类装载器直接从字节数组读取字节代码。由于Java不支持类的重新装载,也不允许已装载类的卸载,所以每次计算表达式都需要新建一个类装载器实例。
(4) 利用反射机制调用Temp类的m方法,完成表达式的最后计算。
其中,字符串exp是要计算的表达式,静态方法m的功能是计算并返回表达式的值。这里表达式的类型可以是任意引用类型或基本类型。若是基本类型,计算结果会自动转换成相应包装类对象返回。
(1) 调用javax.tools.ToolProvider类的getSystemJavaCompiler方法获得编译器对象。
(2) 调用编译器对象的getTask方法创建编译作业对象。getTask方法有六个参数,其中第2个参数需指定一个Java文件管理器,在执行编译作业时,系统会自动调用该文件管理器的相关方法获取用于保存编译产生的字节代码的Java文件对象。第4个参数指定编译所需的相关参数。第6个参数需指定包含待编译源代码的Java文件对象。其他三个参数可置为null。
(3) 调用编译作业对象的call方法,执行编译作业。若方法返回true,表示编译成功;否则表示编译失败。
(4) 调用Java文件管理器的相关方法获取保存有字节代码的Java文件对象,然后调用Java文件对象的相关方法获取字节代码。
Java文件对象是对Java源代码或Java字节代码的抽象表示。默认情况下,一个Java文件对象表示一个Java源文件或一个class文件。为能表示保存在内存(如字符串、字节数组)的Java源代码或字节代码,需要自定义Java文件对象类。
为清晰之见,对两种Java文件对象类分别进行定义。用于表示源代码的Java文件对象类的代码如下:
在执行编译作业时,系统会自动调用getCharContent方法获得源代码。
用于表示字节代码的Java文件对象类的部分代码如下:
当执行编译作业时,系统会自动调用openOutputStream方法获得一个字节输出流,并通过该字节输出流写出编译产生的字节代码。编译结束后,可以调用getByteCode方法获取字节代码。
Java文件管理器用于管理Java文件对象。在创建编译作业对象时,若第2个参数设置为null,系统将采用一个标准Java文件管理器。在这种情况下,编译产生的字节代码将被保存到相应的class文件中。为实现对特殊Java文件对象的管理,需要自定义Java文件管理器类(省略了构造方法):
这种类型的Java文件管理器可以对JavaFileObjectByteCode型文件对象进行管理。当执行编译作业时,系统会自动调用getJavaFileForOutput方法获得用于保存相应字节代码的文件对象。编译结束后,用户也可以调用该方法获得相同的文件对象。
要执行Java字节代码,首先需要由类装载器将其装入JVM。在JVM中,通常存在多个类装载器,每个类装载器负责装载特定位置的class文件。最基本的类装载器包括:
(1) 引导(Bootstrap)类装载器。该装载器属于JVM的一部分,其本身在JVM启动时自行装入。作用有两个:一是装载并创建扩展类装载器和应用程序类装载器;二是需要时装载Java核心类库中的class文件。
(2) 扩展(Extension)类装载器。用于装载java.ext.dirs属性指定位置处的class文件。
(3) 应用程序(Application)类装载器。用于装载java.class.path属性指定位置处的class文件。
其中扩展类装载器代码和应用程序类装载器代码都是Java类,它们都是ClassLoader类子类。
每个类装载器在创建时可以指定一个父类装载器,如扩展类装载器就被指定为应用程序类装载器的父类装载器。装载一个类通常由系统隐含调用类装载器的loadClass(String)方法完成,其过程采用委托模型:首先检查当前类装载器是否已装载该类;若没有,就委托其父类装载器进行装载;若没有父类装载器,则委托引导类装载器进行装载;若上述所有情况都无法定位或装载该类,就调用当前类装载器的findClass(String)方法自行装载。
Web应用环境一般会有更多的类装载器,这些类装载器通常把应用程序类装载器作为父类装载器,它们同样采用上述委托模型进行类的装载。
为了能将由动态编译产生的、保存在字节数组中的Temp类的字节代码装入JVM,需要自定义类装载器类:
这里,loadClass(byte[])方法特用于装载Temp类的字节代码,而Temp类引用的其他类型仍由系统隐含调用定义在超类中的loadClass(String)方法装载,实质上就是委托父类装载器或引导类装载器装载。
只要父类装载器和编译参数(getTask方法的第4个参数)设置得当,Temp类和需要计算的表达式就可以引用任何应用程序能够引用的类型。
装入Temp类的字节代码后,就可以利用反射机制执行其中的静态方法,完成表达式的计算求值:
这里为基于JSF框架的Web应用设计一个名为Calculator的应用程序范围的托管Bean,其他托管Bean可以调用其calculate(String)方法计算指定的Java表达式:
其中,createSource和execute方法的代码已在前面给出,calculate(String)方法除需调用上面两个方法外,主要是要实现代码的动态编译,其处理过程与相关技术在前面已进行了详细介绍,这里不再赘述。
该托管Bean类还定义了若干有名常量。这些常量在每次计算表达式时都是通用的,可以在构造方法中进行设置。这些常量的作用如下:
compiler:编译器对象。用于创建编译作业对象以及标准Java文件管理器。
sm:标准Java文件管理器。在创建自定义的FileManagerImpl型文件管理器时,应将该标准文件管理器作为参数传递给构造方法。
options:作为编译器对象的getTask方法的第4个参数,可包含一些编译参数。如将classpath参数设为Web应用中WEB-INFclasses目录的实际路径。
parent:装载当前Bean类的类装载器。可作为自定义类装载器的父类装载器。
本文采用编译器API、自定义类装载器、反射机制等技术实现了Java表达式的动态编译和计算。该方法已在实际系统中得到了验证和应用,具有通用、便捷、性能好等特点,达到了满意的效果。
[1]David Biesack. 使用javax.tools创建动态应用程序[EB/OL]. http://www.ibm.com/ developerworks/cn/java/j-jcomp/#download, 2007-12-24.
[2]James Gosling, Bill Joy. The Java Language Specification Third Edition[M]. Addison Wesley,2005:308-331.
[3]藏旭毅. 浅析J2EE应用服务器的JAVA类装载器[J]. 电脑知识与技术.2007, 3(18):1609-1610.
[4]陈烨,张蓓. JDK1.5类库大全[M]. 北京:清华大学出版社.2005:403-419.
10.3969/j.issn.1001-8972.2010.16.063
首都经济贸易大学教改项目(00790954210333)
娄不夜(1965-),男,副教授,硕士,研究方向为信息技术与信息系统、Web应用。