Abstract: In this newsletter, we have a look at how we can create new classes in memory and then inject them into any class loader. This will form the basis of a system to generate virtual proxies statically.
A hearty welcome to the 180th edition of The Java(tm) Specialists' Newsletter, sent from my balcony with a stunning view of the snow-capped "Lefka Ori" mountains. On my right is a view down to the Cretan sea. In front I see my neighbour's vineyard and lots of olive trees. The birds think it is spring already and are twittering to their hearts' content...
javaspecialists.teachable.com: Please visit our new self-study course catalog to see how you can upskill your Java knowledge.
Since the code for building the proxy class generator is quite long, I have split the newsletter into several parts. The next issue will probably be sent in ten days from Düsseldorf.
A few weeks ago, we were talking about dynamic proxies at my Java Specialist Master Course in Baltimore, MD. I told my students that generated code would be much faster and that you could do this from within Java. I had known about the javax.tools.JavaCompiler class for a number of years, but had not managed to use it to my satisfaction. I figured out how to compile classes from strings, but the resulting class files were dumped on the disk instead of being loaded into the current class loader.
After much searching and head scratching, I found a
website
that described how to do this in Groovy, using the
JavaCompiler. The key was the
ForwardingJavaFileManager
class. This led to
another excellent article called
Dynamic
In-memory Compilation. Both articles showed how to
convert a String
into a byte[]
representing the Java class.
Once we have obtained the byte[], we need to turn this into
a class. One easy solution is to make a ClassLoader that
inherits from our current one. One of the risks is that we
then enter ClassLoader hell. I wanted to rather take the
dynamic proxy approach, which lets the user specify into
which ClassLoader we want our class to be injected. In my
solution I use the same mechanism by calling the private
static Proxy.defineClass0()
method. We could
probably also have used the
public Unsafe.defineClass() method, but both "solutions" bind
us to an implementation of the JDK and are thus not ideal.
In this newsletter, we look at how the Generator
works. It uses a GeneratedJavaSourceFile to store the
String, in this case actually a CharSequence
.
The CharSequence interface is implemented by String,
StringBuffer and StringBuilder, thus we do not need to
create an unnecessary String. We can simply pass in our
existing StringBuilder
. I wish more classes used the
CharSequence
interface!
According to the JavaDocs, the recommended URI for a Java
source String object is "string:///NameOfClass.java"
,
but "NameOfClass.java"
also works, so that is
what we will use.
import javax.tools.*; import java.io.*; import java.net.*; class GeneratedJavaSourceFile extends SimpleJavaFileObject { private CharSequence javaSource; public GeneratedJavaSourceFile(String className, CharSequence javaSource) { super(URI.create(className + ".java"), Kind.SOURCE); this.javaSource = javaSource; } public CharSequence getCharContent(boolean ignoreEncodeErrors) throws IOException { return javaSource; } }
The next class is used to hold the generated class file. It presents a ByteArrayOutputStream to the JavaFileManager in the openOutputStream() method. The URI here is not used, so I just specify "generated.class". Once the Java source is compiled, we extract the class with getClassAsBytes().
import javax.tools.*; import java.io.*; import java.net.*; class GeneratedClassFile extends SimpleJavaFileObject { private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); public GeneratedClassFile() { super(URI.create("generated.class"), Kind.CLASS); } public OutputStream openOutputStream() { return outputStream; } public byte[] getClassAsBytes() { return outputStream.toByteArray(); } }
The GeneratingJavaFileManager forces the JavaCompiler to use the GeneratedClassFile's output stream for writing the class:
import javax.tools.*; import java.io.*; class GeneratingJavaFileManager extends ForwardingJavaFileManager<JavaFileManager> { private final GeneratedClassFile gcf; public GeneratingJavaFileManager( StandardJavaFileManager sjfm, GeneratedClassFile gcf) { super(sjfm); this.gcf = gcf; } public JavaFileObject getJavaFileForOutput( Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException { return gcf; } }
The Generator class uses the private static "defineClass0" method found in Proxy to add the class into the ClassLoader. This will cause an exception if the class already exists in that class loader. Another approach is to use a new ClassLoader. See the Dynamic In-memory Compilation article for an example of how to do that.
Compiling syntax errors will be printed to System.err. You should replace that code with calls to your favourite logging system.
import javax.tools.*; import java.lang.reflect.*; import java.util.*; public class Generator { private static final Method defineClassMethod; private static final JavaCompiler jc; static { try { defineClassMethod = Proxy.class.getDeclaredMethod( "defineClass0", ClassLoader.class, String.class, byte[].class, int.class, int.class); defineClassMethod.setAccessible(true); } catch (NoSuchMethodException e) { throw new ExceptionInInitializerError(e); } jc = ToolProvider.getSystemJavaCompiler(); if (jc == null) { throw new UnsupportedOperationException( "Cannot find java compiler! " + "Probably only JRE installed."); } } public static Class make(ClassLoader loader, String className, CharSequence javaSource) { GeneratedClassFile gcf = new GeneratedClassFile(); DiagnosticCollector<JavaFileObject> dc = new DiagnosticCollector<JavaFileObject>(); boolean result = compile(className, javaSource, gcf, dc); return processResults(loader, javaSource, gcf, dc, result); } private static boolean compile( String className, CharSequence javaSource, GeneratedClassFile gcf, DiagnosticCollector<JavaFileObject> dc) { GeneratedJavaSourceFile gjsf = new GeneratedJavaSourceFile( className, javaSource ); GeneratingJavaFileManager fileManager = new GeneratingJavaFileManager( jc.getStandardFileManager(dc, null, null), gcf); JavaCompiler.CompilationTask task = jc.getTask( null, fileManager, dc, null, null, Arrays.asList(gjsf)); return task.call(); } private static Class processResults( ClassLoader loader, CharSequence javaSource, GeneratedClassFile gcf, DiagnosticCollector<?> dc, boolean result) { if (result) { return createClass(loader, gcf); } else { // use your logging system of choice here System.err.println("Compile failed:"); System.err.println(javaSource); for (Diagnostic<?> d : dc.getDiagnostics()) { System.err.println(d); } throw new IllegalArgumentException( "Could not create proxy - compile failed"); } } private static Class createClass( ClassLoader loader, GeneratedClassFile gcf) { try { byte[] data = gcf.getClassAsBytes(); return (Class) defineClassMethod.invoke( null, loader, null, data, 0, data.length); } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new IllegalArgumentException("Proxy problem", e); } } }
We can try this out by passing a String to the Generator.
Here we produce a class that implements Runnable, called
WatchThis
. We then make an instance of the
class and pass it to a thread to run.
public class GeneratorTest { public static void main(String[] args) throws Exception { Class testClass = Generator.make( null, "WatchThis", "" + "package coolthings;\n" + "\n" + "public class WatchThis implements Runnable {\n" + " public WatchThis() {\n" + " System.out.println(\"Hey this works!\");\n" + " }\n" + "\n" + " public void run() {\n" + " System.out.println(Thread.currentThread());\n" + " while(Math.random() < 0.95) {\n" + " System.out.println(\"Cool stuff!\");\n" + " }\n" + " }\n" + "}\n" ); Runnable r = (Runnable) testClass.newInstance(); Class<? extends Runnable> clazz = r.getClass(); System.out.println("Our class: " + clazz.getName()); System.out.println("Classloader: " + clazz.getClassLoader()); Thread t = new Thread(r, "Cool Thread"); t.start(); } }
The JavaCompiler depends on the tools.jar file that is distributed with the JDK, but not with the JRE. It searches for it in all the usual install places. Thus your users either have to install the JDK or you need to distribute the tools.jar together with your application. See the README file in the JDK install directory for more information of what you may redistribute.
After I completed my code, I found two more articles that would be of interest: Using built-in JavaCompiler with a custom classloader and Create dynamic applications with javax.tools
Kind regards
Heinz
We are always happy to receive comments from our readers. Feel free to send me a comment via email or discuss the newsletter in our JavaSpecialists Slack Channel (Get an invite here)
We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.