使用Maven进行动态类编译和加载

4

我在使用mvn exec:java命令时,遇到了一些动态编译和加载类的问题。

我根据这篇教程创建了一个小例子,在IntelliJ中可以正常工作,但是在命令行中执行时失败了。

这是我的main类:

public class Debug {

    public static void main(String[] args) {
        DynamicCompiler compiler = new DynamicCompiler();
        SayHello hello = compiler.getHello();
        hello.sayHello();
    }
}

SayHello 接口:

public interface SayHello {
    public void sayHello();
}

DynamicCompiler类:

import javax.tools.*;
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.Arrays;
import java.util.List;

public class DynamicCompiler {

    private final static String CLASS_PATH = "Hello";

    private final static String SOURCE;

    static {
        StringBuilder builder = new StringBuilder();
        builder.append("public class Hello implements SayHello {\n")
                .append("    public void sayHello() {\n")
                .append("        System.out.println(\"Hello World\")\n;")
                .append("    }\n")
                .append("}");
        SOURCE = builder.toString();
    }

    public SayHello getHello() {
        return compileAndLoadSource(SOURCE, CLASS_PATH);
    }

    private SayHello compileAndLoadSource(String src, String fullName) {
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        JavaFileManager fileManager = new 
            ClassFileManager(compiler.getStandardFileManager(null, null, null));

        List<JavaFileObject> jfiles = new ArrayList<JavaFileObject>();
        jfiles.add(new CharSequenceJavaFileObject(fullName, src));

        List<String> optionList = new ArrayList<String>();

        DiagnosticCollector<JavaFileObject> diagnostics = new 
             DiagnosticCollector<JavaFileObject>();


        JavaCompiler.CompilationTask task =
                compiler.getTask(null, fileManager, diagnostics, optionList, 
                                 null, jfiles);

        boolean success = task.call();
        if (!success) {
            System.out.println("UNSUCCESSFUL:");
            for (Diagnostic diagnostic : diagnostics.getDiagnostics()) {
                System.out.println(diagnostic.getCode());
                System.out.println(diagnostic.getKind());
                System.out.println(diagnostic.getPosition());
                System.out.println(diagnostic.getStartPosition());
                System.out.println(diagnostic.getEndPosition());
                System.out.println(diagnostic.getSource());
                System.out.println(diagnostic.getMessage(null));
            }
            return null;
        }

        try {
            Object instance = fileManager.getClassLoader(null).loadClass(fullName).newInstance();
            return (SayHello) instance;
        } catch (InstantiationException e) {
            e.printStackTrace();
            return null;
        } catch (IllegalAccessException e) {
            e.printStackTrace();
            return null;
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            return null;
        }
    }

    public static class JavaClassObject extends SimpleJavaFileObject {

        /**
         * 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.
         */
        protected final ByteArrayOutputStream bos = new ByteArrayOutputStream();

        /**
         * 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
         */
        public JavaClassObject(String name, Kind kind) {
            super(URI.create("string:///" + name.replace('.', '/') + kind.extension), kind);
        }

        /**
         * 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
         */
        public byte[] getBytes() {
            return bos.toByteArray();
        }

        /**
         * 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
        public OutputStream openOutputStream() throws IOException {
            return bos;
        }
    }

    public static class CharSequenceJavaFileObject extends SimpleJavaFileObject {

        /**
         * CharSequence representing the source code to be compiled
         */
        private CharSequence content;

        /**
         * 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
         */
        public CharSequenceJavaFileObject(String className, CharSequence content) {
            super(URI.create("string:///" + className.replace('.', '/') + JavaFileObject.Kind.SOURCE.extension),
                    JavaFileObject.Kind.SOURCE);
            this.content = content;
        }

        /**
         * Answers the CharSequence to be compiled. It will give
         * the source code stored in variable "content"
         */
        @Override
        public CharSequence getCharContent(boolean ignoreEncodingErrors) {
            return content;
        }
    }

    public static class ClassFileManager extends ForwardingJavaFileManager {
        /**
         * Instance of JavaClassObject that will store the
         * compiled bytecode of our class
         */
        private JavaClassObject jclassObject;

        /**
         * Will initialize the manager with the specified
         * standard java file manager
         *
         * @param standardManager
         */
        public ClassFileManager(StandardJavaFileManager standardManager) {
            super(standardManager);
        }

        /**
         * 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
        public ClassLoader getClassLoader(Location location) {
            return new SecureClassLoader() {
                @Override
                protected Class<?> findClass(String name) throws ClassNotFoundException {
                    byte[] b = jclassObject.getBytes();
                    return super.defineClass(name, jclassObject.getBytes(), 0,
                           b.length);
                }
            };
        }

        /**
         * Gives the compiler an instance of the JavaClassObject
         * so that the compiler can write the byte code into it.
         */
        @Override
        public JavaFileObject getJavaFileForOutput(Location location, String className,
             JavaFileObject.Kind kind, FileObject sibling) throws IOException {
            jclassObject = new JavaClassObject(className, kind);
            return jclassObject;
        }
    }
}

最后,我的pom.xml文件。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>Debug</groupId>
    <artifactId>Debug</artifactId>
    <version>1.0-SNAPSHOT</version>

    <build>
        <finalName>pipe</finalName>
        <plugins>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>1.2.1</version>
                <configuration>
                    <mainClass>Debug</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>


</project>

当我使用命令mvn compile exec:java运行此示例时,出现以下错误:
 [INFO] --- exec-maven-plugin:1.2.1:java (default-cli) @ Debug ---
 UNSUCCESSFUL:
 compiler.err.cant.resolve
 ERROR
 30
 30
 38
 string:///Hello.java from CharSequenceJavaFileObject
 string:///Hello.java:1: cannot find symbol
 symbol: class SayHello

我认为问题在于需要指定类路径和输出目录,但我不确定最好的解决方法是什么,我尝试添加了以下行:

    optionList.addAll(Arrays.asList("-classpath", "target/classes", "-d", "target/classes"));

这样可以编译,但仍然无法找到该类。我也不喜欢使用“target/classes”,因为我认为它太粗糙了。
我真的很希望能被展示正确的方法!
更新 - 我需要Maven依赖项保持不变。 这里是更新后的“main”:
import org.jfree.data.xy.XYDataItem;

public class Debug {

    public static void main(String[] args) {
        DynamicCompiler compiler = new DynamicCompiler();
        SayHello hello = compiler.getHello();
        hello.sayHello();

        //Random new XYDataItem in order to produce the dependency error
        XYDataItem xyDataItem = new XYDataItem(10,10);
    }
}

已更新的 pom.xml http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0

    <groupId>Debug</groupId>
    <artifactId>Debug</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>jfree</groupId>
            <artifactId>jfreechart</artifactId>
            <version>1.0.0</version>
        </dependency>
    </dependencies>

    <build>
        <finalName>pipe</finalName>
        <plugins>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>1.2.1</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>exec</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <executable>java</executable>
                    <arguments>
                        <argument>-cp</argument>
                        <argument>target/classes/</argument>
                        <argument>Debug</argument>
                    </arguments>
                </configuration>
            </plugin>
        </plugins>
    </build>


</project>

我现在看到了这个错误:
    [INFO] --- exec-maven-plugin:1.2.1:exec (default-cli) @ Debug ---
Hello World
Exception in thread "main" java.lang.NoClassDefFoundError: org/jfree/data/xy/XYDataItem
    at Debug.main(Debug.java:9)
Caused by: java.lang.ClassNotFoundException: org.jfree.data.xy.XYDataItem
    at java.net.URLClassLoader$1.run(URLClassLoader.java:202)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:190)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:306)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:301)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:247)
    ... 1 more
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------

我怀疑这与类路径有关,唯一的问题是我无法确定再次指向哪里,我尝试使用maven-shade-plugin,但似乎没有任何作用。

谢谢!

1个回答

1
生成一个新的Java进程,即使用以下pom.xml执行mvn compile exec:exec
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>

        <groupId>Debug</groupId>
        <artifactId>Debug</artifactId>
        <version>1.0-SNAPSHOT</version>

        <build>
            <finalName>pipe</finalName>
            <plugins>
                <plugin>
                    <groupId>org.codehaus.mojo</groupId>
                    <artifactId>exec-maven-plugin</artifactId>
                    <version>1.2.1</version>
                    <executions>
                        <execution>
                            <goals>
                                <goal>exec</goal>
                            </goals>
                        </execution>
                    </executions>
                    <configuration>
                        <executable>java</executable>
                        <arguments>
                            <argument>-cp</argument>
                            <classpath/>
                            <argument>Debug</argument>
                        </arguments>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    </project>

"

<classpath/>将被替换为项目的类路径。

PS. 感谢您分享您的代码,我不知道在运行时编译Java类是可能的!

"

只要代码没有依赖关系,这个可以工作。如果添加外部依赖项(如我的更新帖子中所述),它会出现NoClassDefFoundError错误。我想路径需要进行更改,但我不知道应该指向哪里! - Sarah Tattersall
哪个类找不到?SayHello接口吗?如果它不在默认包中,你必须添加包名,即public class Hello implements my.package.SayHello - Peter Keller
我已经添加了一个包含XYDataItem的示例,该示例来自于Maven依赖项。代码是可运行的,因此您应该很容易地看到我遇到的错误。 - Sarah Tattersall
1
在这种情况下,您必须在pom.xml中添加<classpath/>参数。请参见我在原始答案中的更改。 - Peter Keller

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接