Saturday, June 13, 2009

Programtically compile Java source code

There are times we need to compile Java code from our application. For example, if we are dynamically generating some specific Java code to be packaged and sent to some customer.
Of course we can use "javac.exe" in order to achieve our goal. But executing "javac" is less programmatic way of doing the job.
Ever since Java 1.6 there are set of tools allowing us to compile Java sources directly from Java code. We will make use these tools in order to build a neat Java source code compiler, that will allow us to compile java files as well as java strings representing source code.
We will make use of the following classes supplied by the Java language:
  • StandardJavaFileManager: This class is used for adding classes and jars to the compiler as well as determining the output of the compiled classes.
  • JavaCompiler: This class is the actual compiler responsible of compiling all the the sources and other resources added by StandardJavaFileManager.
  • JavaCompiler.CompilationTask: This is the actual compilation task. The compilation takes place by calling it method: call.
  • SimpleJavaFileObject: We will extend this class in order to introduce the ability to add directly a String as a Java source code.
Out main class is called: JavaCodeCompiler. This is a wrapper to the Java compiler classes. This class introduces the following important methods:
  • addSource: This method adds all the Java classes under a given directory. It knows to handle directories and subdirectories as well. In addition, there is another overloaded addSource method, that allows adding a string represting a Java class. This is sometimes useful, when we would like to dynamically generate a Java source code.
  • compile: This methods does the actual compilation. It takes all the resources we added, compiles them and put the result in the output directory we defined in the class constructor.

This is how the JavaCodeCompiler looks like:

package com.bashan.blog.compile;
import javax.tools.*;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class JavaCodeCompiler {
  private static final Pattern PATTERN_CLASS_NAME =
        Pattern.compile(".*?class\\s*?([a-zA-Z_][a-zA-Z0-9_]*)\\s*?", Pattern.DOTALL);
  private JavaCompiler compiler;
  private File dirOutClasses;
  private StandardJavaFileManager filemanager;
  protected List<File> fileSources;
  protected List<JavaFileObject> sources;
  public JavaCodeCompiler(File dirOutClasses) throws IOException {
    compiler = ToolProvider.getSystemJavaCompiler();
    filemanager = compiler.getStandardFileManager(null, null, null);
    this.dirOutClasses = dirOutClasses;
    fileSources = new ArrayList<File>();
    sources = new ArrayList<JavaFileObject>();
  }
  public void addSource(File dir) throws Exception {
    fileSources.clear();
    addDir(dir);
    sources = (List<JavaFileObject>) filemanager.
        getJavaFileObjects(fileSources.toArray(new File[fileSources.size()]));
  }
  private void addDir(File dir) {
    File[] files = dir.listFiles();
    for (File file : files) {
      if (file.isDirectory()) {
        addDir(file);
      } else if (file.getPath().endsWith(JavaFileObject.Kind.SOURCE.extension)) {
        fileSources.add(file);
      }
    }
  }
  private String getClassName(String code)
  {
    Matcher matcher = PATTERN_CLASS_NAME.matcher(code);
    if (matcher.find())
    {
      return matcher.group(1);
    }
    return null;
  }
  public void addSource(String code) {
    String className = getClassName(code);
    sources.add(new JavaSourceString(className, code));
  }
  public void compile() throws IOException, CompileException {
    try {
      dirOutClasses.mkdirs();
      filemanager.setLocation(StandardLocation.CLASS_OUTPUT, Collections.singleton(dirOutClasses));
      JavaCompiler.CompilationTask task = compiler.getTask(null, filemanager, null, null, null, sources);
      if (!task.call()) {
        throw new CompileException("Failed compiling classes");
      }
    }
    finally {
      filemanager.close();
    }
  }
}

In addition to this class, there are 2 more classes used. One is: CompileException. This class is defined since the Java compilation task returns “true” or “false” as success/failure indication, an we would like to generate a more proper mechanism that will force user to deal with compilation failures. This is how this class looks:
package com.bashan.blog.compile;
public class CompileException extends Exception {
  public CompileException() {
  }
  public CompileException(String message) {
    super(message);
  }
  public CompileException(String message, Throwable cause) {
    super(message, cause);
  }
  public CompileException(Throwable cause) {
    super(cause);
  }
}
And the final class named: JavaSourceString extends the class SimpleJavaFileObject which supplied by Java. It gives the ability to add String Java source code. This is how it looks:
package com.bashan.blog.compile;
import javax.tools.SimpleJavaFileObject;
import java.net.URI;
public class JavaSourceString extends SimpleJavaFileObject {
  final String code;
  JavaSourceString(String name, String code) {
    super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);
    this.code = code;
  }
  @Override
  public CharSequence getCharContent(boolean ignoreEncodingErrors) {
    return code;
  }
}

This is a basic Java Source code compiler. You can extend its capabilities further more, for example: allowing to add directly a Velocity template representing a dynamically created Java class.

You can download the classes in this post directly by using this link.

No comments:

Post a Comment