How to compile a Java program embedded in a String? Here’s how! (Better)


I am not quite satisfied with the way I left things in the last post.

There was just a bit too much hand-waving and magic about. Since then, I’ve experimented a bit more, and found out that some things that were claimed in the last couple of posts on this topic are also blatantly wrong – for instance, this approach of dynamic compilation does require a JDK (even though it does not need javac). The javax.tools package depends on tools.jar, which does not come bundled with the JRE.

This post is meant to make amends for that mistake, plus show a better way of handling compilation of Java code stored inside a String object without resorting to creating directories dynamically. In this attempt, we will try and make the following improvements:

  • The client code now invokes a custom compiling class loader.
  • The class loader then compiles the code stored inside the string.
  • The code is then loaded by the class loader into the current JVM.
  • Finally, the code is executed by the Client using Reflection.

Contents

  1. Code Listing
    1. The Class Loader
    2. The Compiler
  2. Testing
    1. The Client
    2. Test Run
  3. Conclusion

The Code

The code is organised as follows:

Timmys-MacBook-Pro:Better z0ltan$ tree
.
├── com
│   └── z0ltan
│   ├── compilers
│   │   └── JavaStringCompiler.java
│   └── loaders
│   └── CompilingClassLoader.java
└── org
└── z0ltan
└── client
└── Client.java

The Class Loader

Top

package com.z0ltan.loaders;

import java.io.File;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

import java.nio.file.Files;
import java.nio.file.Paths;

import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.util.logging.Logger;

import com.z0ltan.compilers.JavaStringCompiler;

public class CompilingClassLoader extends ClassLoader {
    private static final CompilingClassLoader __instance
	= new CompilingClassLoader();

    private static final Logger logger =
        Logger.getLogger(CompilingClassLoader.class.getName());

    private Pattern namePattern;
    private Pattern packagePattern;

    private CompilingClassLoader() {
        this.namePattern =
	Pattern.compile(".*class[ ]+([a-zA-Z0-9$_]+).*");
        this.packagePattern =
	Pattern.compile(".*package[ ]+([a-zA-Z0-9$_.]+).*");
    }

    public static CompilingClassLoader getInstance() {
        return __instance;
    }

    // load the class file after compiling the code
    public Class<?> loadClassFromString(final String program) throws ClassNotFoundException {
        final String className = getClassName(program);
        final String packagePath = getPackagePath(program);

        final String fullClassName;
        if (packagePath != null) {
            fullClassName = packagePath + '.' + className;
        } else {
            fullClassName = className;
        }

        logger.info("Loading " + fullClassName);

        // compile it!
        boolean result =
		JavaStringCompiler.INSTANCE
		.compileStringCode(fullClassName, program);

        if (result) {
            byte[] classBytes = getClassBytes(className);
            if (classBytes != null) {
                logger.info("Loaded " + fullClassName);
                return defineClass(fullClassName, classBytes, 0, classBytes.length);
            } else
                throw new ClassNotFoundException("Unable to load: " + fullClassName +
                                                 ". Reason = failed to load class bytes.");
        } else
            throw new ClassNotFoundException("Unable to load: " + fullClassName +
                                             ". Reason = compilation failed.");
    }

    private String getClassName(final String program) {
        Matcher m = namePattern.matcher(program);

        if (m.matches() && (m.groupCount() == 1)) {
            return m.group(1);
        }
        throw new RuntimeException("Could not find main class to load!");
    }

    private String getPackagePath(final String program) {
        Matcher m = packagePattern.matcher(program);

        if (m.matches() && (m.groupCount() == 1)) {
            return m.group(1);
        }
        return null;
    }        

    private byte[] getClassBytes(final String className) {
        final String classFilePath =
            className.replace('.', File.separatorChar) + ".class";

        try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(classFilePath));
             ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            byte[] buffer = new byte[4 * 1024];
            int bytesRead = -1;

            while ((bytesRead = bin.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesRead);
            }

            // delete the class file before returning
            try {
                Files.deleteIfExists(Paths.get(classFilePath));
            } catch (IOException ex) {
                //
            }

            return baos.toByteArray();
        } catch (IOException ex) {
            return null;
        }
    }
}

The code is as simple as it gets. The loadClassFromString method takes in the Java String containing our program, and constructs the full class name by appending the package path (if any).

It then invokes the compiler to generate the .class file. Finally, it deletes the .class file once the bytes have been read from it, thus cleaning up the unnecessary file.

The Compiler

Top

package com.z0ltan.compilers;

import java.net.URI;

import java.io.File;
import java.io.IOException;

import java.util.Collections;
import java.util.Arrays;
import java.util.logging.Logger;

import javax.tools.ToolProvider;
import javax.tools.JavaCompiler;
import javax.tools.DiagnosticCollector;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.StandardJavaFileManager;
import javax.tools.SimpleJavaFileObject;
import javax.tools.JavaFileObject;

public enum JavaStringCompiler {
    INSTANCE;

    private JavaCompiler compiler;
    private DiagnosticCollector<JavaFileObject> collector;
    private StandardJavaFileManager manager;

    private static final Logger logger =
        Logger.getLogger(JavaStringCompiler.class.getName());

    private JavaStringCompiler() {
        this.compiler = ToolProvider.getSystemJavaCompiler();
        this.collector  = new DiagnosticCollector<JavaFileObject>();
        this.manager = compiler.getStandardFileManager(collector, null, null);
    }

    // class to represent a string object as a source file
    class StringCodeObject extends SimpleJavaFileObject {
        private String code;

        StringCodeObject(final String name, final String code) {
            super(URI.create("string:///" + name.replace('.', File.separatorChar) +
                             Kind.SOURCE.extension),
                  Kind.SOURCE);
            this.code = code;
        }

        @Override
        public CharSequence getCharContent(boolean ignoreEncodingErrors) {
            return this.code;
        }
    }

    // Compile the Java code stored inside the string
    public boolean compileStringCode(final String name, final String code) {
        logger.info("Compiling: " + name);

        boolean result = false;
        StringCodeObject source = new StringCodeObject(name, code);

        result = compiler.getTask(null, manager, null, null, null,
                                  Collections.unmodifiableList(Arrays.asList(source))).call();

        // display errors, if any
        for (Diagnostic<? extends JavaFileObject> d : collector.getDiagnostics()) {
            System.err.format("Error at line: %d, in file: %s\n",
                              d.getLineNumber(),
                              d.getSource().toUri());
        }

        try {
            manager.close();
        } catch (IOException ex) {
            //
        }

        logger.info("Finished compiling: " + name);

        return result;
    }
}

This again makes use of the same facilities available in the javax.tools package. However, unlike last time, this time we make sure that we use the StandardJavaFileManager to output any compilation errors correctly. Apparently, even explicitly specifying a DiagnosticCollector instance in the compiler.getTask() call does not work. We need to have a file manager to be able to catch errors.

The rest of the is pretty much the same as last time.

Testing

Top

The testing code agains covers the two scenarios – using the default package, and using a more realistic package structure.

The Client

Top

Here is the sample client that we will use to test the code:

// The Client code

package org.z0ltan.client;

import java.lang.reflect.Method;
import java.util.logging.Logger;

import com.z0ltan.loaders.CompilingClassLoader;

public class Client {
    private static final Logger logger =
        Logger.getLogger(Client.class.getName());

    public static void main(String[] args) throws Exception {
        final String simpleProgram = "public class SimpleProgram {" +
            " public static void main(String[] args) {" +
            "    System.out.println(\"Hello from SimpleProgram!\");}}";

        testSimpleProgram(simpleProgram);

        final String complexProgram = "package foo.bar.baz.quux;" +
            "import java.util.Random;" +
            "public class ComplexProgram {"+
            "  public static void main(String[] args) {" +
            "   System.out.println(\"'Sup from Fubar\");}" +
            "public int getRandomNumber() {" +
            "  return (new Random()).nextInt(100);}}";

        testComplexProgram(complexProgram);
    }

    private static void testSimpleProgram(final String simpleProgram) throws Exception {
        logger.info("Testing SimpleProgram");

        Class<?> simpleClazz =
            CompilingClassLoader.getInstance().loadClassFromString(simpleProgram);

        if (simpleClazz != null) {
            Method main = simpleClazz.getDeclaredMethod("main", String[].class);

            if (main != null) {
                main.invoke(null, (Object)null);
            }
        }
        logger.info("Finished testing SimpleProgram");
    }

    private static void testComplexProgram(final String complexProgram) throws Exception {
        logger.info("Testing ComplexProgram");

        Class<?> complexClazz =
            CompilingClassLoader.getInstance().loadClassFromString(complexProgram);

        if (complexClazz != null) {
            Object obj = complexClazz.getConstructor().newInstance();
            if (obj != null) {
                Method main = complexClazz.getDeclaredMethod("main", String[].class);

                if (main != null) {
                    main.invoke(null, (Object)null);
                }

                Method getRandomNumber = complexClazz.getDeclaredMethod("getRandomNumber");
                if (getRandomNumber != null) {
                    int n = (int)getRandomNumber.invoke(obj);
                    System.out.format("Random number = %d\n", n);
                }
            }
        }
        logger.info("Finished testing ComplexProgram");
    }
}

Test Run

Top

Timmys-MacBook-Pro:Better z0ltan$ javac -cp . com/z0ltan/compilers/JavaStringCompiler.java 

Timmys-MacBook-Pro:Better z0ltan$ javac -cp . com/z0ltan/loaders/CompilingClassLoader.java 

Timmys-MacBook-Pro:Better z0ltan$ javac -cp . org/z0ltan/client/Client.java 

Timmys-MacBook-Pro:Better z0ltan$ java -cp . org.z0ltan.client.Client

Sep 16, 2016 2:17:49 PM org.z0ltan.client.Client testSimpleProgram
INFO: Testing SimpleProgram
Sep 16, 2016 2:17:49 PM com.z0ltan.loaders.CompilingClassLoader loadClassFromString
INFO: Loading SimpleProgram
Sep 16, 2016 2:17:50 PM com.z0ltan.compilers.JavaStringCompiler compileStringCode
INFO: Compiling: SimpleProgram
Sep 16, 2016 2:17:50 PM com.z0ltan.compilers.JavaStringCompiler compileStringCode
INFO: Finished compiling: SimpleProgram
Sep 16, 2016 2:17:50 PM com.z0ltan.loaders.CompilingClassLoader loadClassFromString
INFO: Loaded SimpleProgram
<strong>Hello from SimpleProgram!</strong>
Sep 16, 2016 2:17:50 PM org.z0ltan.client.Client testSimpleProgram
INFO: Finished testing SimpleProgram
Sep 16, 2016 2:17:50 PM org.z0ltan.client.Client testComplexProgram
INFO: Testing ComplexProgram
Sep 16, 2016 2:17:50 PM com.z0ltan.loaders.CompilingClassLoader loadClassFromString
INFO: Loading foo.bar.baz.quux.ComplexProgram
Sep 16, 2016 2:17:50 PM com.z0ltan.compilers.JavaStringCompiler compileStringCode
INFO: Compiling: foo.bar.baz.quux.ComplexProgram
Sep 16, 2016 2:17:50 PM com.z0ltan.compilers.JavaStringCompiler compileStringCode
INFO: Finished compiling: foo.bar.baz.quux.ComplexProgram
Sep 16, 2016 2:17:50 PM com.z0ltan.loaders.CompilingClassLoader loadClassFromString
INFO: Loaded foo.bar.baz.quux.ComplexProgram
<strong>'Sup from Fubar
Random number = 63</strong>
Sep 16, 2016 2:17:50 PM org.z0ltan.client.Client testComplexProgram
INFO: Finished testing ComplexProgram

And just to verify that no .class files are left in the file system:

Timmys-MacBook-Pro:Better z0ltan$ ls
com		compiler.jar	loader.jar	org

Excellent! That’s much better.

Conclusion

Top

Well, that was much more satisfying that the unnecessary complexity (and extra work!) of the previous version of this program. Now there is better separation of concerns, better diagnostics, and is much easier to read and understand.

Till next time then, folks!

Advertisements
How to compile a Java program embedded in a String? Here’s how! (Better)

Speak your mind!

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s