4

Is it possible to use JavaLoader to get objects returned by CF-called web services, and JavaLoader-loaded objects to be the same classpath context? I mean, without a lot of difficulty?

// get a web service
ws = createObject("webservice", local.lms.wsurl);
// user created by coldfusion
user = ws.GenerateUserObject();
/* user status created by java loader.
** this api provider requires that you move the stubs
** (generated when hitting the wsdl from CF for the first time)
** to the classpath.
** this is one of the stubs/classes that gets called from that.
*/
UserStatus = javaLoader.create("com.geolearning.geonext.webservices.Status");
// set user status: classpath context clash
user.setStatus(UserStatus.Active);

Error:

  • Detail: Either there are no methods with the specified method name and argument types or the setStatus method is overloaded with argument types that ColdFusion cannot decipher reliably. ColdFusion found 0 methods that match the provided arguments. If this is a Java object and you verified that the method exists, use the javacast function to reduce ambiguity.
  • Message: The setStatus method was not found.
  • MethodName setStatus

Even though the call, on the surface, matches a method signature on user--setStatus(com.geolearning.geonext.webservices.Status)--the class is on a different classpath context. That's why I get the error above.

Jamie Jackson
  • 1,158
  • 3
  • 19
  • 34
  • (Edit) Oops, I meant to say did you try setting the [`parentClassLoader`](http://www.compoundtheory.com/javaloader/docs/)? – Leigh Mar 19 '13 at 17:38
  • What would that look like? Some of that "Expert Use Only" stuff isn't documented well enough for this n00b. – Jamie Jackson Mar 19 '13 at 19:54
  • Actually looking at [the API](http://www.compoundtheory.com/javaloader/docs/#API_Documentation_620068746893_8378460819715732) again, the simplest way is setting `loadColdFusionClassPath=true`. *Note - when setting `loadColdFusionClassPath` to 'true', this value is overwritten with the ColdFusion classloader*. It is a shortcut for: `javaLoader.init(loadPaths=arrayOfJars, parentClassLoader=getPageContext().getClass().getClassLoader());` – Leigh Mar 19 '13 at 20:19
  • That doesn't seem to help. I think the problem remains: While UserStatus knows about the CF classpath (as well as the loaded one), I don't think there's any way to get the user object to be aware of the loaded classpath. So I think we're left in the same quandary. (Correct me if that sounds wrong--I'm making this stuff up as I go along.) – Jamie Jackson Mar 19 '13 at 21:02
  • Well .. it sounded like the method call was rejected because `UserStatus` was created by a different class loader. ie Class loaders are notoriously picky. It can be the right class, but if it was created by a *different* class loader it will not be recognized. IF that is the problem, I think sharing the parent class loader *should* resolve it. That said, is there a reason you cannot just use `createObject` for everything? Also, any chance this a public web service we can access/test? – Leigh Mar 19 '13 at 21:24

1 Answers1

5

Jamie and I worked on this off-line and came up with a creative solution :)

(Apologies for the long answer, but I thought a bit of an explanation was warranted for those who find class loaders as confusing as I do. If you are not interested in the "why" aspect, feel free to jump to the end).

Issue:

The problem is definitely due to multiple class loaders/paths. Apparently CF web services use a dynamic URLClassLoader (just like the JavaLoader). That is how it can load the generated web service classes on-the-fly, even though those classes are not in the core CF "class path".

(Based on my limited understanding...) Class loaders follow a hierarchy. When multiple class loaders are involved, they must observe certain rules or they will not play well together. One of the rules is that child class loaders can only "see" objects loaded by an ancestor (parent, grandparent, etcetera). They cannot see classes loaded by a sibling.

If you examine the object created by the JavaLoader, and the other by createObject, they are indeed siblings ie both children of the CF bootstrap class loader. So the one will not recognize objects loaded by the other, which would explain why the setStatus call failed. Child and Parent class loader of the two objects

Given that a child can see objects loaded by a parent, the obvious solution is to change how the objects are constructed. Structure the calls so that one of the class loaders ends up as a parent of the other. Curiously that turned out to be trickier than it sounded. I could not find a way to make that happen, despite trying a number of combinations (including using the switchThreadContextClassLoader method).

Solution:

Finally I had a crazy thought: do not load any jars. Just use the web service's loader as the parentClassLoader. It already has everything it needs in its own individual "class path":

    // display class path of web service class loader
    dynamicLoader = webService.getClass().getClassLoader();
    dynamicClassPath = dynamicLoader.getURLS();
    WriteDump("CLASS PATH: "& dynamicClassPath[1].toString() );

The JavaLoader will automatically delegate calls for classes it cannot find to parentClassLoader - and bingo - everything works. No more more class loader conflict.

    webService = createObject("webservice", webserviceURL, webserviceArgs);
    javaLoader = createObject("component", "javaloader.JavaLoader").init(
            loadPaths = [] // nothing
            , parentClassLoader=webService.getClass().getClassLoader()
        );

    user = webService.GenerateUserObject();
    userStatus = javaLoader.create("com.geolearning.geonext.webservices.Status");
    user.setStatus(userStatus.Active);
    WriteDump(var=user.getStatus(), label="SUCCESS: user.getStatus()");
Leigh
  • 28,765
  • 10
  • 55
  • 103
  • Yup, works beautifully, and has lots of pleasant side effects: We no longer have to copy the stubs; we don't have to keep different copies of the stubs for different API environments; and we don't have to upgrade the stubs when the API changes. Thanks, Leigh. – Jamie Jackson Mar 24 '13 at 23:26
  • Note: This solution doesn't work in Railo/Lucee, because one can't get the classLoader out of its webService object: https://luceeserver.atlassian.net/browse/LDEV-169 – Jamie Jackson Jun 30 '15 at 22:32
  • You cannot call `getClass()` on the object? How very odd. I have not used Lucee much. Wonder what the reasoning is behind that.. – Leigh Jul 01 '15 at 00:11