如何动态编译和加载外部 java 类? [复制]

Posted

技术标签:

【中文标题】如何动态编译和加载外部 java 类? [复制]【英文标题】:How do you dynamically compile and load external java classes? [duplicate] 【发布时间】:2014-02-27 22:44:42 【问题描述】:

(这个问题与我见过的许多问题相似,但大多数问题对我正在做的事情不够具体)

背景:

我的程序的目的是让使用我的程序的人可以轻松地制作自定义“插件”,然后编译并将它们加载到程序中以供使用(与在我的程序中实现的不完整、缓慢的解析器相比)程序)。我的程序允许用户将代码输入到预定义的类中,该类扩展了与我的程序一起打包的已编译类。他们将代码输入到文本窗格中,然后我的程序将代码复制到被覆盖的方法中。然后它将它保存为一个 .java 文件(几乎)为编译器准备好。程序以保存的 .java 文件作为输入运行 javac(java 编译器)。

我的问题是,我如何获取它以便客户端可以(使用我编译的程序)将此 java 文件(扩展我的 InterfaceExample)保存在他们计算机上的任何位置,让我的程序编译它(不说“找不到符号: InterfaceExample") 然后加载它并调用 doSomething() 方法?

我不断看到使用反射或 ClassLoader 的问答,还有一个几乎描述了如何编译它,但没有一个对我来说足够详细/我不完全理解它们。

【问题讨论】:

查看 JANINO。我用它在运行时从 java 源创建类。我更喜欢让它们直接编译到内存并编译到类加载器中。甚至不需要保存 .class 文件。 docs.codehaus.org/display/JANINO/Home 我会使用它,但是我的程序直接加载到内存中的方式可能不是用户想要的,并且编译的类需要在用户之间轻松找到和共享。不过,这对某些人来说可能是一个有用的答案。 酷。 Janino 也可以输出到文件,但如果这是你想要的,我推荐 JavaCompiler 根据接受的答案。 medium.com/@davutgrbz/the-need-history-c91c9d38ec9 【参考方案1】:

看看JavaCompiler

以下内容基于JavaDocs中给出的示例

这将在testcompile 目录中保存File(基于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 由于用户可以访问整个 API,他们几乎可以做他们想做的事,而无需达到拥有自己的库实现的程度,您可以沙箱用户和/或执行代码作为非特权用户 @MadProgrammer 感谢您的解释。为了更进一步,我在 Janino 的网站janino-compiler.github.io/janino/#security 上找到了这一段。在深入研究之前可能值得一读;)【参考方案2】:

我建议使用Java Runtime Compiler 库。您可以在内存中给它一个字符串,它会编译该类并将其加载到当前的类加载器(或您选择的一个)中并返回加载的类。嵌套类也被加载。注意:默认情况下,这完全在内存中工作。

例如

 // 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 实现了一些在运行时已经存在的接口怎么办? A 不能让它在 Spring Boot 应用程序中工作! @CHEM_Eugene 您不能更改已经以这种方式加载的类。您需要将其添加到新的 ClassLoader。

以上是关于如何动态编译和加载外部 java 类? [复制]的主要内容,如果未能解决你的问题,请参考以下文章

java jvm虚拟机类加载器

java可以动态加载一个jar包,并且调用里面的类和方法吗?

[JAVA冷知识]动态加载不适合数组类?那如何动态加载一个数组类?

Java - 如何用 Class.forName 加载外部 Jar 里的类文件?

[JAVA冷知识]动态加载不适合数组类?那如何动态加载一个数组类?

java反射--动态加载