Writing my own JavaProxy for ColdFusion 8 using onMissingMethod
First of all, you may be wondering 'what on earth is a JavaProxy?', well, to answer that question, it is the Java class that does all the work behind the scenes in ColdFusion to allow you to be able to write all that Java code in-line in your ColdFusion CFCs and CFM pages by taking the Coldfusion invocations you have implemented, and passed them to the native Java objects that you want to use.To further your understanding, if you are at all interested, you can also read up on the proxy design pattern here.
Now, what some people may or may not realise, is that inside JavaLoader, I create an instance of the coldfusion.runtime.java.JavaProxy class, so it becomes really easy for developers to create and use instances of Java objects that are loaded from external .jar files within their applications. I have a good blog post on doing this here.
Now just the other day, I became aware of a new setting in ColdFusion 8 entitled 'Disable Access to internal ColdFusion Java components', that really threw me for a bend.
For people who run shared hosts, they probably think of this as a g-d send, in that it will disable access to coldfusion.runtime.ServiceFactory - and for that, I totally understand, however, it completely locks down access to any Java object that sits under the coldfusion.* package space.
What does this mean? It means that JavaLoader, no longer works, along with any other project that also uses JavaLoader could quite potentially not work on some shared hosts providers that have upgraded to ColdFusion 8!
Why did Adobe decided to do this? Not so sure! However, with the power that we have in ColdFusion 8, we are able to implement our own JavaProxy, that should be able to be seamlessly interchanged with the ColdFusion native JavaProxy!
(Disclaimer: This is the first run at this code, and it works in the given tests I have tried on it. I will be running it against all the unit tests on Transfer, and when they all work perfectly, and Transfer can run with this CF8 restriction in place, I will release the full code as part of a new version of JavaLoader)
There are two things that allow us to do this -
-
Nothing stopping us from using Java Reflection to dynamically call methods
on a Java Class or Instance.
- In CF8 we got 'onMissingMethod()' - so we have a hook into every method that is fired on a CFC, regardless of whether or not it has been implemented.
So, first of all, let's look at the few different ways you can instantiate and use ColdFusion Java Objects, we'll use an ArrayList as an example, so that we know what we need to support in our own JavaProxy.
I will use 'createObject' here, but it could also just as easily be JavaLoader.create(className) to create an instance of the JavaProxy.
-
array = createObject("java", "java.util.ArrayList").init();
array.add(obj);
- This would be the most common, and generally the 'best' way of instantiating a Java Object in CF, as it calls the constructor straight away, with the appropriate arguments, and is the closest you will get, in style, to implementing a real constructor.
-
array = createObject("java", "java.util.ArrayList");
array.init();
array.add(obj);
- I've seen this sort of code before... to me, it seems a bit weird to split out the constructor, but it works, and you always know that the object has been instantiated. -
array = createObject("java", "java.util.ArrayList");
array.add(obj);
- This is what I feel is the 'worst' way of using Java objects in CF as the no argument default constructor is called implicitly, which means you really have no control, and it can lead to all sorts of weirdness in your application if you don't track what has been actually instantiated and what hasn't.
-
Collections = createObject("java", "java.util.Collections");
sortedArray = Collections.sort(array);
- This case shows where static methods are called, in which case, there is no constructor.
-
Color = createObject("java", "java.awt.Color");
black = Color.black;
- This is where we want to be able to retrieve a static property.
So, the first decision to make, is that all of our internal methods on the JavaProxy.cfc are going to start with an underscore. This is so that any method that we write, doesn't interfere with the onMissingMethod's we want to be able to pick up. Also as we need to implement a special 'init' method, that isn't the constructor for the JavaProxy, but instead is a constructor for the Java object the JavaProxy represents, so we will have a _init(class) method that instantiates the JavaProxy.
I'm not going to show all the code here, just the relevant parts, but don't worry, you will be able to see it in the next version of JavaLoader.
So the _init method will do the following things -
- Take a Java Class as an argument, and store it in state
- Store some helpful Java Objects in some setters.
- Set the Static Fields of the Class the JavaProxy represents to the this scope
-
Store all the
Method
objects of the Class in a struct of arrays for easy lookup (more on this
later).
<cffunction name="_setStaticFields" hint="loops around all the fields andsets the static one to this scope" access="private" returntype="void"output="false">
<cfscript>
var fields = _getClass().getFields();
var counter = 1;
var len = ArrayLen(fields);
var field = 0;
for(; counter <= len; counter++)
{
field =fields[counter];
if(_getModifier().isStatic(field.getModifiers()))
{
this[field.getName()] = field.get(JavaCast("null", 0));
}
}
</cfscript>
</cffunction>
For reference
- _getClass() returns the class that the JavaProxy represents
-
_getModifier() returns access to
java.lang.reflect.Modifier
Once we know they are static, we retrieve their value, using field.get(), and set them to the same place in the this scope as they would have been in the Java Object.
We are able to use 'null' on the field.get() because the values are static, and are not tied to any actual instance of the Class.
So, what would be nice now, is to be actually be able to instantiate an object! So let's look at implementing our own 'init' method, so that we can instantiate the Java Class that the JavaProxy represents.
<cffunction name="init" hint="create an instance of this object"access="public" returntype="any" output="false">
<cfscript>
var constructor = 0;
var instance = 0;
//make sure we only ever have one instance
if(_hasClassInstance())
{
return _getClassInstance();
}
constructor =_resolveMethodByParams("Constructor", _getClass().getConstructors(), arguments);
instance =constructor.newInstance(_buildArgumentArray(arguments));
_setClassInstance(instance);
return _getClassInstance();
</cfscript>
</cffunction>
For reference-
_buildArgumentArray() simply takes the argument struct, and turns it into an
actual Java Array of the same objects.
So first off, we check to see if we already have created an instance - because
we only want one, otherwise weird stuff could happen. If we do, just give
back the Java instance we already have.The next line, is a little bit more complicated, so we'll break it down.
The _getClass().getConstructors() returns an array of all the possible Constructors that are available for this given class.
The _resolveMethodByParams() method takes a array of Method/Constructor objects, the arguments that have been passed through to the given method, in this case 'init', and find the best match that it can, and returns it. We'll go into the details of that in a minute.
Once we have the right Constructor object, to get an instance of the Object that our JavaProxy represents, we call 'newInstance' on it, and pass in an array of the objects that make up the arguments that the Constructor needs.
And Preso! We have an instance of our Class! We set it to the state of the Class Instance, and return the newly created instance back out.
Wait! What? Return the new created instance? Why aren't we returning this, that doesn't make sense? Well actually, if you think about it, it does.
We really would prefer it if ColdFusion did all the heavy lifting when it comes to the bridge between Java and ColdFusion, not only is it more performant, but it also provides a greater deal of consistency across the code base.
So we have code that is:
obj = JavaProxy.init();
obj is actually an instance of the ColdFusion JavaProxy, and then there is a much more seamless line between the new CFC JavaProxy, and the use of the ColdFusion one, which is a very good thing.
That being said, we need to provide support for all the different types of ways that Java Objects can be used and created, so we have to also cater for the other aspects as well.
So without further ado, let's actually fire off some methods! This is where onMissingMethod really comes into it's power!
<cffunction name="onMissingMethod" access="public" returntype="any"output="false" hint="wires the coldfusion invocation to the JavaObject">
<cfargument name="missingMethodName" type="string" required="true" />
<cfargument name="missingMethodArguments" type="struct" required="true" />
<cfscript>
var method = _findMethod(arguments.missingMethodName, arguments.missingMethodArguments);
if(_getModifier().isStatic(method.getModifiers()))
{
return method.invoke(JavaCast("null", 0), _buildArgumentArray(arguments.missingMethodArguments));
}
else
{
if(NOT _hasClassInstance())
{
//run the default constructor, just like in normal CF, if there is no instance
init();
}
return method.invoke(_getClassInstance(), _buildArgumentArray(arguments.missingMethodArguments));
}
</cfscript>
</cffunction>
Okay, so this is the code that actually takes the methods that are called on the JavaProxy, and passes them to the Java instance or Class as appropriate.
The _findMethod() method, returns the Method that best matches the name of the method that was called, and the arguments that it has. I will go into detail on that in just a second.
Once we have the correct Method, if it is static, we can then invoke it against 'null', and return it's value.
If it isn't static, then we check to see if we have a instance of the Java Class yet, if not, we create one using the default Constructor, which is the same way that ColdFusion does it.
From here, we are able to invoke the method against the class instance, and return any results that we may get.
Now we can look at the logic that allows us to work out which method matches what in ColdFusion.
Our first step, is to look at the _findMethod method, which is actually pretty simple:
<cffunction name="_findMethod" hint="finds the method thatclosest matches the signature" access="public" returntype="any"output="false">
<cfargument name="methodName" hint="the name of the method" type="string" required="Yes">
<cfargument name="methodArgs" hint="the arguments to look for" type="struct" required="Yes">
<cfscript>
var decision = 0;
if(StructKeyExists(_getMethodCollection(), arguments.methodName))
{
decision = StructFind(_getMethodCollection(), arguments.methodName);
//if there is only one option, try it, it's only going to throw a runtime exception if it doesn't work.
if(ArrayLen(decision) == 1)
{
return decision[1];
}
else
{
return _resolveMethodByParams(arguments.methodName, decision, arguments.methodArgs);
}
}
throw("JavaProxy.MethodNotFoundException", "Could not find thedesignated method", "Could not find the method '#arguments.methodName#'in the class #_getClass().getName()#");
</cfscript>
</cffunction>
The first thing to know is, that _getMethodCollection() returns a struct of arrays that was set up in our _init(), the key of which is the name of the methods found in the class. The arrays contained in the struct have all the Methods that have that name, as there may be more than one.
So, the first thing we do, is check to see if the name of the method we need is in the collection of methods we have, if it is we go and grab the array of methods this invocation could possibly be.
You will notice that I have written code that states 'if you only have one option for the method, just return that'. You may be wondering why, as the parameters of that method may not match what has been passed in. Well, if that is the case, we will get a runtime error, which is the same as what we would get otherwise, so there is not a huge difference here to just say 'let's give this a shot, if it doesn't work, no big deal', and we save the performance hit of comparing parameters.
If there are more than one option available, then we have to start comparing parameters, and this is where the _resolveMethodByParams() method that we saw earlier does it's hard work.
<cffunction name="_resolveMethodByParams"hint="resolves the method to use by the parameters provided"access="private" returntype="any" output="false">
<cfargument name="methodName" hint="the name of the method" type="string" required="Yes">
<cfargument name="decision" hint="the array of methods to decide from" type="array" required="Yes">
<cfargument name="methodArgs" hint="the arguments to look for" type="struct" required="Yes">
<cfscript>
var decisionLen = ArrayLen(arguments.decision);
var method = 0;
var counter = 1;
var argLen = ArrayLen(arguments.methodArgs);
var paremeters = 0;
var paramLen = 0;
var pCounter = 0;
var param = 0;
var class = 0;
var found = true;
for(; counter <= decisionLen; counter++)
{
method = arguments.decision[counter];
parameters = method.getParameterTypes();
paramLen = ArrayLen(parameters);
found = true;
if(argLen eq paramLen)
{
for(pCounter = 1; pCounter <= paramLen AND found; pCounter++)
{
param = parameters[pCounter];
class = _getClassMethod().invoke(arguments.methodArgs[pCounter], JavaCast("null", 0));
if(param.isAssignableFrom(class))
{
found = true;
}
else if(param.isPrimitive()) //if it's a primitive, it can be mapped to object primtive classes
{
if(param.getName() eq "boolean" AND class.getName() eq "java.lang.Boolean")
{
found = true;
}
else if(param.getName() eq "int" AND class.getName() eq "java.lang.Integer")
{
found = true;
}
...
else
{
throw("Ack", "Cannot match this primitive type", "'#param.getName()#' is just not matching");
}
}
else
{
found = false;
}
}
if(found)
{
return method;
}
}
}
throw("JavaProxy.MethodNotFoundException", "Could not find thedesignated method", "Could not find the method '#arguments.methodName#'in the class #_getClass().getName()#");
</cfscript>
</cffunction>
Woah! That's a lot of crazy code... well, it's not too bad once you break it down.
What we are doing is looping around all the possible methods we have available in the decision array, and trying to see if they match the parameters that we have in our argument struct.
First test says, 'if the number of parameters is different, well, we can't invoke this method', and simply passes it by.
From there, we need to loop around each of the parameters, and see if it can work with the class that the corresponding argument that matches it's place.
You're probably looking at the line that reads '_getClassMethod().invoke(arguments.methodArgs[pCounter], JavaCast("null", 0));' and thinking... what on earth does that do? Well, it allows us to get to the Class object of that argument.
This
is very similar to doing a obj.getClass(), however, I can't do that in
all instances. If an argument is a CFC, then it will try and resolve
the method 'getClasss' against the CFC, and most likely throw me an
error. So I have to use reflection!
the _getClassMethod() (this was setup in our _init()) is the actually Method class that represents the 'getClass()' method aforementioned, what I then do is invoke it on the required argument, and this gives me back the Class Object I need for comparison.
Now, a Class object has a great method called 'isAssignableFrom', basically, this says 'If the object is a the same as this Class, or a subclass, or implements this interface, return true'. This means that if the argument in the method that have been invoked on the CF side isAssignable to the parameter that is required for this method, then this method can be invoked successfully.
The other thing we need to do is map primitive parameters, such as int, char, boolean, etc back to their Object representations. The nice thing is that Java will map this back and forth at runtime for us, so as long as the values match up, we're good to go.Once the parameters have all been resolved, we can return the method we have found that works, otherwise, we throw an exception.
That pretty much covers the JavaProxy.CFC. As I said before, you will be able to see the code in action once I release the new version of JavaLoader, but feel free to ask any questions you may have, I will be happy to answer them.





