Saturday, July 25, 2009

ColdFusion 8: Experiment with compiling Java code from ColdFusion - Part 1

A few months back I was looking through the ColdFusion8\lib directory and noticed the tools.jar file which contained some intriguingly named classes. Needless to say I did not get around to exploring it then, but I finally did get back to it last weekend for a time.

Gold Rush
Now what caught my eye was a class named sun.tools.javac . If you have ever compiled a java class you will undoubtedly recognize the name. For those that have not, javac.exe is a small program used to compile java code from the command line and sun.tools.javac is a java class that allows you to do the same thing, only programatically. So the class definitely has some interesting potential.


Fool's Gold
At first I was very excited about the prospect of being able to compile java code from ColdFusion. But after further research and testing it did not pan out as well as I had hoped. The primary issues were:

  • The sun.tools.javac class is internal and undocumented
  • The sun.tools.javac class was deprecated around version 1.4
  • When I tried using sun.tools.javac from ColdFusion 8, it did compile my source code. But it also bounced the CF Server in the process. (Granted I only ran a few cursory tests after discovering it was undocumented.)

Back to the drawing board I went.

The Real McCoy
Eventually I found the JavaCompiler class, which is an officially supported replacement for sun.tools.javac. The JavaCompiler was introduced in jdk 1.6. (Note the jdk reference. To use the JavaCompiler you must be running a jdk, not just a jre.) As with many java classes, JavaCompiler is designed to be extended so developers can easily add additional functionality. But you can still use the default implementation from ColdFusion, without having to build a custom java class. Fortunately an article on openjdk.com helped fill in some of the gaps in the main api.

Tool Time
Now to use the JavaCompiler I obviously needed some java code. So I created a rather pointless "Clock" class and saved the code to a file on disk. Note, you can use whatever directory you wish, but the file name must match the class name ie use "Clock.java".
<cfsavecontent variable="sourceCode">
import java.util.Date;
public class Clock
{
public Clock()
{
}

public String tick() {
return "tick tock it is: "+ new Date();
}
}
</cfsavecontent>

<cfset pathToSourceFile = "c:\myFiles\Clock.java" />
<cfset fileWrite(pathToSourceFile, sourceCode)>

Next, I grabbed a reference to the system compiler from the ToolProvider class. To compile the code I used the simpler JavaCompiler.run() method, which accepts several arguments. As the openJDK article describes:

The first three arguments to this method can be used to specify alternative streams to System.in, System.out, and System.err. The first two arguments are ignored by the compiler and the last one control where the output from -help and diagnostic messages will go.

So I first created output stream to capture any error messages. Next I created an array of the .java files I wanted to compile. Finally, I called the run() method to actually compile the java code. The result was a brand new .class file located in the same directory as my source file (ie c:\myFiles\ClockClass.class)

<cfset provider = createObject("java", "javax.tools.ToolProvider")>
<cfset compiler = provider.getSystemJavaCompiler()>

<cfset errStream = createObject("java", "java.io.ByteArrayOutputStream").init()>
<cfset args = [ pathToSourceFile ]>
<cfset status = compiler.run( javacast("null", ""),
javacast("null", ""),
errStream,
args
)>

<cfset message = toString(errStream.toByteArray())>
<cfset errStream.close()>
COMPILER RESULTS:<hr>
<cfoutput>
STATUS: #status# (0 == success) <br />
ERRORS: #message# <br />
</cfoutput>

(Drum Roll Please)
All that was left was to test the .class file. For this task I chose to use the JavaLoader.cfc. In its present incarnation, the JavaLoader cannot load individual class files. At least as far as I know. So I used the handy <cfzip> tag to add my new class file to a jar. Finally, I loaded my jar into the JavaLoader and voila, instant results.

Important note: The example below creates a new instance of the JavaLoader on each request for demonstration purposes only. See Using a Java URLClassLoader in CFMX Can Cause a Memory Leak for proper usage tips.

<cfzip action="zip" source="c:\myFiles\" filter="*.class" file="c:\myFiles\clock.jar">
<cfset jarPaths = [ "c:\myFiles\clock.jar" ] >
<cfset javaLoader = createObject("component", "javaLoader.JavaLoader").init(jarPaths)>

<cfset clock = javaLoader.create("Clock").init()>
COLDFUSION RESULTS:<hr>
<cfoutput>
clock.tick() = #clock.tick()# <br/ ><br />
</cfoutput>

<cfdump var="#clock#" label="My Clock class">



The World's Smallest Violin
As JavaCompiler has been around since version 1.6, I had no illusions that I was the first person to discover this tool. Java developers have probably known about it for quite some time. However, I really enjoyed learning about it and how to use it from ColdFusion. For that reason I chose to wait until the end to see if anyone else in the ColdFusion realm had blogged about it. Well, as I went to post this entry last night I was a bit crushed to discover Adrian Walker had just posted something similar two weeks ago. But as our approaches were a bit different, I decided to rise above my disappointment, and post it anyway ;) But seriously, I thought having a bit of extra background might be useful to those interested in using the tool.

So in the vein of "finish what you start" and "but ...{sputter} ... it is already written", more about compiling code with JavaCompiler.CompilationTask in Part 2 ...

0 comments:

  © Blogger templates The Professional Template by Ourblogtemplates.com 2008

Header image adapted from atomicjeep