49

(This question is similar to many questions I have seen but most are not specific enough for what I am doing)

Background:

The purpose of my program is to make it easy for people who use my program to make custom "plugins" so to speak, then compile and load them into the program for use (vs having an incomplete, slow parser implemented in my program). My program allows users to input code into a predefined class extending a compiled class packaged with my program. They input the code into text panes then my program copies the code into the methods being overridden. It then saves this as a .java file (nearly) ready for the compiler. The program runs javac (java compiler) with the saved .java file as its input.

My question is, how do I get it so that the client can (using my compiled program) save this java file (which extends my InterfaceExample) anywhere on their computer, have my program compile it (without saying "cannot find symbol: InterfaceExample") then load it and call the doSomething() method?

I keep seeing Q&A's using reflection or ClassLoader and one that almost described how to compile it, but none are detailed enough for me/I do not understand them completely.

Shadowtrot
  • 710
  • 1
  • 7
  • 13
  • 1
    check out JANINO. i use it to create classes from java source at runtime. i prefer to have them compile direct to memory and into a classloader. no need to even save the .class files. http://docs.codehaus.org/display/JANINO/Home – slipperyseal Feb 04 '14 at 05:23
  • I would use that, but the way my program works loading straight into memory might not be what users want, and the compiled classes need to be easily found and shared between users. This might be a useful answer for some people though. – Shadowtrot Feb 08 '14 at 23:45
  • cool. Janino can output to files as well, but if that's what you want, i recommend the JavaCompiler as per the accepted answer. – slipperyseal Feb 09 '14 at 08:10
  • https://medium.com/@davutgrbz/the-need-history-c91c9d38ec9 – Davut Gürbüz May 10 '20 at 11:27

2 Answers2

77

Take a look at JavaCompiler

The following is based on the example given in the JavaDocs

This will save a File in the testcompile directory (based on the package name requirements) and the compile the File to a Java class...

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();
    }

}

Now updated to include suppling a classpath for the compiler and loading and execution of the compiled class!

MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • I was wondering what can be the security issues if we compile user-input code and how to protect our system against that ? Thanks – c4k Jan 02 '17 at 15:12
  • @c4k Since the user has access to the entire API, they could pretty much do what they want, without going to the extent of having your own library implementation, you could sandbox the user and/or execute the code as an unprivileged user – MadProgrammer Jan 02 '17 at 20:03
  • @MadProgrammer thanks for the explanation. To go further I found this paragraph on Janino's website http://janino-compiler.github.io/janino/#security. It might be worth reading before going deeper on this ;) – c4k Jan 03 '17 at 11:15
38

I suggest using the Java Runtime Compiler library. You can give it a String in memory and it will compile and load the class into the current class loader (or one of your choice) and return the Class loaded. Nested classes are also loaded. Note: this works entirely in memory by default.

e.g.

 // 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();
Art Licis
  • 3,619
  • 1
  • 29
  • 49
Peter Lawrey
  • 525,659
  • 79
  • 751
  • 1,130
  • What if MyClass implements some interface which already exists at runtime? A can't get it work in Spring Boot application! – CHEM_Eugene Jan 28 '20 at 08:33
  • @CHEM_Eugene You can't change a class which has already been loaded this way. You need to add it to a new ClassLoader. – Peter Lawrey Feb 13 '20 at 18:43