Skip to main content

How compile java code dynamically?

Posted by otaviojava on January 19, 2014 at 7:14 PM PST

The Java is a platform which grown up quickly, for many rations such it can write once time and run anywhere, it runs languages on JVM. There is a myth that Java cannot compile its sources on run time, but it's truth?

Have a dynamic language is important to make some projects, for example, calculate taxes to different cities, so the source should be on the database and each city it puts the specific formula.

To show this functionality we will use a sample to solved the problem above, do change of formula on run time, to be easy and get focus on demonstration, we will use the four simple operations (add, multiply, subtract and divide) and to simulate the database, will have the txt file with Java's source inside. How cannot reference a class that was not created yet, we will make a interface to all classes on our “data bases” will implement it.

public interface Calculo { 

Double calcular(Number valorA, Number valorB);

}

The interface what will be implemented how reference to anothers which classes will compiled on run time.

Following this steps:

The JavaCompiler has responsibility to do the compilation of the source. The call
ToolProvider.getSystemJavaCompiler() returns this object. If there isn't a Java Compiler installed will return null. The JavaCompiler has the getTask() method, that returns a CompilationTask object. Then with this object, using the call() method it does the compilation process and will back a boolean to indicate the success (returns true) or not (returns false).

public class JavaDinamicoCompilador { 

  private JavaCompiler compiler;

  private JavaDinamicoManager javaDinamicoManager;

  private JavaDinamicoClassLoader classLoader;

  private DiagnosticCollector diagnostics;

  public JavaDinamicoCompilador() throws JavaDinamicoException {
    compiler = ToolProvider.getSystemJavaCompiler();
    if (compiler == null) {
    throw new JavaDinamicoException("Compilador não encontrado");
    }

    classLoader = new JavaDinamicoClassLoader(getClass().getClassLoader());
    diagnostics = new DiagnosticCollector();

    StandardJavaFileManager standardFileManager = compiler
        .getStandardFileManager(diagnostics, null, null);
    javaDinamicoManager = new JavaDinamicoManager(standardFileManager, classLoader);
  }

  @SuppressWarnings("unchecked")
  public synchronized Class compile(String packageName, String className,
      String javaSource) throws JavaDinamicoException
  {
    try {
      String qualifiedClassName = JavaDinamicoUtils.INSTANCE.getQualifiedClassName(
          packageName, className);
      JavaDinamicoBean sourceObj = new JavaDinamicoBean(className, javaSource);
      JavaDinamicoBean compiledObj = new JavaDinamicoBean(qualifiedClassName);
      javaDinamicoManager.setSources(sourceObj, compiledObj);

      CompilationTask task = compiler.getTask(null, javaDinamicoManager, diagnostics,
          null, null, Arrays.asList(sourceObj));
      boolean result = task.call();

      if (!result) {
      throw new JavaDinamicoException("A compilação falhou", diagnostics);
    }

      Class newClass = (Class) classLoader.loadClass(qualifiedClassName);
      return newClass;

    }
    catch (Exception exception) {
      throw new JavaDinamicoException(exception, diagnostics);
    }
  }
}

The class that has responsibility to compilation process.

The compilation's process involves two kind of files: The source, the code write on java, and the final product, the code that was compiled (bytecode). On Compiler API these files are represented to just one interface, the JavaFileObject interface. Fortunately API offers a classe that implements this interface, SimpleJavaFileObject.

public class JavaDinamicoBean extends SimpleJavaFileObject { 

  private String source;

  private ByteArrayOutputStream byteCode = new ByteArrayOutputStream();


  public JavaDinamicoBean(String baseName, String source) {
    super(JavaDinamicoUtils.INSTANCE.createURI(JavaDinamicoUtils.INSTANCE.getClassNameWithExt(baseName)),
        Kind.SOURCE);
    this.source = source;
  }

  public JavaDinamicoBean(String name) {
    super(JavaDinamicoUtils.INSTANCE.createURI(name), Kind.CLASS);
  }

  @Override
  public String getCharContent(boolean ignoreEncodingErrors) {
    return source;
  }

  @Override
  public OutputStream openOutputStream() {
    return byteCode;
  }

  public byte[] getBytes() {
    return byteCode.toByteArray();
  }
}

Structure that has the source after and before the compilation process.

To manager this files will use ForwardingJavaFileManager what implements JavaFileManager interface.

public class JavaDinamicoManager extends ForwardingJavaFileManager { 
  private JavaDinamicoClassLoader classLoader;
 

  private JavaDinamicoBean codigoFonte;

  private JavaDinamicoBean arquivoCompilado;
 
  public JavaDinamicoManager(JavaFileManager fileManager, JavaDinamicoClassLoader classLoader)
  {
    super(fileManager);
    this.classLoader = classLoader;
  }

  public void setSources(JavaDinamicoBean sourceObject, JavaDinamicoBean compiledObject) {
    this.codigoFonte = sourceObject;
    this.arquivoCompilado = compiledObject;
    this.classLoader.addClass(compiledObject);
  }

  @Override
  public FileObject getFileForInput(Location location, String packageName,
      String relativeName) throws IOException
  {
    return codigoFonte;
  }

  @Override
  public JavaFileObject getJavaFileForOutput(Location location,
      String qualifiedName, Kind kind, FileObject outputFile)
      throws IOException
  {
    return arquivoCompilado;
  }

  @Override
  public ClassLoader getClassLoader(Location location) {
    return classLoader;
  }
}

Class to manager compiled classes.

This article we talked about the compilation dynamically on Java, the goal was show the basic works and explain a little about the API.

References: