Everything is right . Well, except for the fact that it is all wrong (Class Loaders)
Some time back I wrote a CF translation of Paulo Soares' example of How To Sign a PDF with iText. As the java code required some of the newer iText classes, I used the JavaLoader.cfc to access a more recent version of iText. At one stage I ran into a MethodNoFoundException I just could not figure out. It turns out the primary cause was an error in my code and though I eventually figured out the surface cause, the deeper reason for the exception eluded me. But that question always stuck in the back of my mind. While reading about class loaders last week, the real explanation for the error finally hit me.
The Problem
When using the JavaLoader (with the default settings) it is important to be consistent. If you create one object with the JavaLoader, and want it to communicate with other java objects, you must use the JavaLoader to create all of those objects. While that fact seems blatantly obvious to me now, it was not at the time.
At one point in my code I slipped up and used createObject() instead of javaLoader.create() for one of the iText objects. Since CF already has a version of iText in its classpath, the call to createObject() worked and I went blissfully along with my code. The problem did not manifest itself until I tried to get the two objects to talk to each other. Below is a simplified version of the problem. You will notice the code works up until the last line. At that point CF complains that "The add method was not found."<cfscript>
// get a reference to my javaloader
javaLoader = server[application.MyUniqueKeyForJavaLoader];
// note: the document is created with createObject() and not the javaLoader
document = createObject("java", "com.lowagie.text.Document").init();
document.open();
paragraph = javaLoader.create("com.lowagie.text.Paragraph").init("Hello World");
// This does NOT work
document.add(paragraph);
</cfscript>
The Clue
On the surface it seemed like the code should work. But obviously it did not. I double checked everything from spelling to paths to cfdump'ing the objects. I went so far as to use javap to verify the method signatures. I even tried loading the built-in version of the iText.jar instead. As far as I could tell, everything was correct. Yet still the MethodNotFoundException persisted.
Eventually I figured out the difference was the usage of createObject. It seemed innocuous enough, but it turns out it had a greater effect than I thought. Apparently the children of createObject and javaLoader.create() are like the Hatfields and the McCoys: they do not get along. (At least not with the default settings, which is what I was using). I knew the breakdown in communication had something to do with the fact that the objects were created with two different class loaders: one by the JavaLoader and the other by ColdFusion's class loader. But I was not sure why that made a difference.
The Answer
The light finally dawned while reading a great article in the O'Reilly series. The article provides a detailed description of exactly how class files are loaded. One of the key steps in the process is the findClass() method. Typically, the findClass() method is where the class loader grabs the actual byte codes for a given class. After the bytes are loaded, another method called defineClass() is invoked. That method is responsible for converting the bytes "into an instance of [the given class]". In other words, it creates the new instance for you when you call init() on a java object. Now here is the comment that set off a light bulb in my brain:... the runtime is very particular about which ClassLoader instance calls [defineClass] ...if two ClassLoader instances define byte codes from the same or different sources, the defined classes are different.
And the sign said "Long-haired freaky people, need not apply"
So my two objects were incompatible because the JavaLoader and the ColdFusion class loader had each generated their own definitions of the class. According to the rules the two definitions are not considered equal. So when I attempted to pass the one object into the other:
.. CF spewed out an error message. Not because the document.add(...) method did not exist, but because the existing method would not accept what I was passing into it. I may as well have tried to pass in a brass trombone. From the class loader's perspective, there would be little difference. It would not recognize either one, so both would be rejected equally. (Interestingly, these same rules apply even if you were to use two separate instances of the same ClassLoader class).
<cfscript>
// This does NOT work
document.add(paragraph);
</cfscript>
So I finally answered that nagging question and temporarily silenced the curious child in me that is always asking "But why?". If you are interested in learning more about class loaders, I would recommend the Internals of Java Class Loading article. Though a bit dated, it provides an excellent overview of the topic.
0 comments:
Post a Comment