Loading java libraries dynamically in Lucee (without JavaLoader)

26 June 2020

Perhaps the feature I love most about CFML as a programming language is the ability to easily use the power of its underlying platform: Java.

I am not a Java developer. I have a fragmentary understanding of the basics, but I'd struggle to build anything useful in pure Java, and frankly I'm not sure I'd want to.

But over the years I've found myself increasingly using bits of Java here and there within my ColdFusion/Lucee apps where CFML didn't provide what I needed. Sometimes this has meant simply calling a built-in Java class to, for example, perform a DNS lookup:


 ipAddresses = CreateObject( "java", "java.net.InetAddress" ).getAllByName( JavaCast( "string", "google.com" ) );

But the greatest benefit has come from being able to use third-party java libraries, usually packaged as ".jar" files, which can be done in several different ways.

Using the /lib path

The "traditional" method is to place the "jar" in the CFML engine's "/lib" directory and restart so that it becomes available to call using CreateObject() as above.

Using JavaLoader

Way back in 2009, Mark Mandel came up with a fiendishly clever tool that allowed java libraries to be loaded dynamically, by which I mean:

  1. the jar doesn't have to be placed in the /lib path, it can be anywhere
  2. no need to restart the engine to pick it up: you can load classes immediately
  3. updates to the library can be loaded without an engine restart
  4. no confusion if another version of the library is already in the engine's lib path

Using this.javaSettings

About 3 years later, ColdFusion 10 introduced a way of integrating jars on a per-application rather than per-server basis, which seemed to make JavaLoader redundant. In your Application.cfc you can use this.javaSettings to point to the jars you wanted to be available, and have the engine periodically check for updates.

Using CreateObject()

Lucee also added the ability to specify jars on-the-fly from anywhere in your application code, not just Application.cfc, using a third argument to CreateObject().


myObject = CreateObject( "java", "com.mylib.class", ExpandPath( "mylib.jar" );

Class clash

As a result, dynamic class loading became much easier in CFML. But there is one respect in which JavaLoader has continued to outdo the newer native options—avoiding version clashes.

I won't go into the details, mainly because I don't understand them very well, but suffice to say that if you try to load a class using any of the native dynamic methods above while a different version of that class has already been loaded, the engine may be confused as to which you mean and return an error or unexpected results.

What's really clever about JavaLoader is that it manages to load classes in isolation from those already loaded. This means you can use it to load any library you want without worrying that it might clash with existing jars.

This is why I use it with my Spreadsheet Library for example. Both ColdFusion and Lucee ship with a version of the POI library in their /lib paths, but since it's older than the version the library uses, it would clash without the isolation provided by JavaLoader.

OSGi bundles

Since version 5 Lucee has supported a native way of addressing this problem, albeit with some limitations, through its modular OSGi architecture. As long as the library you want to use has been correctly packaged as an "OSGi bundle", you can drop it into Lucee's bundles folder and start using it without fear of clashes.

While this is certainly dynamic in the sense of not requiring a restart, there's still a manual "set-up" to be done at the server level similar to putting your jar in the /lib directory.

Truly dynamic bundle loading

Fortunately Brad Wood discovered how to use Lucee's hidden tools to load local bundles automatically from your code, without having to manually pre-install them. There are basically two steps to loading jars this way.

1. Convert the jar to an OSGi bundle

Your jar may be already packaged as an OSGi bundle, something you can check using manifestRead(). Look for Bundle-SymbolicName and Bundle-Version values.

If not, Brad explains how to convert it manually, or you could try Evagoras Charalambous's OSGi JAR Converter for CommandBox.

2. Install the bundle

Use Brad's "hidden" method to programmatically install the bundle before calling its classes as needed.


// install the CFLint bundle
CFMLEngine = createObject( "java", "lucee.loader.engine.CFMLEngineFactory" ).getInstance();
OSGiUtil = createObject( "java", "lucee.runtime.osgi.OSGiUtil" );
resource = CFMLEngine.getResourceUtil().toResourceExisting( getPageContext(), expandpath( 'CFLint.jar' ) );
bundle = OSGiUtil.installBundle( CFMLEngine.getBundleContext(), resource, true);
// call
api = createObject( "java",  "com.cflint.api.CFLintAPI",  'com.cflint.CFLint' ).init();

Lucee OSGi Loader

I've pasted Brad's code above, but to avoid doing so whenever I needed to call on a bundle, I decided to enhance and package it up as a simple loading tool called osgiLoader which can be used as follows.

With CreateObject()

Use osgiLoader in the onApplicationStart() method of your Application.cfc (or another global lifecycle location as appropriate to your application).


void function onApplicationStart(){
 var bundleSymbolicName = "com.cflint.CFLint";
 var bundleVersion = "1.4.0";
 var bundlePath = ExpandPath( "/path/to/CFLint.jar" );
 osgiLoader = New osgiLoader();
 if( !osgiLoader.bundleIsLoaded( bundleSymbolicName, bundleVersion ) )
  osgiLoader.installBundle( bundlePath );
}

You can then load classes anywhere in your application confident that the bundle is available.


bundleSymbolicName = "com.cflint.CFLint";
bundleVersion = "1.4.0";
className = "com.cflint.CFLint.CFLintAPI";
apiObject = CreateObject( "java", className , bundleSymbolicName , bundleVersion  );

With loadClass()

Alternatively you can skip that initialization check and let osgiLoader handle it for you whenever you load a class.


bundleSymbolicName = "com.cflint.CFLint";
bundleVersion = "1.4.0";
bundlePath = ExpandPath( "/path/to/CFLint.jar" );
className = "com.cflint.CFLint.CFLintAPI";
osgiLoader = New osgiLoader();
apiObject = osgiLoader.loadClass( className, bundlePath, bundleSymbolicName, bundleVersion );

Uninstall a bundle

There probably won't be many situations in which you need to uninstall a bundle, but the option is there.

osgiLoader.uninstallBundle( bundleSymbolicName, bundleVersion );

osgiLoader is available on Github.

Posted on . Updated

Comments

  • Formatting comments: See this list of formatting tags you can use in your comments.
  • Want to paste code? Enclose within <pre><code> tags for syntax higlighting and better formatting and if possible use script. If your code includes "self-closing" tags, such as <cfargument>, you must add an explicit closing tag, otherwise it is likely to be mangled by the Disqus parser.
Back to the top