(这个问题与我所见过的许多问题相似,但大多数问题对我的工作不够具体)
背景:
我程序的目的就是说,使使用我的程序的人可以轻松地创建自定义“插件”,然后将其编译并加载到程序中使用(与在我的程序中实现的不完整,缓慢的解析器相比)。我的程序允许用户将代码输入到预定义的类中,以扩展与我的程序一起打包的已编译类。他们将代码输入到文本窗格中,然后我的程序将代码复制到要重写的方法中。然后,将其保存为(几乎)准备好供编译器使用的.java文件。该程序以保存的.java文件作为输入来运行javac(java编译器)。我的问题是,如何获取它,以便客户端(使用我的编译程序)可以保存此java文件。 (这扩展了我的InterfaceExample)在他们计算机上的任何位置,让我的程序对其进行编译(不说“找不到符号:InterfaceExample”),然后加载它并调用doSomething()方法吗?
我一直在查看问答使用反射或ClassLoader以及几乎描述了如何编译它的方法,但是对于我/我而言,它们不够详细,我无法完全理解它们。
#1 楼
看看JavaCompiler
以下内容基于JavaDocs中给出的示例
,这会将
File
保存在testcompile
目录中(基于package
名称要求),并且将File
编译为Java类... package inlinecompiler;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
public class InlineCompiler {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder(64);
sb.append("package testcompile;\n");
sb.append("public class HelloWorld implements inlinecompiler.InlineCompiler.DoStuff {\n");
sb.append(" public void doStuff() {\n");
sb.append(" System.out.println(\"Hello world\");\n");
sb.append(" }\n");
sb.append("}\n");
File helloWorldJava = new File("testcompile/HelloWorld.java");
if (helloWorldJava.getParentFile().exists() || helloWorldJava.getParentFile().mkdirs()) {
try {
Writer writer = null;
try {
writer = new FileWriter(helloWorldJava);
writer.write(sb.toString());
writer.flush();
} finally {
try {
writer.close();
} catch (Exception e) {
}
}
/** Compilation Requirements *********************************************************************************************/
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<JavaFileObject>();
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null);
// This sets up the class path that the compiler will use.
// I've added the .jar file that contains the DoStuff interface within in it...
List<String> optionList = new ArrayList<String>();
optionList.add("-classpath");
optionList.add(System.getProperty("java.class.path") + File.pathSeparator + "dist/InlineCompiler.jar");
Iterable<? extends JavaFileObject> compilationUnit
= fileManager.getJavaFileObjectsFromFiles(Arrays.asList(helloWorldJava));
JavaCompiler.CompilationTask task = compiler.getTask(
null,
fileManager,
diagnostics,
optionList,
null,
compilationUnit);
/********************************************************************************************* Compilation Requirements **/
if (task.call()) {
/** Load and execute *************************************************************************************************/
System.out.println("Yipe");
// Create a new custom class loader, pointing to the directory that contains the compiled
// classes, this should point to the top of the package structure!
URLClassLoader classLoader = new URLClassLoader(new URL[]{new File("./").toURI().toURL()});
// Load the class from the classloader by name....
Class<?> loadedClass = classLoader.loadClass("testcompile.HelloWorld");
// Create a new instance...
Object obj = loadedClass.newInstance();
// Santity check
if (obj instanceof DoStuff) {
// Cast to the DoStuff interface
DoStuff stuffToDo = (DoStuff)obj;
// Run it baby
stuffToDo.doStuff();
}
/************************************************************************************************* Load and execute **/
} else {
for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()) {
System.out.format("Error on line %d in %s%n",
diagnostic.getLineNumber(),
diagnostic.getSource().toUri());
}
}
fileManager.close();
} catch (IOException | ClassNotFoundException | InstantiationException | IllegalAccessException exp) {
exp.printStackTrace();
}
}
}
public static interface DoStuff {
public void doStuff();
}
}
现在已更新,包括为编译器提供类路径以及加载和执行已编译的类! >
评论
我想知道如果我们编译用户输入的代码会带来什么安全问题,以及如何保护我们的系统免受此伤害?谢谢
–c4k
17年1月2日在15:12
@ c4k由于用户可以访问整个API,因此他们几乎可以做自己想做的事,而不必考虑自己的库实现的程度,因此您可以沙盒用户和/或以非特权用户身份执行代码。
– MadProgrammer
17年1月2日在20:03
@MadProgrammer感谢您的解释。更进一步,我在Janino的网站janino-compiler.github.io/janino/#security上找到了此段。在深入研究之前,可能值得阅读;)
–c4k
17年1月3日,11:15
#2 楼
我建议使用Java Runtime Compiler库。您可以在内存中给它一个String,它将编译该类并将其加载到当前的类加载器(或您选择的一种)中,并返回已加载的Class。嵌套类也被加载。注意:默认情况下,这完全在内存中起作用。例如
// dynamically you can call
String className = "mypackage.MyClass";
String javaCode = "package mypackage;\n" +
"public class MyClass implements Runnable {\n" +
" public void run() {\n" +
" System.out.println(\"Hello World\");\n" +
" }\n" +
"}\n";
Class aClass = CompilerUtils.CACHED_COMPILER.loadFromJava(className, javaCode);
Runnable runner = (Runnable) aClass.newInstance();
runner.run();
评论
如果MyClass实现了一些在运行时已经存在的接口怎么办?在Spring Boot应用程序中无法正常工作!
–CHEM_Eugene
1月28日8:33
@CHEM_Eugene您不能更改已经通过这种方式加载的类。您需要将其添加到新的ClassLoader中。
– Peter Lawrey
2月13日18:43
评论
查看JANINO。我用它在运行时从Java源创建类。我更喜欢让它们直接编译到内存并装入类加载器。甚至不需要保存.class文件。 docs.codehaus.org/display/JANINO/Home我会使用它,但是我的程序将工作方式直接加载到内存中的方式可能不是用户想要的,并且需要轻松地找到并在用户之间共享已编译的类。对于某些人来说,这可能是一个有用的答案。
凉。 Janino也可以输出到文件,但是如果您要这样做,我建议您按照公认的答案使用JavaCompiler。
medium.com/@davutgrbz/the-need-history-c91c9d38ec9