[home]   [me]   [my gallery]   [my blog]   [my articles]   [my code and projects]   [my links]
 

Using Eclipse compiler to create dynamic Java objects


In this blog I'll describe how I used the Eclipse compiler for compiling Java source on-the-fly to create instant 'view' of the result.

More precisely I'll show how to compile and invoke the following simple HalloWorld class on the fly:

public class HalloWorldTest{
    public static void main(String[] args) {
        System.out.println("Hallo world");
    }
}

Choosing compiler

Actually there are several Java compilers available; In Java 1.6 the javax.tools.JavaCompiler interface was introduced to give a uniform way to access a Java compiler. The following factory method gives access the Suns javacompiler (when launched from Suns JDK).

JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
However to gain access to the JDK compiler you need to run your application from the JDK, and since this is not default behaviour, I choose to use the Eclipse compiler instaid. (Besides the Eclipse compiler share the same interface, so the two compilers should behave similar).

Using Eclipse compiler

The first step is to download the Eclipse compiler. The Eclipse project is an extremely large project, but you are able to download the compiler as a part of the Eclipse Development Tools Core package.

The package is found at http://download.eclipse.org/eclipse/downloads/. Click on latest release - and scroll down to the JDT Core Batch Compiler section. The file you are looking for is called ecj-3.4.jar (where 3.4 is the version number).

With this file in your classpath, you are able access the Eclipse compiler using the simple assignment:

JavaCompiler javac = new EclipseCompiler(); 

The JavaCompiler interface

You start the compilation using creating a CompilationTask object that you invoke the method call on it:

JavaCompiler.CompilationTask compile = javac.getTask(out, fileManager, dianosticListener, options, classes, compilationUnits);
Boolean res = compile.call();

If the res is true, then all files have been compiled without any errors.

Feeding the source to the compiler

I want to compile the source code in memory, and have therefore created a MemorySource class that implements the JavaFileObject (that is passed to the java compiler as compilationUnits).

class MemorySource extends SimpleJavaFileObject {
    private String src;
    public MemorySource(String name, String src) {
        super(URI.create("file:///" + name + ".java"), Kind.SOURCE);
        this.src = src;
    }
    public CharSequence getCharContent(boolean ignoreEncodingErrors) {
        return src;
    }
    public OutputStream openOutputStream() {
        throw new IllegalStateException();
    }
    public InputStream openInputStream() {
        return new ByteArrayInputStream(src.getBytes());
    }
}

Note the URI must start with the prefix 'file:' for the Eclipse compiler to accept the file.

Receiving the compiled bytecode

The default behaviour of the compiler is to create the result in actual files, but I want to keep the compiled classes in memory and be able to load these classes as well.

For this I need two things:

  • A custom class loader, that stores compiled classes in memory and loads the compiled classes from memory
  • A custom JavaFileManager that deletages the storage of the compiled classes to the custom classloader
class SpecialClassLoader extends ClassLoader {
    private Map<String,MemoryByteCode> m = new HashMap<String, MemoryByteCode>();

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        MemoryByteCode mbc = m.get(name);
        if (mbc==null){
            mbc = m.get(name.replace(".","/"));
            if (mbc==null){
                return super.findClass(name);
            }
        }
        return defineClass(name, mbc.getBytes(), 0, mbc.getBytes().length);
    }

    public void addClass(String name, MemoryByteCode mbc) {
        m.put(name, mbc);
    }
}

This simple classloader adds the compiled classes in a Map using the addClass(...) method, and loads class from memory in findClass(...) method.

class SpecialJavaFileManager extends ForwardingJavaFileManager<StandardJavaFileManager> {
    private SpecialClassLoader xcl;
    public SpecialJavaFileManager(StandardJavaFileManager sjfm, SpecialClassLoader xcl) {
        super(sjfm);
        this.xcl = xcl;
    }
    public JavaFileObject getJavaFileForOutput(Location location, String name, JavaFileObject.Kind kind, FileObject sibling) throws IOException {
        MemoryByteCode mbc = new MemoryByteCode(name);
        xcl.addClass(name, mbc);
        return mbc;
    }

    public ClassLoader getClassLoader(Location location) {
        return xcl;
    }
}

Putting it all together

The following method will use the classes described above to compile java source code in memory and return a class with the result.

private static Class compileClass(String halloWorldProgram, String className) {
    try{
        JavaCompiler javac = new EclipseCompiler();

        StandardJavaFileManager sjfm = javac.getStandardFileManager(null, null, null);
         
        SpecialClassLoader cl = new SpecialClassLoader();
        SpecialJavaFileManager fileManager = new SpecialJavaFileManager(sjfm, cl);
        List<String> options = Collections.emptyList();
       List<MemorySource> compilationUnits = Arrays.asList(new MemorySource(className, halloWorldProgram));
        DiagnosticListener<JavaFileObject> dianosticListener = null;
        Iterable<String> classes = null;
        Writer out = new PrintWriter(System.err);
        JavaCompiler.CompilationTask compile = javac.getTask(out, fileManager, dianosticListener, options, classes, compilationUnits);
        boolean res = compile.call();
        if (res){
            return cl.findClass(className);
         }
    }
    catch (Exception e){
        e.printStackTrace();
    }
    return null;
}

I don't use the DiagnosticListener - instead I use System.err (wrapped in a PrintWriter) for error reporting.

Finally I'm ready for my testprogram to run:

private final static String HALLO_WORLD_CLASS_NAME = "HalloWorldTest";
private final static String HALLO_WORLD_SOURCE = "public class "+HALLO_WORLD_CLASS_NAME+"{\n" +
        "    public static void main(String[] args) {\n" +
        "        System.out.println(\"Hallo world\");\n" +
        "    }\n" +
        "}";

public static void main(String[] args) {
    Class compiledClass = compileClass(HALLO_WORLD_SOURCE, HALLO_WORLD_CLASS_NAME);
    if (compiledClass==null){
        return;
    }
    try{
        Method m = compiledClass.getMethod("main",String[].class);
        m.invoke(null, new Object[]{null});
    } catch (Exception e) {
        e.printStackTrace();
    }
}

When invoked the famous "Hallo world" is printed on my screen.

The full sourcecode to the program is listed here:

 import org.eclipse.jdt.internal.compiler.tool.EclipseCompiler;

import javax.tools.*;
import java.lang.reflect.Method;
import java.util.*;
import java.io.*;
import java.net.URI;

public class DynamicHalloWorld {
    private final static String HALLO_WORLD_CLASS_NAME = "HalloWorldTest";
    private final static String HALLO_WORLD_SOURCE = "public class "+HALLO_WORLD_CLASS_NAME+"{\n" +
            "    public static void main(String[] args) {\n" +
            "        System.out.println(\"Hallo world\");\n" +
            "    }\n" +
            "}";

    public static void main(String[] args) {
        Class compiledClass = compileClass(HALLO_WORLD_SOURCE, HALLO_WORLD_CLASS_NAME);
        if (compiledClass==null){
            return;
        }
        try{
            Method m = compiledClass.getMethod("main",String[].class);
            m.invoke(null, new Object[]{null});
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static Class compileClass(String halloWorldProgram, String className) {
        try{
            JavaCompiler javac = new EclipseCompiler();

            StandardJavaFileManager sjfm = javac.getStandardFileManager(null, null, null);
            
            SpecialClassLoader cl = new SpecialClassLoader();
            SpecialJavaFileManager fileManager = new SpecialJavaFileManager(sjfm, cl);
            List<String> options = Collections.emptyList();

            List<MemorySource> compilationUnits = Arrays.asList(new MemorySource(className, halloWorldProgram));
            DiagnosticListener<JavaFileObject> dianosticListener = null;
            Iterable<String> classes = null;
            Writer out = new PrintWriter(System.err);
            JavaCompiler.CompilationTask compile = javac.getTask(out, fileManager, dianosticListener, options, classes, compilationUnits);
            boolean res = compile.call();
            if (res){
                return cl.findClass(className);
            }
        }
        catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }
}


class MemorySource extends SimpleJavaFileObject {
    private String src;
    public MemorySource(String name, String src) {
        super(URI.create("file:///" + name + ".java"), Kind.SOURCE);
        this.src = src;
    }
    public CharSequence getCharContent(boolean ignoreEncodingErrors) {
        return src;
    }
    public OutputStream openOutputStream() {
        throw new IllegalStateException();
    }
    public InputStream openInputStream() {
        return new ByteArrayInputStream(src.getBytes());
    }
}


 class SpecialJavaFileManager extends ForwardingJavaFileManager<StandardJavaFileManager> {
    private SpecialClassLoader xcl;
    public SpecialJavaFileManager(StandardJavaFileManager sjfm, SpecialClassLoader xcl) {
        super(sjfm);
        this.xcl = xcl;
    }
    public JavaFileObject getJavaFileForOutput(Location location, String name, JavaFileObject.Kind kind, FileObject sibling) throws IOException {
        MemoryByteCode mbc = new MemoryByteCode(name);
        xcl.addClass(name, mbc);
        return mbc;
    }

    public ClassLoader getClassLoader(Location location) {
        return xcl;
    }
}


class MemoryByteCode extends SimpleJavaFileObject {
    private ByteArrayOutputStream baos;
    public MemoryByteCode(String name) {
        super(URI.create("byte:///" + name + ".class"), Kind.CLASS);
    }
    public CharSequence getCharContent(boolean ignoreEncodingErrors) {
        throw new IllegalStateException();
    }
    public OutputStream openOutputStream() {
        baos = new ByteArrayOutputStream();
        return baos;
    }
    public InputStream openInputStream() {
        throw new IllegalStateException();
    }
    public byte[] getBytes() {
        return baos.toByteArray();
    }
}

class SpecialClassLoader extends ClassLoader {
    private Map<String,MemoryByteCode> m = new HashMap<String, MemoryByteCode>();

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        MemoryByteCode mbc = m.get(name);
        if (mbc==null){
            mbc = m.get(name.replace(".","/"));
            if (mbc==null){
                return super.findClass(name);
            }
        }
        return defineClass(name, mbc.getBytes(), 0, mbc.getBytes().length);
    }

    public void addClass(String name, MemoryByteCode mbc) {
        m.put(name, mbc);
    }
}

More advanced example

For a more advanced example checkout my Scenegraph Shell 0.2. The Scenegraph Shell is a code editor with a visual representation of what you code. Read more about Java Scenegraph here: https://scenegraph.dev.java.net/ and read more about the project in my two previous blogs: Scenegraph Shell and Scenegraph Shell 0.2.

(Sources: I used a post on a Danish programming forum as inspiration for this blog. www.eksperten.dk )

 
 
 
 
Comments:

Great article, helped me fix a problem I was having with the Eclipse compiler (I didn't know about needing the file:///) Have you tried compiling code when running under Web Start? Simple hello world examples work fine as do classes which only reference core APIs. However, if you try and reference classes in other JAR files (i.e. those supplied with the <jar> tag in the JNLP file) then the compiler fails as it can't find the packages to import. This must be an issue with the Web Start classloader as running the same code outside of Web Start and the imports can be located without issue. Unfortunately I can't figure out what is going wrong. I've ensured that my classloader instance makes use of the classloader returned by Thread.currentThread().getContextClassLoader() for classes it doesn't know how to load itself but that doesn't help either. Any suggestions would be great and thanks again for a great article.

Posted by Mark Greenwood on oktober 08, 2008 at 11:17 AM CEST #

Hi Mark, and thanks for your positive feedback. Actually I spend quite some time trying to get my Scenegraph Shell compile and run Java in a webstart application, but I faced the same problems as you have. However I have recently read an article that might solve the problem: http://weblogs.java.net/blog/kirillcool/archive/2005/05/using_java_comp_1.html .Please let me know if it solves your issue :-)

Posted by Morten Nobel-Jørgensen on oktober 08, 2008 at 09:26 PM CEST #

Morten, Thanks for the suggestions. In the end I decided that the approach you linked to was just far too ugly to be considered! I do, however, have a solution to the problem. Unfortunately I can't claim all the credit as the code originated in Apache Tomcat and was modified to be part of GATE before I took it and worked it over further. Anyway given that I can't post HTML in this comment it seems silly to try and reproduce the code here. Instead I've blogged about it on my blog -- http://www.dcs.shef.ac.uk/~mark/blog/2008/10/compiled-code-compiles-code.html The main difference of course is that I've given up on the idea of using javax.tools.JavaCompiler (for reasons that are made clear in my blog post) but that means that I now have a compiler which will work in Java 5 as well as Java 6 and works in standalone applications and in applications run under Web Start. Hope that helps!

Posted by Mark Greenwood on oktober 16, 2008 at 02:39 PM CEST #

Hi Mark. Nice done!!! I'm can finally create the webstart application I wanted, thanks to your easy EclipseCompiler class, and do it without any ugly hacks :-) Kind regards, Morten

Posted by Morten Nobel-Jørgensen on oktober 18, 2008 at 05:55 PM CEST #

Post a Comment:
  • HTML Syntax: NOT allowed
 

« november 2008
mationtofr
     
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
       
Today

AddThis Feed Button

 
© Morten Nobel