Java Compiler API - Compile Code to a File and Load it using Default Class Loader

[Updated: Jan 23, 2016, Created: Dec 27, 2015]

In the previous topic we saw how to compile java sources from files. In this example we are going to show how to compile a string object containing java code. Here are the steps:

  1. Extend SimpleJavaFileObject, let's say as class JavaStringObject
  2. Override getCharContent(..) in JavaStringObject to return our in memory string object instead of reading some file content from disk.
    public class JavaStringObject extends SimpleJavaFileObject {
        private final String source;
    
        protected JavaStringObject(String name, String source) {
            super(URI.create("string:///" + name.replaceAll("\\.", "/") +
                    Kind.SOURCE.extension), Kind.SOURCE);
            this.source = source;
        }
    
        @Override
        public CharSequence getCharContent(boolean ignoreEncodingErrors)
                                                     throws IOException {
            return source;
        }
    }
  3. Pass the instance of JavaStringObject (bundled in Iterable) as the last argument to JavaCompiler#getTask(..).
  4. We are also going to skip standardFileManager.getJavaFileObjects(files) call, as it returns instance of the JavaFileObject (On debugging, I found the default implementation comes as com.sun.tools.javac.file.RegularFileObject) which handles reading source content from a file.
    public class StringCompilation {
    
        public static void main(String[] args) {
            JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
    
            DiagnosticCollector<JavaFileObject> diagnostics =
                                             new DiagnosticCollector<>();
    
            JavaCompiler.CompilationTask task = compiler.getTask(null,
                    null, diagnostics, null, null, getCompilationUnits());
    
            if (!task.call()) {
                diagnostics.getDiagnostics().forEach(System.out::println);
            }
        }
    
        public static Iterable<? extends JavaFileObject> getCompilationUnits() {
            JavaStringObject stringObject =
                                       new JavaStringObject("Test", getSource());
            return Arrays.asList(stringObject);
        }
    
        public static String getSource() {
            return "public class Test{" +
                    "public void doSomething(){" +
                    "System.out.println(\"testing\");}}";
        }
    }
    

Loading the Compiled Source

Classes compiled this way can only be loaded (via reflection) if they are already in the classpath. The compiled file, in our example, goes to the project root if we are running it from an IDE. I'm using Intellij:

That means we have to set the root of the project as classpath before running the main method. There's currently no standard way to set classpath during runtime.

Example Project

Here's the complete example project. Notice I implemented a predefined interface ITest to our generated class Test so that we won't be doing everything (method calling) via reflection.

Dependencies and Technologies Used :

  • JDK 1.8
  • Maven 3.0.4

Compiler Api String Source Example Select All Download
  • compiling-string-example
    • src
      • main
        • java
          • com
            • logicbig
              • example

Running from Jar

Running the application from jar (I packaged it using: mvn package) will compile the class file Test.class next to the jar file.

D:\compiling-string-example\target>dir
 Volume in drive D is Data
 Volume Serial Number is 68F9-EDFA
 
Directory of D:\compiling-string-example\target
 
12/27/2015  05:04 PM    <DIR>          .
12/27/2015  05:04 PM    <DIR>          ..
12/27/2015  04:24 PM    <DIR>          classes
12/27/2015  05:01 PM             9,700 compiling-string-example-1.0-SNAPSHOT.jar
12/27/2015  04:24 PM    <DIR>          generated-sources
12/27/2015  04:24 PM    <DIR>          maven-archiver
12/27/2015  05:01 PM    <DIR>          surefire
12/27/2015  04:08 PM               421 Test.class
               2 File(s)         10,121 bytes
               6 Dir(s)  84,648,378,368 bytes free

That means we can run it like this:

D:\compiling-string-example\target>java -cp
    .;compiling-string-example-1.0-SNAPSHOT.jar
    com.logicbig.example.StringCompilation
testing

Also be careful about a JRE 1.7 and JRE 1.8 bug. If you have a null pointer exception on the compiler instance (ToolProvider.getSystemJavaCompiler() can return null), you have to set JDK path instead of JRE. JRE 6.x works fine.


Setting an Explicit Output Path

We can control the class output folder by using fileManager.setLocation.

JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

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

StandardJavaFileManager fileManager =
        compiler.getStandardFileManager(diagnostics, null, null);

//should be platform independent path
//also the folder should already exist
fileManager.setLocation(StandardLocation.CLASS_OUTPUT,
        Arrays.asList(new File("d:\\compiledClasses")));

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

if (!task.call()) {
    diagnostics.getDiagnostics().forEach(System.out::println);
}
fileManager.close();

//loading and using our compiled class
Class<ITest> test = (Class<ITest>) Class.forName("Test");
ITest iTest = test.newInstance();
iTest.doSomething();

But of course it doesn't change the running class path. It will just compile the files in that folder. We still have to set our runtime path before running the app.

D:\compiling-string-example\target>java -cp
    .;D:\compiledClasses;compiling-string-example-1.0-SNAPSHOT.jar
    com.logicbig.example.StringCompilation
testing

See Also