helion-prime
home about us blogs contacts

Posts Tagged ‘java’

On-the-fly compilation in Java6

Friday, June 13th, 2008

One of interesting novelty of Java6 is a possibility to access compiler via special API.
Let’s look on this feature a bit closer.

In order to have an access to compilation subsystem we should use classes located at javax.tools package [http://java.sun.com/javase/6/docs/api/javax/tools/package-summary.html].
In the future in this package possibly appear classes to work with different external utilities, but at this moment we have only access to the compiler.

As simple example let’s look on request to compile:

1
2
3
4
5
6
7
8
9
10
11
12
13
   JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

   Iterable<SimpleJavaFileObject> srcList =  Arrays.asList(new SimpleJavaFileObject[]{
      new SimpleJavaFileObject(URI.create("string:///myclass.java"), Kind.SOURCE) {

         @Override
         public CharSequence getCharContent(boolean ignoreEncodingErrors) {
            return "class myclass {}";
         }
      }
   });
       JavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
       compiler.getTask(null, fileManager, null, null, null, srcList).call();

Let’s try to understand how it’s work:

The compiler access input data not directly but via objects inherited form JavaFileObject. Therefore depending on needs of developer information for compilation can be received from any place: network, file, memory; for that we need to implement successor of JavaFileObject.
It is better to do it by inheriting class-gag SimpleJavaFileObject

In our example implementation of SimpleJavaFileObject returns source of class as constant string. It is slightly harder with output data. There is another abstraction layer called JavaFileManager that is object factory per se.

Standard file manager that we receive in our example let us work with files on the disk. If you need to place output data on the network on in the memory you need to override JavaFileManager. It is better to do it inheriting ForwardingJavaFileManager, this class retarget request to provided during creation file manager. At the same time you can handle only those request that you need.

Last string of example creates compilation command and execute it at once. As a result of execution we have file in the execution directory of example with name myclass.class that contains bytecode of appropriate class. It is noteworthy that classloaders of system know nothing about existence of given class and so call Class.forName(“myclass”) throws ClassNotFoundException.

Let’s complete our example to get class bytecode as byte array. For that we need to implement our own file manager:

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class JavaMemFileManager extends ForwardingJavaFileManager {

   class ClassMemFileObject extends SimpleJavaFileObject {
      ByteArrayOutputStream os = new ByteArrayOutputStream();

      ClassMemFileObject(String className) {
         super(URI.create("mem:///" + className + Kind.CLASS.extension), Kind.CLASS);
      }
      byte[] getBytes() {
         return os.toByteArray();
      }

      @Override
      public OutputStream openOutputStream() throws IOException {
         return os;
      }
   }

   private HashMap<String, ClassMemFileObject> classes =
         new HashMap<String, ClassMemFileObject>();

   public JavaMemFileManager() {
      super(ToolProvider.getSystemJavaCompiler().getStandardFileManager(null, null, null));
   }

   @Override
   public JavaFileObject getJavaFileForOutput(Location location,
         String className, Kind kind, FileObject sibling) throws IOException {
      if (StandardLocation.CLASS_OUTPUT == location && JavaFileObject.Kind.CLASS == kind) {
         ClassMemFileObject clazz = new ClassMemFileObject(className);
         classes.put(className, clazz);
         return clazz;
      } else {
         return super.getJavaFileForOutput(location, className, kind, sibling);
      }
   }

   public byte[] getClassBytes(String className) {
      if (classes.containsKey(className)) {
         return classes.get(className).getBytes();
      }
      return null;
   }
}

As you see our file manager override method getJavaFileForOutput that complier calls to receive output file object. Here we check destination, for new classes it should be StandardLocation.CLASS_OUTPUT and type. If it correspond to newly compiled class then we create new file object: we save it and pass it to the compiler.
Then we can receive access to the bytecode with method getClassBytes(className) passing the name of the class.

Let us change previous example to use new functionality:

1
2
3
4
5
6
   ....
   JavaFileManager fileManager = new JavaMemFileManager();
   compiler.getTask(null, fileManager, null, null, null, srcList).call();

   byte[] myClassBytes = ((JavaMemFileManager)fileManager).getClassBytes(“myclass”);
   ....

And yet one feature of example execution in our case will be absence of file on the disk.

Finally I made a library that makes easier access to Java6 Compiler API.
See details on: [ http://opensource.helion-prime.com/jruntime/ ]

©2010 Helion-Prime Solutions Ltd.
Custom Software Development Agile Company.