Java语言是一种具有动态性的解释型编程语言,当指定程序运行时,java虚拟机就将编译生成的.class文件按照需求和一定的规则加载进内存,并组织成一个完整的java应用程序。Java语言把单独的一个类和接口编译成单独的一个.class文件,这些文件对于java运行环境来说是一个可以动态加载的单元。当某个类或接口发送改变后,只需编译这个类或接口,等下次java虚拟机重新激活时,java应用程序的功能就会得到更新。有时候,这种编译前就写好的源代码编译,是不能满足某些对动态加载要求更高的需求,典型的场景就是算法竞赛的在线评测系统,允许上传java代码,由系统在后台编译,运行并进行判定。

本文将通过JDK 6提供的编译器API和JDK中的工具类com.sun.tools.javac.Main两种方法讲解动态编译java程序。

JDK6提供的编译器API

javax.tools 包是一种添加到 Java SE 6 的标准 API,可以实现 Java 源代码编译,使您能够添加动态功能来扩展静态应用程序。该包中提供主要类可以从 Java String、StringBuffer 或其他 CharSequence 中编译 Java 源代码,而不是从文件中编译。

下面我们先看一个简单的例子再来介绍javax.tools包中的类:

import java.io.File;

import java.io.IOException;

import java.lang.reflect.InvocationTargetException;

import java.lang.reflect.Method;

import java.net.URI;

import java.util.Arrays;

 

import javax.tools.JavaCompiler;

import javax.tools.SimpleJavaFileObject;

import javax.tools.StandardJavaFileManager;

import javax.tools.StandardLocation;

import javax.tools.ToolProvider;

import javax.tools.JavaCompiler.CompilationTask;

import javax.tools.JavaFileManager.Location;

 

public class CompilerTest {

 

public static void main(String[] args) {

String className = “DynamicClass”;

String methodName = “sayHello”;

 

StringBuffer source = new StringBuffer(“public class “);

source.append(className);

source.append(“{ public static void “);

source.append(methodName + “()”);

source.append(” { System.out.println(/” Hello World/”); }}”);

 

JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);

Location location = StandardLocation.CLASS_OUTPUT;

File[] outputs = new File[]{new File(“bin/”)};

try {

fileManager.setLocation(location, Arrays.asList(outputs));

} catch (IOException e1) {

e1.printStackTrace();

}

StringSourceJavaObject sourceObject = new CompilerTest.StringSourceJavaObject(className, source.toString());

Iterable< ? extends SimpleJavaFileObject> fileObjects = Arrays.asList(sourceObject);

CompilationTask task = compiler.getTask(null, fileManager, null, null, null, fileObjects);

boolean result = task.call();

if(result) {

System.out.println(“编译成功!”);

try {

ClassLoader classLoader = CompilerTest.class.getClassLoader();

Class clazz = classLoader.loadClass(className);

Method mainMethod = clazz.getMethod(methodName, new Class[] {});

mainMethod.invoke(null, new Object[] {});

} catch (ClassNotFoundException e) {

e.printStackTrace();

} catch (SecurityException e) {

e.printStackTrace();

} catch (NoSuchMethodException e) {

e.printStackTrace();

} catch (IllegalArgumentException e) {

e.printStackTrace();

} catch (IllegalAccessException e) {

e.printStackTrace();

} catch (InvocationTargetException e) {

e.printStackTrace();

}

}

}

 

 

static class StringSourceJavaObject extends SimpleJavaFileObject {

 

private String code;

 

protected StringSourceJavaObject(String name, String content) {

super(URI.create(“String:///” + name.replace(“.”, “/”) + Kind.SOURCE.extension), Kind.SOURCE);

this.code = content;

}

 

@Override

public CharSequence getCharContent(boolean paramBoolean)

throws IOException {

return code;

}

}

}

运行结果:

编译成功!

Hello World

编译器JavaCompiler:

JavaCompiler是程序调用java语言编译器的接口。编译器通过getTask创建JavaCompiler.CompilationTask 对象后者从 JavaFileManager 中的 JavaFileObject SOURCE 对象编译源代码,创建新的输出 JavaFileObject CLASS 文件和 Diagnostic(警告和错误)。

 

Java文件管理器JavaFileManager:

JavaFileManager 提供了一个抽象的文件系统,可以将源文件和输出文件的文件名映射到 JavaFileObject 实例(其中,文件 表示一个惟一名称和一串字节之间的关联。客户机不需要使用实际的文件系统)。 JavaFileManager的功能主要是提供所有文件操作的规则,如源代码路径、编译的classpath,class文件目标目录等,其相关属性都提供默认值

JavaFileManager.Location 包含一个文件名和一个标记,该标记可以表明该位置是源代码还是一个输出位置。由于我是在myeclipse下建的工程,项目的目标路径就是项目下的bin目录,如果不设置的话,class文件输出路径即为默认值,也就是直接在项目根路径下。如果没有下面的几行代码:

Location location = StandardLocation.CLASS_OUTPUT;

File[] outputs = new File[]{new File(“bin/”)};

try {

fileManager.setLocation(location, Arrays.asList(outputs));

} catch (IOException e1) {

e1.printStackTrace();

}

当你编译完源码后,加载类时,会找不到类。

该示例中javax.tools包中的其他类信息,可以通过JDK 1.6的 API获取,这里就不再介绍。

com.sun.tools.javac.Main

通过编译和加载 Java 扩展来对应用程序进行扩展,这种想法并不新鲜,并且一些现有框架也支持这一功能。Java Platform, Enterprise Edition (Java EE) 中的 JavaServer Pages (JSP) 技术就是一种广为人知的动态框架,能够生成并编译 Java 类。JSP 转换器通过中间产物即源代码文件将 .jsp 文件转换为 Java servlet,JSP 引擎随后将源代码文件编译并加载到 Java EE servlet 容器中。编译过程通常是通过直接调用 javac 编译器完成的,这需要安装 Java Development Kit (JDK) 或者调用。

import java.io.File;

import java.io.FileWriter;

import java.lang.reflect.Method;

 

public class CreateClass {

private static String CLASS_NAME = “CreateClassTest”;

private static String CLASS_FILE = CLASS_NAME + “.java”;

 

public static void main(String args[]) {

CreateClass mtc = new CreateClass();

mtc.createClass();

System.out.println(“javac ” + CLASS_FILE);

mtc.compileClass();

System.out.println(“java ” + CLASS_NAME);

mtc.runClass();

}

 

public void createClass() {

try {

new File(CLASS_FILE).delete();

FileWriter aWriter = new FileWriter(CLASS_FILE, true);

aWriter.write(“public   class   ” + CLASS_NAME + “{“);

aWriter.write(“public   void   println()   {“);

aWriter.write(“System.out.println(/”=” + CLASS_NAME + “=/”);”);

aWriter.write(“}}”);

aWriter.flush();

aWriter.close();

} catch (Exception e) {

e.printStackTrace();

}

}

 

public void compileClass() {

String filePath = new File(CreateClass.class.getClassLoader()

.getResource(“”).getFile()).getAbsolutePath();

String[] source = { “-d”, filePath, new String(CLASS_FILE) };

System.out.println(“javac out:”

+ com.sun.tools.javac.Main.compile(source));

}

 

public void runClass() {

try {

Class params[] = {};

Object paramsObj[] = {};

Class testClass = Class.forName(CLASS_NAME);

Object iClass = testClass.newInstance();

Method thisMethod = testClass.getDeclaredMethod(“println”, params);

thisMethod.invoke(iClass, paramsObj);

} catch (Exception e) {

e.printStackTrace();

}

}

}

其他

另外一个可用的工具是Eclipse JDT Core提供的编译器。这是Eclipse Java开发环境使用的增量式Java编译器,支持运行和调试有错误的代码。该编译器也可以单独使用。Play框架在 内部使用了JDT的编译器来动态编译Java源代码。在开发模式下,Play框架会定期扫描项目中的Java源代码文件,一旦发现有修改,会自动编译 Java源代码。因此在修改代码之后,刷新页面就可以看到变化。使用这些动态编译的方式的时候,需要确保JDK中的tools.jar在应用的 CLASSPATH中。Apache Commons JCI 提供了一种机制可以将 Java 类编译并加载到运行的应用程序中。Janino 和 Javassist 也提供了类似的动态功能,但是 Janino 只限于 Java 1.4 之前的语言,而 Javassist 只能工作在 Java 类抽象级别,而不能在源代码级别工作。

参考资料

Java深度历险(一)

使用 javax.tools 创建动态应用程序

动态生成编译运行java类