如何在运行时,动态将一个由java源代码组成的字符串编译为Java类?
注意,全部的操作都需要在内存中完成,不能把字符串保存到磁盘上。
package compiles;import java.lang.reflect.Method;
/*
* User: Freewind
* Blog: http://freewind.me
* Date: 12-11-16
/
public class CreateClassFromString {<span class="kwrd">private</span> <span class="kwrd">static</span> final String JAVA_SOURCE = <span class="str">"public class Hello {"</span> + <span class="str">" public void say() {"</span> + <span class="str">" System.out.println(\"world!\");"</span> + <span class="str">" }"</span> + <span class="str">"}"</span>; <span class="kwrd">public</span> <span class="kwrd">static</span> <span class="kwrd">void</span> main(String[] args) throws Exception { Class dynaCls = compile(JAVA_SOURCE); Method say = dynaCls.getMethod(<span class="str">"say"</span>); say.invoke(dynaCls.newInstance()); <span class="rem">// it will print "world!" on the console</span> } <span class="kwrd">private</span> <span class="kwrd">static</span> Class compile(String source) { <span class="rem">// compile the source to a class</span> <span class="kwrd">return</span> <span class="kwrd">null</span>; }
}```
解决方案一:使用由Jdk1.6提供的JavaCompiler
在jdk1.6中,提供了一个JavaCompiler接口,它可以方便的将java源代码编译为字节码。
思路如下:
- 通过ToolProvider.getSystemJavaCompiler()得到系统javac
- 通过compiler.getTask(…)创建一个编译任务,需传入一些参数,如java源文件等
- 调用task.call(),进行编译
- loadClass(…)载入刚编译出来的类,进行操作
额外操作:
- 传入-g参数,使得生成的字节码不会改变变量名等
- 两类互调:源代码一同传入getTask(…)
- 调用第三方库中的代码。只要将第三方库的jar加入到项目的classpath中
代码如下:
package compiles;import javax.tools.FileObject;
import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.security.SecureClassLoader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;public class DynaCompTest {
<span class="kwrd">public</span> <span class="kwrd">static</span> <span class="kwrd">void</span> main(String[] args) throws Exception { <span class="rem">// Full name of the class that will be compiled.</span> <span class="rem">// If class should be in some package, fullName should contain it too (ex. "testpackage.DynaClass")</span> String fullName = <span class="str">"DynaClass"</span>; <span class="rem">// Here we specify the source code of the class to be compiled</span> StringBuilder src = <span class="kwrd">new</span> StringBuilder(); src.append(<span class="str">"public class DynaClass {\n"</span>); src.append(<span class="str">" public void setName(String name) {}\n"</span>); src.append(<span class="str">" public void invokeAnother() { System.out.println(new DynaClass1()); }\n"</span>); src.append(<span class="str">" public String toString() {\n"</span>); src.append(<span class="str">" return \"Hello, I am \" + this.getClass().getSimpleName();\n"</span>); src.append(<span class="str">" }\n"</span>); src.append(<span class="str">"}\n"</span>); String fullName1 = <span class="str">"DynaClass1"</span>; StringBuilder src1 = <span class="kwrd">new</span> StringBuilder(); src1.append(<span class="str">"public class DynaClass1 {\n"</span>); src1.append(<span class="str">" public void invokeAnother() { System.out.println(new DynaClass()); }\n"</span>); src1.append(<span class="str">" public String toString() {\n"</span>); src1.append(<span class="str">" org.apache.commons.lang.StringUtils.trim(\" a \");\n"</span>); src1.append(<span class="str">" return \"Hello, I am \" + this.getClass().getSimpleName();\n"</span>); src1.append(<span class="str">" }\n"</span>); src1.append(<span class="str">"}\n"</span>); <span class="rem">// We get an instance of JavaCompiler. Then we create a file manager (our custom implementation of it)</span> JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); JavaFileManager fileManager = <span class="kwrd">new</span> ClassFileManager(compiler.getStandardFileManager(<span class="kwrd">null</span>, <span class="kwrd">null</span>, <span class="kwrd">null</span>)); <span class="rem">// Dynamic compiling requires specifying</span> <span class="rem">// a list of "files" to compile. In our case this is a list containing one "file" which is in our case</span> <span class="rem">// our own implementation (see details below)</span> List<JavaFileObject> jfiles = <span class="kwrd">new</span> ArrayList<JavaFileObject>(); jfiles.add(<span class="kwrd">new</span> CharSequenceJavaFileObject(fullName, src)); jfiles.add(<span class="kwrd">new</span> CharSequenceJavaFileObject(fullName1, src1)); <span class="rem">// We specify a task to the compiler. Compiler should use our file manager and our list of "files".</span> <span class="rem">// Then we run the compilation with call()</span> <span class="kwrd">if</span> (compiler.getTask(<span class="kwrd">null</span>, fileManager, <span class="kwrd">null</span>, <span class="kwrd">null</span>, <span class="kwrd">null</span>, jfiles).call()) { <span class="rem">// Creating an instance of our compiled class and running its toString() method</span> Object instance = fileManager.getClassLoader(<span class="kwrd">null</span>).loadClass(fullName1).newInstance(); System.<span class="kwrd">out</span>.println(instance); } <span class="kwrd">else</span> { System.<span class="kwrd">out</span>.println(<span class="str">"### failed"</span>); } }
}
class CharSequenceJavaFileObject extends SimpleJavaFileObject {
<span class="rem">/**</span>
* CharSequence representing the source code to be compiled
*/<span class="kwrd">private</span> CharSequence content; <span class="rem">/**</span>
* This constructor will store the source code in the internal “content” variable and register it as a source code,
* using a URI containing the class full name
* @param className name of the public class in the source code
* @param content source code to compile
/<span class="kwrd">public</span> CharSequenceJavaFileObject(String className, CharSequence content) { super(URI.create(<span class="str">"string:///"</span> + className.replace(<span class="str">'.'</span>, <span class="str">'/'</span>) + Kind.SOURCE.extension), Kind.SOURCE); <span class="kwrd">this</span>.content = content; } <span class="rem">/**</span>
* Answers the CharSequence to be compiled. It will give the source code stored in variable “content”
*/@Override <span class="kwrd">public</span> CharSequence getCharContent(boolean ignoreEncodingErrors) { <span class="kwrd">return</span> content; }
}
class JavaClassObject extends SimpleJavaFileObject {
<span class="rem">/**</span>
* Byte code created by the compiler will be stored in this ByteArrayOutputStream
* so that we can later get the byte array out of it and put it in the memory as an instance of our class.
*/<span class="kwrd">protected</span> final ByteArrayOutputStream bos = <span class="kwrd">new</span> ByteArrayOutputStream(); <span class="rem">/**</span>
* Registers the compiled class object under URI containing the class full name
* @param name Full name of the compiled class
* @param kind Kind of the data. It will be CLASS in our case
/<span class="kwrd">public</span> JavaClassObject(String name, Kind kind) { super(URI.create(<span class="str">"string:///"</span> + name.replace(<span class="str">'.'</span>, <span class="str">'/'</span>) + kind.extension), kind); } <span class="rem">/**</span>
* Will be used by our file manager to get the byte code that can be put into memory to instantiate our class
* @return compiled byte code
/<span class="kwrd">public</span> <span class="kwrd">byte</span>[] getBytes() { <span class="kwrd">return</span> bos.toByteArray(); } <span class="rem">/**</span>
* Will provide the compiler with an output stream that leads to our byte array.
* This way the compiler will write everything into the byte array that we will instantiate later
*/@Override <span class="kwrd">public</span> OutputStream openOutputStream() throws IOException { <span class="kwrd">return</span> bos; }
}
class ClassFileManager extends ForwardingJavaFileManager {
<span class="rem">/**</span>
* Instance of JavaClassObject that will store the compiled bytecode of our class
*/<span class="kwrd">private</span> Map<String, JavaClassObject> map = <span class="kwrd">new</span> HashMap<String, JavaClassObject>(); <span class="rem">/**</span>
* Will initialize the manager with the specified standard java file manager
*/<span class="kwrd">public</span> ClassFileManager(StandardJavaFileManager standardManager) { super(standardManager); } <span class="rem">/**</span>
* Will be used by us to get the class loader for our compiled class.
* It creates an anonymous class extending the SecureClassLoader which uses the byte code created by the compiler
* and stored in the JavaClassObject, and returns the Class for it
*/@Override <span class="kwrd">public</span> ClassLoader getClassLoader(Location location) { <span class="kwrd">return</span> <span class="kwrd">new</span> SecureClassLoader() { @Override <span class="kwrd">protected</span> Class<?> findClass(String name) throws ClassNotFoundException { JavaClassObject jclassObject = map.get(name); <span class="kwrd">byte</span>[] b = jclassObject.getBytes(); <span class="kwrd">return</span> super.defineClass(name, jclassObject.getBytes(), 0, b.length); } }; } <span class="rem">/**</span>
* Gives the compiler an instance of the JavaClassObject so that the compiler can write the byte code into it.
*/@Override <span class="kwrd">public</span> JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException { map.put(className, <span class="kwrd">new</span> JavaClassObject(className, kind)); <span class="kwrd">return</span> map.get(className); }
}```