Using THIS.jarPaths To Create An Application-Specific URL Class Loader In ColdFusion
At this last CFUNITED, Rupesh Kumar gave a talk titled, "Extending Java Applications with ColdFusion." In the Q&A portion of the talk, I asked Rupesh if Adobe had any plans to make application-specific Java class loaders in the same way that Application.cfc can currently define its own mappings and custom tag paths. He said that this was something that they were looking into; however, I wanted to see if I could play around with a little proof-of-concept on my own.
Right now, if you want to make new Java classes available to your ColdFusion instance, you have to add the given JAR files to the server's lib folder and then restart the ColdFusion service. While this isn't such a big deal, there is something very nice about keeping all aspects of an application in a single, cohesive location. This increases the portability of the application which, in turn, reduces the chances of you making an error.
To keep the JAR files within the application code base, I was picturing the use of a THIS-scoped JAR path collections property, much in the same vein as the currently-supported custom tag paths and mappings:
Application.cfc Properties
- this.mappings = []
- this.customTagPaths = ""
- this.jarPaths = []
NOTE: I am using an array for jarPaths; I have no idea why customTagPaths gets defined as a list.
Once the "this.jarPaths" is defined within the Application.cfc, any calls to the createObject("java") function made within the same application would automatically check the application-specific JAR paths before it attempted to load the Java class from the core class collection. The way ColdFusion is set up now, however, you can't really override core functions. You can create like-named functions as properties of an object; but, if you were to try and call those custom functions without any scoping, ColdFusion will always assume that you are trying to call the core functions. As such, for this proof of concept, I can't override the createObject() function directly; rather, I have to create a globally-accessible UDF called createJava().
Before we see how this proof of concept is wired together, let's take a look at the kind of implementation that I think might be nice. Here is the simple Application.cfc for my demo:
Application.cfc
<cfcomponent
extends="BaseApplication"
output="false"
hint="I define the application settings and event handlers.">
<!--- Define the application settings. --->
<cfset this.name = hash( getCurrentTemplatePath() ) />
<cfset this.applicationTimeout = createTimeSpan( 0, 0, 5, 0 ) />
<!---
Add the current directory to our collection of application-
specific JAR paths to be used with createJava().
--->
<cfset this.jarPaths = [
"file://#getDirectoryFromPath( getCurrentTemplatePath() )#"
] />
</cfcomponent>
Ignoring for a moment the fact that this Application.cfc extends BaseApplication.cfc, you can see that this component defines a "this.jarPaths" property and adds the root application directory as a source of application-specific JAR paths.
Now, let's take a look at a demo page executed within the context of this application:
<!---
At this point, the ColdFusion server has implicitly instantiated
Application.cfc - our ColdFusion framework component. That has
created a URL classloader with the application-specific JAR files
and created a globally-accessible "createJava()" method.
Create an instance of the java class, HelloWorld.
NOTE: This calls the default constructor implicitly. We would
need to get a bit more compliated to be able to pass in
constructor arguments... which goes beyond my know-how.
--->
<cfset helloWorld = createJava( "HelloWorld" ) />
<!--- Say hello via the new class. --->
<cfoutput>
Hello: #helloWorld.sayHello()#
</cfoutput>
As you can see, the page is making use of a globally-accessible createJava() method in order to create an instance of our HelloWorld Java class. Then, it calls the sayHello() method on that Java class instance. In doing this, we get the following page output:
Hello: Waaaaazzzzuuuuuuupppp!
The HelloWorld Java class that it is loading is located in the root directory of the application and is picked up using the this.jarPaths Application.cfc property:
HelloWorld.class (As .java File)
public class HelloWorld {
public HelloWorld(){
// Constructor code.
}
public java.lang.String SayHello(){
return( "Waaaaazzzzuuuuuuupppp!" );
}
}
Now that we see how this kind of functionality might be used, let's take a look at how this proof-of-concept is put together. The magic behind this comes from that BaseApplication.cfc that my above Application.cfc was extending. This BaseApplication.cfc creates a URLClassLoader and defines a createJava() method that it stores in the globally-accessible URL scope (a hack used to create globally-accessible variables in ColdFusion).
BaseApplication.cfc
<cfcomponent
output="false"
hint="I am a base Application component meant to be extended by other Application.cfc instances.">
<!---
Define the collection of JAR paths to be used for the URL
class loader in this application.
--->
<cfset this.jarPaths = [] />
<!--- ------------------------------------------------- --->
<!--- ------------------------------------------------- --->
<!---
Append the CreateJava() method to the URL collection. While
this makes NO sense from a semantic standpoint, the way in
which variables are "discovered" in ColdFusion allows us to
use the URL scope to create globally accessible functions.
--->
<cfset url.createJava = this.createJava />
<!---
Because methods copied by reference do not retain their
original context, we also have to store a reference to THIS
Application.cfc instance such that he createJava method can
get access to the URL classloader instance.
--->
<cfset url.createJavaContext = this />
<!--- ------------------------------------------------- --->
<!--- ------------------------------------------------- --->
<cffunction
name="createJava"
access="public"
returntype="any"
output="false"
hint="I create the given Java object using the URL class loader powered by the local JAR Paths. NOTE: This will be called OUTSIDE of the context of this Application.cfc; this is why it makes reference to URL-scope values.">
<!--- Define arguments. --->
<cfargument
name="javaClass"
type="string"
required="true"
hint="I am the Java class to be loaded from the class loader."
/>
<!--- Define the local scope. --->
<cfset var local = {} />
<!---
Overwrite the THIS context to fake out the rest of this
function body into thinking it's part of the original
Application.cfc instance.
In a UDF, the variable "this" is already declared as a
LOCAL variable; as such, all we have to do is overwrite
it for this link to be created.
--->
<cfset this = url.createJavaContext />
<!---
Check to see if the URL class loader has been created
for this page request.
--->
<cfif !structKeyExists( this, "urlClassLoader" )>
<!---
Create the URL class loader. Typically, we'd need to
create some sort of locking around this; but, this is
just a proof of concept.
--->
<cfset this.urlClassLoader = createObject( "java", "java.net.URLClassLoader" ).init(
this.toJava(
"java.net.URL[]",
this.jarPaths,
"string"
),
javaCast( "null", "" )
) />
</cfif>
<!---
Create a new instance of the given Java class.
NOTE: When we use the newInstance() method, it calls the
default constructor on the class with no arguments. I
believe that if we want to use constructor arguments, we
need to get the actual constructor object.
--->
<cfreturn this.urlClassLoader
.loadClass( arguments.javaClass )
.newInstance()
/>
</cffunction>
<cffunction
name="toJava"
access="public"
returntype="any"
output="false"
hint="I convert the given ColdFusion data type to Java using a more robust conversion set than the native javaCast() function.">
<!--- Define arguments. --->
<cfargument
name="type"
type="string"
required="true"
hint="I am the Java data type being cast. I can be a core data type, a Java class. [] can be appended to the type for array conversions."
/>
<cfargument
name="data"
type="any"
required="true"
hint="I am the ColdFusion data type being cast to Java."
/>
<cfargument
name="initHint"
type="string"
required="false"
default=""
hint="When creating Java class instances, we will be using your ColdFusion values to initialize the Java instances. By default, we won't use any explicit casting. However, you can provide additional casting hints if you like (for use with JavaCast())."
/>
<!--- Define the local scope. --->
<cfset var local = {} />
<!---
Check to see if a type was provided. If not, then simply
return the given value.
NOTE: This feature is NOT intended to be used by the
outside world; this is an efficiency used in conjunction
with the javaCast() initHint argument when calling the
toJava() method recursively.
--->
<cfif !len( arguments.type )>
<!--- Return given value, no casting at all. --->
<cfreturn arguments.data />
</cfif>
<!---
Check to see if we are working with the core data types -
the ones that would normally be handled by javaCast(). If
so, we can just pass those off to the core method.
NOTE: Line break / concatenation is being used here
strickly for presentation purposes to avoid line-wrapping.
--->
<cfif reFindNoCase(
("^(bigdecimal|boolean|byte|char|int|long|float|double|short|string|null)(\[\])?"),
arguments.type
)>
<!---
Pass the processing off to the core function. This
will be a quicker approach - as Elliott Sprehn says -
you have to trust the language for its speed.
--->
<cfreturn javaCast( arguments.type, arguments.data ) />
</cfif>
<!---
Check to see if we have a complex Java type that is not
an Array. Array will take special processing.
--->
<cfif !reFind( "\[\]$", arguments.type )>
<!---
This is just a standard Java class - let's see
if we can invoke the default constructor (fingers
crossed!!).
NOTE: We are calling toJava() recursively in order to
levarage the constructor hinting as a data type for
native Java casting.
--->
<cfreturn createObject( "java", arguments.type ).init(
this.toJava( arguments.initHint, arguments.data )
) />
</cfif>
<!---
If we have made it this far, we are going to be building
an array of Java clases. This is going to be tricky since
we will need to perform this action using Reflection.
--->
<!---
Since we know we are working with an array, we want to
remove the array notation from the data type at this
point. This will give us the ability to use it more
effectively belowy.
--->
<cfset arguments.type = listFirst( arguments.type, "[]" ) />
<!---
Let's double check to make sure the given data is in
array format. If not, we can implicitly create an array.
--->
<cfif !isArray( arguments.data )>
<!---
Convert the data to an array. Due to ColdFusion
implicit array bugs, we have to do this via an
intermediary variable.
--->
<cfset local.tempArray = [ arguments.data ] />
<cfset arguments.data = local.tempArray />
</cfif>
<!---
Let's get a refrence to Java class we need to work with
within our reflected array.
--->
<cfset local.javaClass = createObject( "java", arguments.type ) />
<!---
Let's create an instance of the Reflect Array that will
allows us to create typed arrays and set array values.
--->
<cfset local.reflectArray = createObject(
"java",
"java.lang.reflect.Array"
) />
<!---
Now, we can use the reflect array to create a static-
length Java array of the given Java type.
--->
<cfset local.javaArray = local.reflectArray.newInstance(
local.javaClass.getClass(),
arrayLen( arguments.data )
) />
<!---
Now, we can loop over the ColdFusion array and
reflectively set the data type into each position.
--->
<cfloop
index="local.index"
from="1"
to="#arrayLen( arguments.data )#"
step="1">
<!---
Set ColdFusion data value into Java array. Notice
that this step is calling the toJava() method
recursively. I could have done the type-casting here,
but I felt that this was a cleaner (albeit slower)
solution.
--->
<cfset local.reflectArray.set(
local.javaArray,
javaCast( "int", (local.index - 1) ),
this.toJava(
arguments.type,
arguments.data[ local.index ],
arguments.initHint
)
) />
</cfloop>
<!--- Return the Java array. --->
<cfreturn local.javaArray />
</cffunction>
</cfcomponent>
The bulk of this BaseApplication.cfc code is my toJava() method which I am using to easily cast ColdFusion arrays to typed-Java arrays for use within my URLClassLoader. Beyond that, there's not too much going on. The trickiest thing that I am doing is overriding the THIS scope of the createJava() method in order to "fake" the method into thinking it is being executed as part of the current Application.cfc instance.
Right now, I'm not making any use of caching. Theoretically, you'd probably want to cache the URL class loader in the Application or Server scope such that it doesn't have to be recreated on every single page request; however, for this proof of concept, I'm simply lazy-loading the URL class loader whenever the createJava() method gets called.
The fact that ColdFusion is built on top of Java is easily one of the most awesome aspects of the language; this let's us leverage some really amazing 3rd party projects coming out of the Java world. Now, as great as this is already, I think being able to organize those 3rd party Java projects on an application-specific basis would make this significantly more useful. Hopefully, we'll see some kind of functionality like this in future releases of ColdFusion.
Want to use code from this post? Check out the license.
Reader Comments
Just a note - you talk about your method of creating implicit UDFs, and your sample call calls it as if you did, but in your BaseApplication you end up using the URL scope instead.
@Ray,
Yeah, good point; I didn't mean to mislead anyone here. I am putting the UDF inside the URL scope so as to leverage the way ColdFusion looks for variable references. The URL scope is one of the scopes that gets crawled when a non-scoped variable is referenced in the code. So, technically, you could also use:
url.createJava()
... however, since URL can be implied (for lack of a better term), you can *sort of* call the createJava() as if it were a globally-accessible core function.
You saved the URLClassLoader instance in this scope of the BaseApplication. Is that the application scope ?
@Nelle,
It is getting cached as a public variable of the Application.cfc instance for that page request. Application.cfc gets re-created on every single page request; as such, we are also re-creating our URLClassLoader on each page request.
This is not so efficient; ideally, you'd probably want to cache the URLClassLoader in the Application or Server scope so that it only has to get created when, say, onApplicationStart() is executed.
@Ben: You said up top:
"Right now, if you want to make new Java classes available to your ColdFusion instance, you have to add the given JAR files to the server's lib folder and then restart the ColdFusion service."
For years now, my approach has been to define my government agency's(*) own directory in the classpath via ColdFusion Administrator or JRun Administrator, depending on how CF was installed.
(*) As always, I can't say what agency I work for without tons of disclaimers that I don't speak for the agency and that the agency isn't endorsing any commercial product, such as ColdFusion.
For the sake of illustration, let's refer to the directory with a Unixy environment variable name, such as $OURLIB. That one extra directory in the classpath has been all we've ever needed. It allows us to have numerous packages, using standard Java naming conventions, of the form $OURLIB/gov/ouragencyname/xxx, where xxx is whatever's unique about the package. But we keep them in our own folder, not the server's lib folder, so we don't have to track which files are ours and which ones belong to ColdFusion. It's tidier to have them separate like that.
In test and production, we can also package the class files up into $OURLIB/xxx.jar files, and that works just fine too.
But, as you indicated, we have to restart the ColdFusion service to pick up new classes and jar files.
I find this very vexing. ColdFusion interfaces to the class loader in such a way that, if a CFM file changes (and you don't have trusted cache turned on), CF recompiles it to a new class file and loads it, even though it's already loaded. Whenever I create new versions of my own class files, however, Java refuses to load the new versions because the old versions are already in memory.
With Java CFXs in CF 4.0, I used to be able to say reload="Yes/No/Auto", but that stopped working in CF 4.5. It's sad to pine for a feature I had 5 levels of ColdFusion ago. Maybe I just don't know the Java class loader well enough, but I would love to be able to put a new version of a class or jar file out to the server and not have to restart the ColdFusion service to pick up the new version.
Could you point me in the right direction about how to force the class loader to pick up my new class and jar files? How does CF do it???
Surely it can't be all of those gobbledy-gooky file names in the cfclasses directory, can it? Could CF be forcing the reload of classes by generating unique file names for every version of a CFM file? Seriously? If true, that seems kinda gross.
Sorry if this question is too simple and everyone else but me already knows the answer.
@Ben,
the reason i'm asking is that there was a memory leak when using the Java URLClassLoader:
http://www.transfer-orm.com/?action=displayPost&ID=212
i do not know if it was fixed in the CF8/9, but since we had problems in the past with the heap overflows, i'm still a bit too carefull when it comes to URLClassLoader and tend to save its instances in the Application scope.
@Steve,
Sounds like a good strategy with adding agency-specific JAR directory; even though you have to restart the ColdFusion service, at least you are keeping your JARs in a cohesive, application-specific way.
As far as your caching issue, I think a Java Class loader would help since the JAR file isn't actually going through the ColdFusion system directly (the URL Class Loader is what's loading the JAR). That said, I've never actually built my own Class files (except for this actual demo). As such, I've never gone head-to-head with a JAR-caching issue. Sorry I don't have any advice on that matter.
A number of people have asked for this kind of functionality. There is some tricky functionality with Class Loaders (in the order in which they search the class loader chain for JAR files).
@Nelle,
Very interesting post (Mark Mandel is one bright dude!). I am not sure I follow his application-timeout issue though - if an Application were to timeout, I'd have to assume that all session had already timed out as well. As such, I can't see how the garbage collection issue isn't met (meaning that the URL Class Loader and all of the instances it has created have been de-referenced).
That said, very good to point that out to us, or at least me, since I had no idea that the URL Class Loader caused this kind of behavior.
Dear Ben,
again a very nice post. Thanks for it!
I have got a problem to get access to classes, which comes from JAR-Files.
If I use your method for normal CLASS-Files all works fine, but if I try to get them out of a JAR-File, which means, that I've done something like:
<cfset this.jarPaths = [
"file://#getDirectoryFromPath( getCurrentTemplatePath() )#ProjektFolder\",
"file://#getDirectoryFromPath( getCurrentTemplatePath() )#ProjektFolder\lib\testjar.jar"
] />
in the Application.cfc.
I've got an error message from the BaseApplication.cfc in the createJava-Function every time I try to create Java-Objects from the Jar-File:
java.lang.ClassNotFoundException: Test
at java.net.URLClassLoader$1.run(Unknown Source)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at coldfusion.runtime.StructBean.invoke(StructBean.java:511)
at coldfusion.runtime.CfJspPage._invoke(CfJspPage.java:2300)
...
Maybe it is only a syntax-problem?
Thanks for any help,
Isabel.