how to add dependencies when running the Java compiler programmatically

931 Views Asked by At

Hi I am trying to compile a java code(which contains classes from third party jars i.e; dependencies) dynamically and execute the same but its not working for me.

Ex : If i want to execute a simple java code (System.out.println) it works fine but if i add any third party jar files (ex: selenium). It throws error that symbol not found.

 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("import org.sikuli.script.*;\n");
            sb.append("public class HelloWorld implements inlinecompiler.InlineCompiler.DoStuff {\n");
            sb.append("    public void doStuff() {\n");
 sb.append("Screen s = new Screen();\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") + ";sikuli.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();
        }

    }

the code should run but it may through error symbol not found.

1

There are 1 best solutions below

0
Christian Hujer On

First of all, I suggest that when printing the diagnostics, add the message. Change the part that prints the error message to this:

System.out.format("Error %s on line %d in %s%n",
    diagnostic.getMessage(null), // ⇐ Message, very useful
    diagnostic.getLineNumber(),
    diagnostic.getSource().toUri());

This will help with understanding what the compiler thought went wrong.

On Linux or Mac OS, we now see a problem: Even with a sikuli.jar containig a Screen.class in the right place, that class Screen is not found. Why? Beause Java isn't looking for a sikuli.jar file. Why? Because the wrong path concatenation character is used. The path concatenation character is ; on Windows and : on POSIX-y systems. Instead of hard-coding ;, we should use System.getProperty("path.separator").

I could now see 3 compiler errors (and one message that says "Error" but is actually a warning):

  • cannot find symbol "class DoStuff"
  • cannot find symbol "class Screen" (twice)

So, change the line that builds the classpath entry in the optionList to this:

optionList.add(System.getProperty("java.class.path") + System.getProperty("path.separator") + "sikuli.jar");

After this change, the code works.

Here's the directory structure that I used:

playground/ ⇐ current working directory
+- inlinecompiler/
|  +- Compiler.java
+- sikuli.jar ⇐ contains org/sikuli/script/Screen.class

And the commands:

javac inlinecompiler/Compiler.java
java -cp .:sikuli.jar inlinecompiler.Compiler

Which means if it doesn't work, also check your directory structure.

Last but not least, java.lang.Compiler is deprecated and scheduled for removal. You might want to switch to javax.tools.JavaCompiler. You can obtain an instance using:

JavaCompiler compiler = ServiceLoader.load(JavaCompiler.class).iterator().next();