Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at cf.Objective() 2013 (Bloomington, MN) with: Jason Dean and Simon Free
Ben Nadel at cf.Objective() 2013 (Bloomington, MN) with: Jason Dean@JasonPDean ) and Simon Free@simonfree )

Using LaunchDarkly With ColdFusion And JavaLoader

By Ben Nadel on
Tags: ColdFusion

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:


 
 
 

 
JavaLoader tweet conversation with Mark Mandel and Barney Boisvert. 
 
 
 

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:


 
 
 

 
 LaunchDarkly integration with a ColdFusion application. 
 
 
 

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.




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.

Reply to this Comment

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

Reply to this Comment

@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.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.