如何以编程方式编译以字符串形式提供的Java代码的问题被问相当频繁和以各种形式,有时引用存储在数据库或由用户..当我搜索有关这方面的信息时,我偶然发现了许多这样的问题,并且失望地看到,一般的建议是使用外部工具(BeanShell,Groovy.)。这个亚当·佩恩特对这个问题的回答是最有帮助的,至少找出了相关的关键词。但即使通过咨询更多的外部资源(比如Java 2的示例),我很难实现一个或多个Java类的纯内存编译(实际上是这样的)。工作过)只使用JavaCompilerAPI
下面是一个例子,展示了在运行时在内存中编译一个或多个类的整个过程,当它们的源代码以字符串的形式给出时。它是围绕一个小的实用工具类建造的,RuntimeCompiler,它只接收序列类名和相应的源代码,然后允许编译这些类并获得Class物品。
这是一个MCVE可以直接编译和执行的JDK, 不带着JRE,因为后者不包含像JavaCompiler.import java.io.ByteArrayOutputStream;import java.io.IOException;import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;import java.lang.reflect.Modifier;
import java.net.URI;import java.util.ArrayList;import java.util.Arrays;import java.util.Collections;import java.util.Comparator;
import java.util.LinkedHashMap;import java.util.List;import java.util.Map;import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;import javax.tools.FileObject;import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaCompiler;import javax.tools.JavaCompiler.CompilationTask;import javax.tools.JavaFileObject;
import javax.tools.JavaFileObject.Kind;import javax.tools.SimpleJavaFileObject;import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;/**
* An example showing how to use the RuntimeCompiler utility class
*/public class RuntimeCompilerExample{
public static void main(String[] args) throws Exception
{
simpleExample();
twoClassExample();
useLoadedClassExample();
}
/**
* Simple example: Shows how to add and compile a class, and then
* invoke a static method on the loaded class.
*/
private static void simpleExample()
{
String classNameA = "ExampleClass";
String codeA =
"public class ExampleClass {" + "\n" +
" public static void exampleMethod(String name) {" + "\n" +
" System.out.println(\"Hello, \"+name);" + "\n" +
" }" + "\n" +
"}" + "\n";
RuntimeCompiler r = new RuntimeCompiler();
r.addClass(classNameA, codeA);
r.compile();
MethodInvocationUtils.invokeStaticMethod(
r.getCompiledClass(classNameA),
"exampleMethod", "exampleParameter");
}
/**
* An example showing how to add two classes (where one refers to the
* other), compile them, and invoke a static method on one of them
*/
private static void twoClassExample()
{
String classNameA = "ExampleClassA";
String codeA =
"public class ExampleClassA {" + "\n" +
" public static void exampleMethodA(String name) {" + "\n" +
" System.out.println(\"Hello, \"+name);" + "\n" +
" }" + "\n" +
"}" + "\n";
String classNameB = "ExampleClassB";
String codeB =
"public class ExampleClassB {" + "\n" +
" public static void exampleMethodB(String name) {" + "\n" +
" System.out.println(\"Passing to other class\");" + "\n" +
" ExampleClassA.exampleMethodA(name);" + "\n" +
" }" + "\n" +
"}" + "\n";
RuntimeCompiler r = new RuntimeCompiler();
r.addClass(classNameA, codeA);
r.addClass(classNameB, codeB);
r.compile();
MethodInvocationUtils.invokeStaticMethod(
r.getCompiledClass(classNameB),
"exampleMethodB", "exampleParameter");
}
/**
* An example that compiles and loads a class, and then uses an
* instance of this class
*/
private static void useLoadedClassExample() throws Exception
{
String classNameA = "ExampleComparator";
String codeA =
"import java.util.Comparator;" + "\n" +
"public class ExampleComparator " + "\n" +
" implements Comparator {" + "\n" +
" @Override" + "\n" +
" public int compare(Integer i0, Integer i1) {" + "\n" +
" System.out.println(i0+\" and \"+i1);" + "\n" +
" return Integer.compare(i0, i1);" + "\n" +
" }" + "\n" +
"}" + "\n";
RuntimeCompiler r = new RuntimeCompiler();
r.addClass(classNameA, codeA);
r.compile();
Class> c = r.getCompiledClass("ExampleComparator");
Comparator comparator = (Comparator) c.newInstance();
System.out.println("Sorting...");
List list = new ArrayList(Arrays.asList(3,1,2));
Collections.sort(list, comparator);
System.out.println("Result: "+list);
}}/**
* Utility class for compiling classes whose source code is given as
* strings, in-memory, at runtime, using the JavaCompiler tools.
*/class RuntimeCompiler{
/**
* The Java Compiler
*/
private final JavaCompiler javaCompiler;
/**
* The mapping from fully qualified class names to the class data
*/
private final Map classData;
/**
* A class loader that will look up classes in the {@link #classData}
*/
private final MapClassLoader mapClassLoader;
/**
* The JavaFileManager that will handle the compiled classes, and
* eventually put them into the {@link #classData}
*/
private final ClassDataFileManager classDataFileManager;
/**
* The compilation units for the next compilation task
*/
private final List compilationUnits;
/**
* Creates a new RuntimeCompiler
*
* @throws NullPointerException If no JavaCompiler could be obtained.
* This is the case when the application was not started with a JDK,
* but only with a JRE. (More specifically: When the JDK tools are
* not in the classpath).
*/
public RuntimeCompiler()
{
this.javaCompiler = ToolProvider.getSystemJavaCompiler();
if (javaCompiler == null)
{
throw new NullPointerException(
"No JavaCompiler found. Make sure to run this with "
+ "a JDK, and not only with a JRE");
}
this.classData = new LinkedHashMap();
this.mapClassLoader = new MapClassLoader();
this.classDataFileManager =
new ClassDataFileManager(
javaCompiler.getStandardFileManager(null, null, null));
this.compilationUnits = new ArrayList();
}
/**
* Add a class with the given name and source code to be compiled
* with the next call to {@link #compile()}
*
* @param className The class name
* @param code The code of the class
*/
public void addClass(String className, String code)
{
String javaFileName = className + ".java";
JavaFileObject javaFileObject =
new MemoryJavaSourceFileObject(javaFileName, code);
compilationUnits.add(javaFileObject);
}
/**
* Compile all classes that have been added by calling
* {@link #addClass(String, String)}
*
* @return Whether the compilation succeeded
*/
boolean compile()
{
DiagnosticCollector diagnosticsCollector =
new DiagnosticCollector();
CompilationTask task =
javaCompiler.getTask(null, classDataFileManager,
diagnosticsCollector, null, null,
compilationUnits);
boolean success = task.call();
compilationUnits.clear();
for (Diagnostic> diagnostic : diagnosticsCollector.getDiagnostics())
{
System.out.println(
diagnostic.getKind() + " : " +
diagnostic.getMessage(null));
System.out.println(
"Line " + diagnostic.getLineNumber() +
" of " + diagnostic.getSource());
System.out.println();
}
return success;
}
/**
* Obtain a class that was previously compiled by adding it with
* {@link #addClass(String, String)} and calling {@link #compile()}.
*
* @param className The class name
* @return The class. Returns null
if the compilation failed.
*/
public Class> getCompiledClass(String className)
{
return mapClassLoader.findClass(className);
}
/**
* In-memory representation of a source JavaFileObject
*/
private static final class MemoryJavaSourceFileObject extends
SimpleJavaFileObject
{
/**
* The source code of the class
*/
private final String code;
/**
* Creates a new in-memory representation of a Java file
*
* @param fileName The file name
* @param code The source code of the file
*/
private MemoryJavaSourceFileObject(String fileName, String code)
{
super(URI.create("string:///" + fileName), Kind.SOURCE);
this.code = code;
}
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors)
throws IOException
{
return code;
}
}
/**
* A class loader that will look up classes in the {@link #classData}
*/
private class MapClassLoader extends ClassLoader
{
@Override
public Class> findClass(String name)
{
byte[] b = classData.get(name);
return defineClass(name, b, 0, b.length);
}
}
/**
* In-memory representation of a class JavaFileObject
* @author User
*
*/
private class MemoryJavaClassFileObject extends SimpleJavaFileObject
{
/**
* The name of the class represented by the file object
*/
private final String className;
/**
* Create a new java file object that represents the specified class
*
* @param className THe name of the class
*/
private MemoryJavaClassFileObject(String className)
{
super(URI.create("string:///" + className + ".class"),
Kind.CLASS);
this.className = className;
}
@Override
public OutputStream openOutputStream() throws IOException
{
return new ClassDataOutputStream(className);
}
}
/**
* A JavaFileManager that manages the compiled classes by passing
* them to the {@link #classData} map via a ClassDataOutputStream
*/
private class ClassDataFileManager extends
ForwardingJavaFileManager
{
/**
* Create a new file manager that delegates to the given file manager
*
* @param standardJavaFileManager The delegate file manager
*/
private ClassDataFileManager(
StandardJavaFileManager standardJavaFileManager)
{
super(standardJavaFileManager);
}
@Override
public JavaFileObject getJavaFileForOutput(final Location location,
final String className, Kind kind, FileObject sibling)
throws IOException
{
return new MemoryJavaClassFileObject(className);
}
}
/**
* An output stream that is used by the ClassDataFileManager
* to store the compiled classes in the {@link #classData} map
*/
private class ClassDataOutputStream extends OutputStream
{
/**
* The name of the class that the received class data represents
*/
private final String className;
/**
* The output stream that will receive the class data
*/
private final ByteArrayOutputStream baos;
/**
* Creates a new output stream that will store the class
* data for the class with the given name
*
* @param className The class name
*/
private ClassDataOutputStream(String className)
{
this.className = className;
this.baos = new ByteArrayOutputStream();
}
@Override
public void write(int b) throws IOException
{
baos.write(b);
}
@Override
public void close() throws IOException
{
classData.put(className, baos.toByteArray());
super.close();
}
}}/**
* Utility methods not directly related to the RuntimeCompiler
*/class MethodInvocationUtils{
/**
* Utility method to invoke the first static method in the given
* class that can accept the given parameters.
*
* @param c The class
* @param methodName The method name
* @param args The arguments for the method call
* @return The return value of the method call
* @throws RuntimeException If either the class or a matching method
* could not be found
*/
public static Object invokeStaticMethod(
Class> c, String methodName, Object... args)
{
Method m = findFirstMatchingStaticMethod(c, methodName, args);
if (m == null)
{
throw new RuntimeException("No matching method found");
}
try
{
return m.invoke(null, args);
}
catch (IllegalAccessException e)
{
throw new RuntimeException(e);
}
catch (IllegalArgumentException e)
{
throw new RuntimeException(e);
}
catch (InvocationTargetException e)
{
throw new RuntimeException(e);
}
catch (SecurityException e)
{
throw new RuntimeException(e);
}
}
/**
* Utility method to find the first static method in the given
* class that has the given name and can accept the given
* arguments. Returns null
if no such method
* can be found.
*
* @param c The class
* @param methodName The name of the method
* @param args The arguments
* @return The first matching static method.
*/
private static Method findFirstMatchingStaticMethod(
Class> c, String methodName, Object ... args)
{
Method methods[] = c.getDeclaredMethods();
for (Method m : methods)
{
if (m.getName().equals(methodName) &&
Modifier.isStatic(m.getModifiers()))
{
Class>[] parameterTypes = m.getParameterTypes();
if (areAssignable(parameterTypes, args))
{
return m;
}
}
}
return null;
}
/**
* Returns whether the given arguments are assignable to the
* respective types
*
* @param types The types
* @param args The arguments
* @return Whether the arguments are assignable
*/
private static boolean areAssignable(Class> types[], Object ...args)
{
if (types.length != args.length)
{
return false;
}
for (int i=0; i
{
Object arg = args[i];
Class> type = types[i];
if (arg != null && !type.isAssignableFrom(arg.getClass()))
{
return false;
}
}
return true;
}}根据评论编辑:
为了编译包含在外部JAR文件中的类,只需将JAR添加到classpath调用应用程序。这个JavaCompiler然后将选择这个类路径来查找编译所需的类。
似乎涉及到了一些魔法。至少,我还没有弄清楚这背后的确切机制,只是用一个例子对其进行了测试。
还有一个旁白:当然,我们可以从字面上考虑任意这个类的扩展。我的目标是创建一个简单、独立、易于复制和传递的示例,它显示了整个过程,甚至可能对某些应用程序模式“有用”。
对于更复杂的功能,可以考虑相应地扩展这个类,或者查看一下,例如,Java-Runtime-编译器来自OpenHFT项目(我在写了这个答案几周后偶然发现了这个问题)。它基本上在内部使用相同的技术,但以一种更复杂的方式使用,并且还提供了用于处理外部依赖项的类加载器的专用机制。