Java SDK 4.x to 5.0 migration guide
Read time: 15 minutes
Last edited: May 01, 2024
Overview
This topic explains how to adapt code that currently uses a 4.x version of the Java server-side SDK to use version 5.0 or later.
Before you migrate to 5.0, update to the latest 4.x version. Many of the changes that are mandatory in 5.0 were originally added in a 4.x version and made optional to use.
If you update to the latest 4.x version, deprecation warnings appear in areas of your code that need to be changed for 5.0. You can then update them while still using 4.x, rather than migrating everything simultaneously.
To learn more about updating to the latest 4.x version, visit the SDK's GitHub repository.
Identifying Java versions for the 5.0 SDK
The minimum version for LaunchDarkly SDK 5.0 is Java 8. LaunchDarkly no longer supports Java 7, per our End of Life policy. Because Java 8 is still in long-term support, you must use that version or later to use the version 5.0 SDK.
The primary impact of this in the public API is that the SDK now uses java.time.Duration
to represent time intervals. Previously, these were represented by an integer number of milliseconds or seconds, depending on the case. Using Duration
avoids that ambiguity. This only affects configuration methods.
Many other improvements have been made internally to take advantage of Java 8 features, but those do not require any changes in your code.
Using updated package names
The package naming convention has changed. Instead of com.launchdarkly.client
, the main packages are now com.launchdarkly.sdk
and com.launchdarkly.sdk.server
. Because most of the class names have not changed, you may be able to fix your import statements with a find and replace.
The use of the word "client" in package names was potentially confusing, because even though this library is a service client that makes requests to the LaunchDarkly service, we built it to use in a server-side context. Classes that are specific to this server-side implementation are now in .sdk.server
.
The base .sdk
package contains classes that are not specific to server-side Java. They also exist in the Android SDK. When you view the SDK source code, you will find that the base .sdk
package is implemented in a new repository, java-sdk-common
.
To learn more about the differences between SDKs, read Client-side, server-side, and edge SDKs.
The full package schema is as follows:
-
com.launchdarkly.sdk
- Provides:
LDUser
,LDUser.Builder
,LDValue
,EvaluationDetail
,EvaluationReason
- Provides:
-
com.launchdarkly.sdk.json
- Provides: new features related to JSON serialization.
-
com.launchdarkly.sdk.server
- Provides:
Components
,LDClient
,LDConfig
,FeatureFlagsState
- Provides:
-
com.launchdarkly.sdk.server.interfaces
- Provides: types like
DataStore
, formerlyFeatureStore
, andEvent
that are only used if you are writing a custom component. These were formerly incom.launchdarkly.client
, so moving them makes the API much less cluttered.
- Provides: types like
-
com.launchdarkly.sdk.server.integrations
- Provides: the same types that used to be in
com.launchdarkly.client.integrations
, added in 4.12.0.
- Provides: the same types that used to be in
Understanding changes to SDK configuration
Java SDK 4.12.0 and 4.13.0 added newer ways to use LDConfig
. As of 5.0, those ways are mandatory. They are the only way to use LDConfig
.
Instead of having many unrelated properties, each with their own LDConfig.Builder
method, most of the properties are now grouped into areas of functionality, each of which has its own builder class that is only available if you are using that functionality. For instance, if you use streaming mode, there are options you can set to control streaming mode, and if you use polling mode, you use a different builder that does not have the streaming options. Similarly, if you have disabled analytics events, the event-related options are not accessible.
The basic areas of functionality are "data source" (polling, streaming, or file data), "data store" (memory or database), events, and networking.
If your code was already using the newer model, you should not need to change it except for changing the package names in your imports.
Understanding changes to data source methods
For each data source type, there is a factory method whose name ends in dataSource
. These methods give you a builder object with methods for whatever options are appropriate. Pass that object to LDConfig.Builder.dataSource()
.
Here is an example:
// 4.x model: setting custom options for streaming modeLDConfig config = new LDConfig.Builder().stream(true).reconnectTimeMs(2000).build();// 4.x model: specifying polling mode and setting custom polling optionsLDConfig config = new LDConfig.Builder().stream(false).pollingIntervalMillis(60000).build();
The default is to use streaming mode. Unlike the earlier model, it is no longer possible to construct a meaningless configuration such as "use streaming mode, but set the polling interval to 1 minute" or "disable events, but set the flush interval to 10 seconds."
Another data source option is the file-based data source. To learn more, read the API documentation for com.launchdarkly.sdk.server.integrations.FileData
.
Understanding changes to the data store
As before, the default data store is a simple in-memory cache.
If you want to use a persistent datastore database integration, you must call a dataStore
factory method for that integration. This gives you a builder object with whatever options are appropriate for that database. Next, pass the object to Components.persistentDataStore()
, a wrapper that provides caching options that are not specific to any one database but are built into the SDK. Put this into LDConfig.Builder.dataStore()
.
To learn more, read Persistent data stores.
The following examples use the Redis integration:
// 4.x model: use Redis, set custom Redis URI and key prefix, set cache TTL to 45 secondsLDConfig config = new LDConfig.Builder().featureStore(Components.redisFeatureStore(URI.create("redis://my-redis-host")).prefix("my-prefix").cacheTime(45, TimeUnit.SECOND)).build();
Understanding changes to events
Analytics events are enabled by default. To customize their behavior, call Components.sendEvents()
to get a builder object with event-related options, and then pass that object to LDConfig.Builder.events()
.
To completely disable events, set LDConfig.Builder.events()
to Components.noEvents()
.
If you use private user attributes and are configuring private attributes for the entire SDK rather than for individual users, there is a new syntax based on the new UserAttribute
class. This ensures that built-in attributes are not misspelled and helps to distinguish these parameters from other kinds of string parameters.
Here is an example:
// 4.x model: disabling eventsLDConfig config = new LDConfig.Builder().sendEvents(false).build();// 4.x model: customizing event behaviorLDConfig config = new LDConfig.Builder().capacity(20000).flushInterval(10).privateAttributes("email", "name", "myCustomAttribute").build();
It is no longer possible to construct a meaningless configuration like "disable events, but set the flush interval to 10 seconds."
Understanding changes to networking
Options in this category affect how the SDK communicates with LaunchDarkly over HTTP/HTTPS, including connection timeout, proxy servers, and more. If you need to customize these, call Components.httpConfiguration()
to get a builder object, configure this builder, and then pass it to LDConfig.Builder.http()
.
Some of the methods have changed slightly in terms of the type or number of parameters. More details are available in HttpConfigurationBuilder
. The biggest difference is in how you specify an authenticated proxy server, which now uses a more general concept of authentication schemes.
Here is an example:
// 4.x model: setting connection and socket timeoutsLDConfig config = new LDConfig.Builder().connectTimeout(3).socketTimeout(4).build();// 4.x model: specifying an HTTP proxy with basic authenticationLDConfig config = new LDConfig.Builder().proxyHost("my-proxy").proxyPort(8080).proxyUsername("user").proxyPassword("pass").build();
Understanding changes to the JSON value type
Before Java SDK 4.9.0, if you wanted to describe a value that could be of any valid JSON type (boolean, number, string, array, object, or null), you would use the Gson type JsonElement
.
For example, LDClient.jsonVariation()
returned that type, and also took a default value parameter of that type. However, there were two problems with using JsonElement
:
- it made the SDK's public API dependent on third-party types, and
- the array and object types were mutable, which was a potential concurrency problem.
Version 4.9.0 added the immutable class LDValue
as a replacement for this, along with LDClient.jsonValueVariation()
, which is equivalent to jsonVariation
, but uses LDValue
, and overloads in LDUser.Builder.custom()
for setting user attributes of any JSON type. As of version 5.0, this is now the only option and JsonElement
is no longer exposed in the public API.
Here is an example of setting a user custom attribute to a list of strings:
// 4.x way: set user's "groups" to ["cats", "dogs"]JsonArray groups = new JsonArray();groups.add("cats");groups.add("dogs");LDUser user = new LDUser.Builder("key").custom("group", groups).build();// The following shortcut method was exactly equivalent:LDUser user = new LDUser.Builder("key").customString("group", Arrays.asList("cats", "dogs")).build();
Understanding changes to EvaluationDetail
The EvaluationDetail
class, which was formerly in com.launchdarkly.client
, and is now in com.launchdarkly.sdk
, has been modified in the following two ways:
- The constructor is now private. To obtain an instance, call the factory method
EvaluationDetail.fromValue()
orEvaluationDetail.error()
. This lets the SDK reuse instances for commonly used values, instead of allocating new ones. - The
variationIndex
property is now anint
rather than a nullableInteger
. Instead of a null, the constantNO_VARIATION
(-1) is used in the case where evaluation failed and so no flag variation was selected. This is similar to how Java'sList.indexOf()
andString.indexOf()
can return -1 to mean "not found." The methodisDefaultValue()
still works as before.
Here is an example:
// 4.x model: create an EvaluationDetail instance, maybe for testingEvaluationDetail<String> myValue = new EvaluationDetail<>(EvaluationReason.off(), 1, "x");// 4.x model: check the variation index of the resultEvaluationDetail<String> resultDetail =client.stringVariationDetail(flagKey, user, "default string value");Integer variation = resultDetail.getVariationIndex();if (variation == null) {// do something for the default value case// note that "if (resultDetail.isDefaultValue())" would also have worked} else {doSomethingWithVariation(variation.intValue());}
Understanding changes to add-on integration packages
The Redis integration is no longer part of the main SDK distribution. It is now in a separate package: launchdarkly-java-server-sdk-redis-store
on Maven, launchdarkly/java-server-sdk-redis
on GitHub.
The Consul and DynamoDB integrations were already in separate packages, but they have new major versions that are compatible with Java SDK 5.0.
For the configuration syntax, read Data store above.
Understanding changes to logging
The SDK still uses SLF4J as a logging framework, but the use of logger names has changed. In SLF4J, logger names can be used to configure different filtering rules for different kinds of messages, so if you have been using that feature you may need to adjust your configuration.
Previously, most of the SDK's log output used the logger name com.launchdarkly.client.LDClient
, but some of it used the names of other classes in the same package, such as com.launchdarkly.client.DefaultEventProcessor
or com.launchdarkly.eventsource
. These class names were implementation details that were subject to change, so there was not a well-defined set of logger names.
Starting in version 5.0, the SDK uses the following logger names:
com.launchdarkly.sdk.server.LDClient
: general messages that do not fall into any other categoriescom.launchdarkly.sdk.server.LDClient.DataSource
: messages related to how the SDK obtains feature flag data-- normally this means messages about the streaming connection to LaunchDarkly, but if you use polling mode or file data instead, those will be logged under this name toocom.launchdarkly.sdk.server.LDClient.DataStore
: messages related to how feature flag data is stored-- for instance, database errors if you are using a database integrationcom.launchdarkly.sdk.server.LDClient.Evaluation
: messages related to feature flag evaluationcom.launchdarkly.sdk.server.LDClient.Events
: messages related to analytics event processing
The use of log levels has also changed somewhat. Previously, many kinds of I/O errors-- for instance, a network failure when the SDK was trying to make a streaming connection to LaunchDarkly, or trying to send analytics events-- were logged at ERROR
level. This could cause an unwanted amount of noise from monitoring systems, because such errors were often due to temporary problems that the SDK could recover from without intervention.
Starting in version 5.0, if the SDK gets a network error that might interfere with receiving feature flag data from LaunchDarkly, it will log it at first at WARN
level. Then it will continue retrying the connection as usual. If it remains unable to get a successful connection for a full minute, then it will report the problem again at the more serious ERROR
level. You can change the connection interval using LoggingConfigurationBuilder
.
Using the Relay Proxy
There are two ways you can configure the SDK to use the Relay Proxy:
- proxy mode, connecting to it via HTTP/HTTPS just as if it were the LaunchDarkly service
- daemon mode, receiving flag data only through a database.
A new syntax for configuring these was added in version 4.12.0, and in version 5.0 the new syntax is the only way.
Here is an example:
// 4.x model: proxy mode with streamingURI relayUri = URI.create("http://my-relay-host:8000");LDConfig config = new LDConfig.Builder().baseUri(relayUri).streamUri(relayUri).eventsUri(relayUri) // if you want to proxy events.build();// 4.x model: proxy mode with pollingURI relayUri = URI.create("http://my-relay-host:8000");LDConfig config = new LDConfig.Builder().stream(false).baseUri(relayUri).eventsUri(relayUri) // if you want to proxy events.build();// 4.x model: daemon mode with a Redis databaseLDConfig config = new LDConfig.Builder().featureStore(Components.redisFeatureStore(URI.create("redis://my-redis-host"))).useLdd(true).build();
Converting objects to or from JSON data
You might want to convert LaunchDarkly SDK classes such as LDUser
, LDValue
, or FeatureFlagsState
into JSON if you are passing them to front-end JavaScript code. Less commonly, you might want to convert them from JSON. The SDK provides ways to do both.
First, you can use the serialize and deserialize methods in com.launchdarkly.sdk.json.JsonSerialization
to quickly convert things to or from JSON strings.
Second, if you prefer to use the popular Gson or Jackson JSON frameworks for Java, read the following section.
Gson
Previously, classes from Gson were exposed in the SDK's public API, so gson
had to be a regular dependency on your application classpath. This has been changed so that even though Gson is used internally by the SDK, it is a private shaded copy that does not interact with outside code. This means it cannot conflict with any other Gson dependency in your application.
However, this also means that if you want to convert LaunchDarkly SDK objects such as LDUser
, LDValue
, or FeatureFlagsState
to or from JSON—for instance, to pass them to front-end JavaScript code—and you are using Gson, you can't simply call Gson.toJson()
or Gson.fromJson()
and get correct results. The Gson in your application does not automatically recognize the Gson annotations and custom serializers that are contained in the SDK.
To make this work correctly, you must add an extra step to the setup of your Gson instance:
import com.google.gson.*;import com.launchdarkly.sdk.json.LDGson;Gson gson = new GsonBuilder().registerTypeAdapterFactory(LDGson.typeAdapters())// any other GsonBuilder options go here.create();
If you have registered LDGson.typeAdapters()
in the Gson instance you're using, toJson()
and fromJson()
will work as expected with LaunchDarkly types.
Jackson
Previously, it was not possible to convert LaunchDarkly SDK classes to or from JSON using Jackson. This is now possible as long as you configure your Jackson ObjectMapper
with LDJackson.module()
as follows:
import com.google.jackson.databind.*;import com.launchdarkly.sdk.json.LDJackson;ObjectMapper mapper = new ObjectMapper();mapper.registerModule(LDJackson.module());
Using other third-party dependencies
Because Java 7 compatibility is no longer an issue, the SDK now uses more modern versions of the okhttp
, guava
, and gson
packages internally.
As before, if you want to force the SDK to use the same versions of these dependencies that you are using, you can use the "thin" jar which does not embed them.
Implementing custom components
Most applications use either the default in-memory storage or one of the database integrations provided by LaunchDarkly, including Consul, DynamoDB, and Redis. However, the data store interface, formerly called "feature store," has always been public so developers can write their own integrations.
Starting in Java SDK 5.0, this model has been changed to make it easier to implement database integrations. The basic concepts are the same: the SDK defines its own "data kinds," such as feature flags and user segments. The data store must provide a way to add and update items of any of these kinds, without knowing anything about their properties except the key and version.
The main changes are:
- Caching is now handled by the SDK, not by the store implementation. Now you only need to implement the lower-level operations of adding or updating data.
- The SDK takes care of serializing and deserializing the data, so the store operates only on strings.
- Data structures have been simplified to avoid the use of generics and maps.
The interface for "data source" components that receive feature flag data, either from LaunchDarkly or from some other source, such as a file, has also changed slightly to use the new data store model.
You may also want to review the source code for one of the LaunchDarkly database integrations, such as Redis, to learn how it changed from the previous major version.
Miscellaneous API changes
Here are several changes to names and types that do not affect the basic functionality of the SDK, but were changed for consistency.
- The
initialized()
method ofLDClient
was renamed toisInitialized()
, for consistency with Java naming conventions for methods that return a boolean. - The
intVariation()
anddoubleVariation()
methods ofLDClient
previously used the nullable typesInteger
andDouble
. They have been changed to useint
anddouble
, because the point of these type-specific methods is to be able to conveniently treat the feature flag value as a numeric type without having to worry about whether it is a null value or some other type. TheboolVariation
method already used the non-nullableboolean
type, so it has not changed.
Understanding what was deprecated
All types and methods that were marked as deprecated in the last 4.x release have been removed from the 5.0 release. If you were using these with a recent version previously, you should already have received deprecation warnings at compile time, with suggestions about their recommended replacements.
For a full list of all deprecated symbols, read the release notes for 5.0.0.
The built-in New Relic integration has also been removed. This was never part of the public API, so it does not affect any application code.
Understanding new functionality
The 5.0 release also includes new features as described in the release notes.
These include:
- You can tell the SDK to notify you whenever a feature flag's configuration has changed, using
LDClient.getFlagTracker()
. - You can monitor the status of the SDK's connection to LaunchDarkly services with
LDClient.getDataSourceStatusProvider()
. - You can monitor the status of a persistent data store, for example to get caching statistics or to be notified if the store's availability changes due to a database outage, with
LDClient.getDataStoreStatusProvider()
. - You can configure the SDK's use of different logging levels depending on the duration of a service outage with
LDConfig.Builder.logging()
andLoggingConfigurationBuilder
. - The thread priority for SDK background tasks can be configured with
LDConfig.Builder.threadPriority()
.
To learn more, read the complete API documentation for these classes and methods.