Using LaunchDarkly With ColdFusion And JavaLoader
UPDATE: This original post was written in 2015 in Adobe ColdFusion 10 (I think). Fast forward 6 years to 2021 and Lucee CFML 5.3, integrating the LaunchDarkly Java SDK is much easier. Read an updated post on consuming the Java SDK in a modern ColdFusion runtime.
Lately, at InVision App, we've been thinking about using LaunchDarkly as our feature flag service so that we can slowly - and safely - roll out new features to a targeted subset of users. But, integrating LaunchDarkly into a ColdFusion app is not exactly a straightforward process. Not only does it require the use external Java JAR files, elements of the required dependency tree conflict with classes that come natively with ColdFusion. Luckily, we can use Mark Mandel's JavaLoader library. But, even with the JavaLoader, it's still an uphill battle!
When it comes to Java, I'm a total noob. Actually, I'm probably lower on the totem pole than that. I know how to reach down into the "Java layer" from ColdFusion and call some methods. But, beyond that, Java - and its inner workings - are a mystery. Thankfully, I get to stand on the shoulders of a few giants in order to get a better understanding:
When I tried to use the JavaLoader to pull in the LaunchDarkly JAR file, I was getting an error about JAR file casting (line breaks added for readability):
ClassCastException: attempting to castjar: file:/opt/coldfusion10/cfusion/lib/jsr311-api-1.1.1.jar
!/javax/ws/rs/ext/RuntimeDelegate.class
to jar:file:/testing/launchdarkly/jars/launchdarkly/java-client-0.14.0-all.jar
!/javax/ws/rs/ext/RuntimeDelegate.class
As far as I'm concerned, that's Greek to me.
Mark Mandel suggested that something in LaunchDarkly (or one of its dependencies) was doing something very naughty with class loaders. And, suggested that I try to run some of the code using JavaLoader's switchThreadContextClassLoader() method. This method will ensure that the given code is executed in the correct context by explicitly switching the class loader on the current thread (more Greek to me).
After a bit of trial an error, I was finally able to get something to work. But, it definitely requires jumping through a number of hoops. Not only do I have to use the switchThreadContextClassLoader() method to create the LaunchDarkly client, I also have to create the necessary Java Proxy objects beforehand. Apparently, once you're in the switchThreadContextClassLoader() method execution, you can't create any ColdFusion classes (because it's using an isolated class loader); which means that, within the "safe code", I can't use the JavaLoader to create the LaunchDarkly class proxies since the JavaLoader uses "coldfusion.runtime.java.JavaProxy" under the hood.
That said, after banging my head against the wall for a while, I was finally able to create and consume the LaunchDarkly client in ColdFusion (version 10). To demonstrate this, I've put together a super simple ColdFusion application that creates a "FeatureFlags" service which uses LaunchDarkly under the hood.
First, let's look at the Application.cfc which instantiates and caches the FeatureFlags.cfc using the JavaLoader to manage the JAR files:
component
output = false
hint = "I define the application settings and event handlers."
{
// Define the application settings.
this.name = hash( getCurrentTemplatePath() );
this.applicationTimeout = createTimeSpan( 0, 0, 20, 0 );
this.sessionManagement = false;
// Get the current application root to help facilitate other actions.
this.rootPath = getDirectoryFromPath( getCurrentTemplatePath() );
// Setup custom path mappings.
this.mappings[ "/jars" ] = ( this.rootPath & "jars/" );
this.mappings[ "/libs" ] = ( this.rootPath & "libs/" );
/**
* I initialize the application.
*
* @output false
*/
public boolean function onApplicationStart() {
var config = deserializeJson( fileRead( this.rootPath & "config.json" ) );
// Create an isolated JavaLoader that deals only with creating classes
// required by the LaunchDarkly interactions.
// --
// NOTE: This is a "fat JAR" that was specially provided for this ColdFusion.
// integration research. It contains all of its own dependencies.
// --
// CAUTION: When using this in production, consider using the JavaLoaderFactory
// in order to cache instances in the Server scope to prevent memory leaks.
// --
// Read More: https://github.com/jamiekrug/JavaLoaderFactory
var launchDarklyJavaLoader = new lib.javaloader.JavaLoader(
loadPaths = [
expandPath( "/jars/launchdarkly/java-client-0.14.0-all.jar" )
]
);
// Create our feature flag service, which uses LaunchDakrly under the hood.
application.featureFlags = new lib.FeatureFlags(
launchDarklyJavaLoader,
config.launchDarkly.key
);
return( true );
}
/**
* I initialize the request.
*
* @scriptName I am the path of the script being requested.
* @output false
*/
public boolean function onRequestStart( required string scriptName ) {
if ( structKeyExists( url, "init" ) ) {
onApplicationStart();
// Indicate an application refresh, but kill the request.
writeOutput( "Application re-initialized" );
abort;
}
return( true );
}
}
NOTE: For the purposes of my research, the LaunchDarkly team provided me with a "fat JAR", which is a JAR file that contains all of its own dependencies (making it about 5MB in size).
When we instantiate the FeatureFlags.cfc , we are passing it the JavaLoader instance that proxies the LaunchDarkly JAR file. Internally, the FeatureFlags.cfc will be using this to both create LaunchDarkly Java classes as well as ensure that various bits of the code are fun safely.
Here is the FeatureFlags.cfc:
component
output = false
hint = "I provide feature flag insights (powered by LaunchDarkly)."
{
/**
* I create a new feature flag service that uses LaunchDarkly as the underlying
* source of truth for what users can access in the application.
*
* @javaLoader I am the JavaLoader instance for the LaunchDarkly JAR files.
* @key I am the LaunchDarkly test key.
* @output false
*/
public any function init(
required any javaLoader,
required string apiKey
) {
// Store the incoming dependencies.
setJavaLoader( javaLoader );
setApiKey( apiKey );
// Using LaunchDarkly in ColdFusion is a bit of a hurdle because it requires Java
// classes that conflict with Java classes that come natively with ColdFusion. As
// such, we have to jump through a lot of hoops to get it to play nicely:
// --
// First, we have to use an isolated JavaLoader to load the JAR file(s).
// --
// Second, we have to ensure that certain chunks of code run in the context of
// the correct class loader which is where switchThreadContextClassLoader() comes
// into play.
// --
// Third, since the switchThreadContextClassLoader() executes code in an isolated
// class loader context, we won't be able to create any ColdFusion classes within
// that code. As such, we have to create our Class Proxy instances while still in
// ColdFusion class loader context.
// --
// It's just that simple!
// Create any class proxies that we'll have to consume inside a "safe" context.
javaClasses = {
client: javaLoader.create( "com.launchdarkly.client.LDClient" ),
clientConfigBuilder: javaLoader.create( "com.launchdarkly.client.LDConfig$Builder" ),
userBuilder: javaLoader.create( "com.launchdarkly.client.LDUser$Builder" )
};
// When creating the LaunchDarkly client, something, somewhere under the hood is
// doing something unwise with how it uses classes loaders. As such, we have to
// FORCE the client-creation code to run in the correct context using the
// javaLoader.switchThreadContextClassLoader() method.
// --
// READ MORE: https://github.com/markmandel/JavaLoader/wiki/Switching-the-ThreadContextClassLoader
ldClient = javaLoader.switchThreadContextClassLoader( this, "__safelyCreateClient__" );
return( this );
}
// ---
// PUBLIC METHODS.
// ---
/**
* I get the collection of features for the given user configuration. Not all features
* will be enabled; but, all features will be returned.
*
* @userID I am the ID of the user, which we are using as the unique key.
* @userName I am the name of the user.
* @userEmail I am the email of the user (NOTE: While this is unique, it is not constant).
* @userIpAddress I am the IP address from which the top-level request is being made.
* @output false
*/
public struct function getFeatures(
required numeric userID,
required string userName,
required string userEmail,
required string userIpAddress
) {
// NOTICE: We don't have to use the ".switchThreadContextClassLoader()" method in
// this workflow because nothing here seems to be throwing an error. Woot!
var user = javaClasses.userBuilder
.init( javaCast( "string", userID ) )
.name( javaCast( "string", userName ) )
.email( javaCast( "string", userEmail ) )
.ip( javaCast( "string", userIpAddress ) )
.build()
;
// NOTE: Using the .toggle() method will inherently call the .identify() method
// so we don't have to call it explicitly.
var features = {
"featureA" = ldClient.toggle( "bennadel.a", user, javaCast( "boolean", false ) ),
"featureB" = ldClient.toggle( "bennadel.b", user, javaCast( "boolean", false ) ),
"featureC" = ldClient.toggle( "bennadel.c", user, javaCast( "boolean", false ) ),
"featureD" = ldClient.toggle( "bennadel.d", user, javaCast( "boolean", false ) )
};
return( features );
}
// ---
// HIDDEN PUBLIC METHODS.
// ---
/**
* NOT FOR PUBLIC USE - THIS METHOD HAS TO BE PUBLIC FOR JAVALOADER CONSUMPTION.
*
* I create the LaunchDarkly client while safely in the context of the correct class
* loader.
*
* CAUTION: While in the context of the LaunchDarkly class loader, this method cannot
* create any ColdFusion classes. This includes using ANY COLDFUSION TAGS THAT USE
* COLDFUSION JAVA CLASSES under the hood, like CFDump / writeDump(). This method
* should do the minimal amount of work necessary and then return to the ColdFusion
* context where behavior is much more predictable.
*
* @output false
*/
public any function __safelyCreateClient__() {
var ldClientConfig = javaClasses.clientConfigBuilder
.init()
.build()
;
var ldClient = javaClasses.client
.init( javaCast( "string", apiKey ), ldClientConfig )
;
return( ldClient );
}
// ---
// PRIVATE METHODS.
// ---
/**
* I store the new API key.
*
* @newApiKey I am the LaunchDarkly API key.
* @output false
*/
private void function setApiKey( required string newApiKey ) {
apiKey = newApiKey;
}
/**
* I store the new JavaLoader instance for the LaunchDarkly JAR files.
*
* @newJavaLoader I am the JavaLoader instance to be used for class creation.
* @output false
*/
private void function setJavaLoader( required any newJavaLoader ) {
javaLoader = newJavaLoader;
}
}
As you can see, when I go to create the LaunchDarkly client, I have first create the appropriate Java classes. Then, I have run the actual client instantiation using the switchThreadContextClassLoader() method. It's not pretty, but so far (in my limited testing), it seems to work.
And, once we have the FeatureFlags.cfc instantiated and cached, we can use it within the ColdFusion application to check which features any given user can access:
<cfscript>
// Get the feature flags for the identified user.
featureFlags = application.featureFlags.getFeatures(
userID = 4,
userName = "Ben Nadel",
userEmail = "ben@bennadel.com",
userIpAddress = "127.0.0.1"
);
</cfscript>
<cfcontent type="text/html; charset=utf-8" />
<cfoutput>
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<h1>
Using LaunchDarkly With ColdFusion And JavaLoader
</h1>
<p>
The current user has access to:
</p>
<cfdump label="Feature Flags" var="#featureFlags#" />
</body>
</html>
</cfoutput>
When we run the above code, we get the following output:
As you can see, this user has access to some features but does not have access to other features. LaunchDarkly uses some streaming and caching techniques behind the scenes to make sure feature checks don't have a negative impact on performance.
Ok, so that's where I'm at so far with my LaunchDarkly / ColdFusion integration. I'll caveat this heavily with the fact that none of this has been tested in production yet. But, hopefully this may help other people who are interested in giving it a try. And, please, forgive me for any Java terminology that I completely butchered in this post - like I said before, I know about as much Java as I do Greek.
Want to use code from this post? Check out the license.
Reader Comments
@All,
I should also mention that there is a different approach that can be taken. In addition to letting the client do the syncing, LaunchDarkly also allows for the synching to be done by an external, isolated system that persists the syncing to a Redis store. Then, you can configure the client to just read from the Redis store without having to do any of the fancy network stuff (at least this is my understanding). That said, I have not explored any of that yet.
I think the process has become more difficult with Javaloader. Overall post is just awesome, I wasn't aware this much about LaunchDarkly before reading this post. Always being pleasure to read your post Mr. Ben
@Darshan,
Yeah, using JavaLoader in this case is definitely complicated. But, I am not sure that there is any way around it. One of the LaunchDarkly dependencies conflicts with (at least) one of the JAR files that ships with ColdFusion; so, you can't just dump this stuff in the lib directory or something will probably go wrong (at least, that's my understanding). So, for the time being, at least, I think this is what you have to do.