|
The Java Specialists' Newsletter
Issue 180 2010-02-19
Category:
Performance
Java version: 6+ Generating Static Proxy Classes - 1/2by Dr. Heinz M. KabutzAbstract: 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...
But please do not be jealous! As from May 2010, you will have an
excuse to visit this historical island and do so as a tax
write-off! We are now offering courses on Crete at
our new conference facility with even better views than from
my balcony. Here is what our typical day will look like:
We will cover about 250 slides of advanced Java material per
day, together with lots of practical exercises to apply your
new knowledge. To obtain your JavaSpecialist.EU Certificate
of Training (view
a sample student report here),
you have to complete the entire course, so no slacking will
be tolerated! (Tell your boss that :-)
For lunch we will pop up to our favourite
restaurant "Taverna Irene", where you will be served
delicious traditional Cretan home-cooked food. Coffee time
in the afternoons will be held at our pool with the
opportunity for a quick dip to cool down. Included in the
price is a dinner at the Kalathas Beach Restaurant and a spit
roast at my house on the last evening. On other evenings
we can go for a stroll through the olive groves and past the
watermelon fields down to Tersanas beach for a swim and
perhaps a drink or two.
As always, if Crete does not suit you, we also have
excellent training partners in Canada, USA, France, Germany
and Norway through whom we offer the exact same training as
on the island of Crete. Plus, for larger groups we can do
in-house courses at your company.
Generating Static Proxy Classes - 1/2
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.
package util.gen;
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().
package util.gen;
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:
package util.gen;
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.
package util.gen;
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.
import util.gen.*;
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();
}
}
Distributing Code
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.
Additional Articles
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
Performance Articles
Related Java Course
Discuss at The Java Specialist Club
|