Running on Java 24-ea+24-2960 (Preview)
Home of The JavaSpecialists' Newsletter

180Generating Static Proxy Classes - Part 1

Author: Dr. Heinz M. KabutzDate: 2010-02-19Java Version: 6Category: Performance
 

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.

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.

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();
  }
}

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

 

Comments

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)

When you load these comments, you'll be connected to Disqus. Privacy Statement.

Related Articles

Browse the Newsletter Archive

About the Author

Heinz Kabutz Java Conference Speaker

Java Champion, author of the Javaspecialists Newsletter, conference speaking regular... About Heinz

Superpack '23

Superpack '23 Our entire Java Specialists Training in one huge bundle more...

Free Java Book

Dynamic Proxies in Java Book
Java Training

We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.

Java Consulting

We can help make your Java application run faster and trouble-shoot concurrency and performance bugs...