This guide provides detailed information about our Java SDK. All of our SDKs are open source. Go to our Java SDK GitHub repository to see the source code.
Language Support
The Java SDK supports JDK8 and later.
Initialization
To get started, set up Split in your code base with two simple steps.
1. Import the SDK into your project
You can import the SDK into your project using either of the two methods below.
<dependency>
<groupId>io.split.client</groupId>
<artifactId>java-client</artifactId>
<version>4.1.3</version>
</dependency>
compile 'io.split.client:java-client:4.1.3'
If you cannot find the dependency, it may be due to the lag in the sync time between Sonatype and Maven central. In this case, use the following repository.
<repositories>
<repository>
<id>sonatype releases</id>
<url>https://oss.sonatype.org/content/repositories/releases/</url>
</repository>
</repositories>
2. Instantiate the SDK and create a new Split client
If upgrading an existing SDK - Block until ready changes
Starting version 3.0.1, SplitClientConfig#ready(int) is deprecated and migrated to a two part implementation:
- Set the desired value in
SplitClientConfig#setBlockUntilReadyTimeout(int)
. - Call
SplitClient#blockUntilReady()
orSplitManager#blockUntilReady()
.
When the SDK is instantiated, it kicks off background tasks to update an in-memory cache with small amounts of data fetched from Split servers. This process can take up to a few hundred milliseconds depending on the size of data. If the SDK is asked to evaluate which treatment to show to a customer for a specific split while its in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the SDK does not fail, rather, it returns the control treatment.
To make sure the SDK is properly loaded before asking it for a treatment, block until the SDK is ready. This is done by setting the desired wait using .setBlockUntilReadyTimeout()
in the configuration and calling blockUntilReady()
on the client. Do this all as a part of the startup sequence of your application.
We recommend instantiating the SDK once as a singleton and reusing it throughout your application.
Use the code snippet below and plug in your API key. The API key is available on your Organization Settings page, on the APIs tab. The API key is of type sdk
. For more information, see Understanding API Keys.
import io.split.client.SplitFactoryBuilder;
import io.split.client.SplitClient;
import io.split.client.SplitClientConfig;
import io.split.client.SplitFactory;
SplitClientConfig config = SplitClientConfig.builder()
.setBlockUntilReadyTimeout(10000)
.build();
SplitFactory splitFactory = SplitFactoryBuilder.build("YOUR_API_KEY", config);
SplitClient client = splitFactory.client();
try {
client.blockUntilReady();
} catch (TimeoutException | InterruptedException e) {
// log & handle
}
Now you can start asking the SDK to evaluate treatments for your customers.
Using the SDK
Basic use
After you instantiate the SDK client, you can start using the getTreatment
method of the SDK client to decide what version of your features your customers are served. The method requires the SPLIT_NAME
attribute that you want to ask for a treatment and a unique key
attribute that corresponds to the end user that you are serving the feature to.
Then use an if-else-if block as shown below and insert the code for the different treatments that you defined in the Split UI. Remember the final else branch in your code to handle the client returning the control treatment.
// The key here represents the ID of the user/account/etc you're trying to evaluate a treatment for
String treatment = client.getTreatment("key","SPLIT_NAME");
if (treatment.equals("on")) {
// insert code here to show on treatment
} else if (treatment.equals("off")) {
// insert code here to show off treatment
} else {
// insert your control treatment code here
}
Shutdown
You should make sure to call .destroy()
before letting a process using the SDK exit as it gracefully shuts down the Split SDK by stopping all background threads, clearing caches, closing connections and flushing the remaining unpublished impressions and events. The Java SDK specifically subscribes to the JVM shutdown hook (SIGTERM signal) which in normal circumstances is invoked automatically by the JVM during a shutdown process. This means that on a graceful shutdown of the server, the client will automatically call destroy() and will flush the buffers and release the resources.
In cases where you don't want our SDK to automatically destroy on shutdown, you can use the config: disableDestroyOnShutDown()
(example usage in the Configuration section below) and set it to true
.
If you do this, the SDK will ignore any signals like SIGTERM and it will be your responsibility to properly call destroy at the right time. If a manual shutdown is required, you can then call:
client.destroy()
After destroy()
is called, any subsequent invocations to client.getTreatment()
or manager methods result in control
or empty list, respectively.
Important!
A call to the destroy()
method also destroys the factory object. When creating new client instance, first create a new factory instance.
Multiple evaluations at once
This functionality is currently not supported for this SDK, but is coming soon! Subscribe to our release notes for updates.
Attribute syntax
To target based on custom attributes, the SDK's getTreatment
method needs to be passed an attribute map at runtime.
In the example below, we are rolling out a split to users. The provided attributes plan_type
, registered_date
, permissions
, paying_customer
, and deal_size
are passed to the getTreatment
call. These attributes are compared and evaluated against the attributes used in the Rollout plan as defined in the Split Web console to decide whether to show the on
or off
treatment to this account.
The getTreatment
method supports five types of attributes: strings, numbers, dates, booleans, and sets. The proper data type and syntax for each are:
- Strings: Use type String.
- Numbers: Use type
java.lang.Long
orjava.lang.Integer
. - Dates: Express the value in
milliseconds since epoch
. In Java,milliseconds since epoch
is of typejava.lang.Long
. For example, the value for theregistered_date
attribute below isSystem.currentTimeInMillis()
, which is a long. - Booleans: Use type
java.lang.boolean
. - Sets: Use type
java.util.Collection
.
import io.split.client.SplitFactoryBuilder;
import io.split.client.SplitFactory;
import io.codigo.client.SplitClient;
import java.util.Map;
import java.util.HashMap;
import java.util.Date;
SplitFactory splitFactory = SplitFactoryBuilder.build("YOUR_API_KEY");
SplitClient client = splitFactory.client();
Map<String, Object> attributes = new HashMap<String, Object>();
attributes.put("plan_type", "growth");
attributes.put("registered_date", System.currentTimeMillis());
attributes.put("deal_size", 1000);
attributes.put("paying_customer", true);
String[] perms = {"read", "write"};
attributes.put("permissions",perms);
String treatment = client.getTreatment("key", "SPLIT_NAME", attributes);
if (treatment.equals("on")) {
// insert on code here
} else if (treatment.equals("off")) {
// insert off code here
} else {
// insert control code here
}
Get Treatments with Configurations
To leverage dynamic configurations with your treatments, you should use the getTreatmentWithConfig
method.
This method will return an object containing the treatment and associated configuration.
The config element will be a stringified version of the configuration JSON defined in the Split web console. If there is no configuration defined for a treatment, the SDK will return null
for the config parameter.
This method takes the exact same set of arguments as the standard getTreatment
method. See below for examples on proper usage:
SplitResult result = client.getTreatmentWithConfig("key", "new_boxes", attributes);
String treatment = result.treatment();
if (null != result.config()) {
MyConfiguration config = gson.fromJson(result.config(), MyConfiguration.class);
}
Track
Use the track
method to record any actions your customers perform. Each action is known as an event
and corresponds to an event type
. Calling track
through one of our SDKs or via the API is the first step to getting experimentation data into Split and allows you to measure the impact of your splits on your users' actions and metrics.
Learn more about using track events in splits.
In the examples below you can see that the .track()
method can take up to five arguments. The proper data type and syntax for each are:
- key: The
key
variable used in thegetTreatment
call and firing this track event. The expected data type is String. - TRAFFIC_TYPE: The traffic type of the key in the track call. The expected data type is String. You can only pass values that match the names of traffic types that you have defined in your instance of Split.
- EVENT_TYPE: The event type that this event should correspond to. The expected data type is String. Full requirements on this argument are:
- Contains 63 characters or fewer.
- Starts with a letter or number.
- Contains only letters, numbers, hyphen, underscore, or period.
- This is the regular expression we use to validate the value:
[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}
- VALUE: (Optional) The value to be used in creating the metric. This field can be sent in as null or 0 if you intend to purely use the count function when creating a metric. The expected data type is Integer or Float.
- PROPERTIES: (Optional) A Map of key value pairs that can be used to filter your metrics. Learn more about event property capture here. Split currently supports three types of properties: strings, numbers, and booleans.
The track
method returns a boolean value of true
or false
to indicate whether or not the SDK was able to successfully queue the event to be sent back to Split's servers on the next event post. The SDK will return false
if the current queue size is equal to the config set by eventsQueueSize
or if an incorrect input to the track
method has been provided.
In the case that a bad input has been provided, you can read more about our SDK's expected behavior here
// If you would like to send an event without a value
boolean trackEvent = client.track("key", "TRAFFIC_TYPE", "EVENT_TYPE");
// Example
boolean trackEvent = client.track("john@doe.com", "user", "page_load_time");
// If you would like to associate a value to an event
boolean trackEvent = client.track("key", "TRAFFIC_TYPE", "EVENT_TYPE", VALUE);
// Example
boolean trackEvent = client.track("john@doe.com", "user", "page_load_time", 83.334);
// If you would like to associate a value and properties to an event
boolean trackEvent = client.track("key", "TRAFFIC_TYPE", "EVENT_TYPE", VALUE, {PROPERTIES});
// Example
HashMap<String, Object> properties = new HashMap<>();
properties.put("package", "premium");
properties.put("admin", true);
properties.put("discount", 50);
boolean trackEvent = client.track("john@doe.com", "user", "page_load_time", 83.334, properties);
// If you would like to associate just properties to an event
boolean trackEvent = client.track("key", "TRAFFIC_TYPE", "EVENT_TYPE", {PROPERTIES});
// Example
HashMap<String, Object> properties = new HashMap<>();
properties.put("package", "premium");
properties.put("admin", true);
properties.put("discount", 50);
boolean trackEvent = client.track("john@doe.com", "user", "page_load_time", properties);
Configuration
The SDK has a number of knobs for configuring performance. Each knob is tuned to a reasonable default. However, you can override the value while instantiating the SDK. The parameters available for configuration are shown below.
Configuration | Description | Default value |
---|---|---|
featuresRefreshRate | The SDK polls Split servers for changes to feature rollout plans. This parameter controls this polling period in seconds. | 60 seconds |
segmentsRefreshRate | The SDK polls Split servers for changes to segments at this rate (in seconds). | 60 seconds |
impressionsRefreshRate | The treatment log captures which customer saw what treatment (on, off, etc) at what time. This log is periodically flushed back to Split servers. This configuration controls how quickly the cache expires after a write (in seconds). | 60 seconds |
metricsRefreshRate | The SDK caches diagnostic data that it periodically sends to Split servers. This configuration controls how frequently this data is sent back to Split servers (in seconds). | 60 seconds |
eventsQueueSize | When using .track , the number of events to be kept in memory. |
500 |
eventFlushIntervalInMillis | When using .track , how often (in miliseconds) the events queue is flushed to Split servers. |
30000 ms |
connectionTimeOutInMs | HTTP client connection timeout (in ms). | 15000ms |
readTimeoutInMs | HTTP socket read timeout (in ms). | 15000ms |
setBlockUntilReadyTimeout | If specified, the client building process blocks until the SDK is ready to serve traffic or the specified time has elapsed. If the SDK is not ready within the specified time, a TimeOutException is thrown (in ms). |
0ms |
impressionsQueueSize | Default queue size for impressions. | 30K |
disableLabels | Disable labels from being sent to Split backend. Labels may contain sensitive information. | enabled |
disableIPAddress | Disable sending IP Address & hostname to the backend. | enabled |
proxyHost | The location of the proxy. | localhost |
proxyPort | The port of the proxy. | -1 (not set) |
proxyUsername | Username to authenticate against the proxy server. | null |
proxyPassword | Password to authenticate against the proxy server. | null |
StreamingEnabled | Boolean flag to enable the streaming service as default synchronization mechanism. In the event of an issue with streaming, the SDK will fallback to the polling mechanism. If false, the SDK will poll for changes as usual without attempting to use streaming. | true |
impressionsMode | This configuration defines how impressions are queued on the SDK. Supported modes are OPTIMIZED and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Split. In DEBUG mode, ALL impressions are queued and sent to Split. Use DEBUG mode when you want every impression to be logged in Split's webconsole when trying to debug your SDK setup. This setting does not impact the impression listener which will receive all generated impressions. | OPTIMIZED |
To set each of the parameters defined above, use the following syntax.
import io.split.client.*;
SplitClientConfig config = SplitClientConfig.builder()
.impressionsRefreshRate(60)
.metricsRefreshRate(60)
.connectionTimeout(15000)
.readTimeout(15000)
.enableDebug()
.setBlockUntilReadyTimeout(10000)
.build();
SplitFactory splitFactory = SplitFactoryBuilder.build("YOUR_SDK_API_KEY",config);
SplitClient client = splitFactory.client();
client.blockUntilReady();
Connecting to Synchronizer using proxy mode
Java SDK can only connect to the Split Synchronizer in Proxy mode. Currently it does not support the Redis integration.
In Proxy mode, the SDK connects directly to the Split Synchronizer as though it was connecting to our CDN, and the Synchronizer synchs the data and writes impressions and events back to the Split server.
Make sure to install the Split Synchronizer following the steps in Split Synchronizer.
To start the Split Synchronizer in Proxy mode, pass the -proxy
, -sync-admin-port
, -proxy-port
, and -proxy-apikeys
parameters, as shown below.
split-sync -proxy -sync-admin-port 3010 -proxy-port 3000 -api-key "split_sdk_key" -proxy-apikeys "myownapikey_not_sdk_or_admin_key"
The -proxy
parameter forces the Synchronizer to use proxy mode. The same port used for the admin dashboard page can be used for the -sync-admin-port
parameter.
The -proxy-port
parameter determines where the Java SDK connects.
The -proxy-apikeys
parameter specifies any custom key that the developer may want to use for Java SDK to connect to Split Synchronizer, and can be any string of characters. -proxy-apikeys
is not an SDK or admin key created in Split.
Use the .endpoint()
property in the SplitClientConfig builder object to point the Java SDK to the Synchronizer, making sure to use the proxy port specified in the Synchronizer command line.
When creating the SplitFactory
object, use the custom API key specified in the -proxy-apikeys
parameter for the Synchronizer. The Synchronizer uses the Split API key when connecting to Split.
For the Synchronizer process started above, here is the Java code example.
import io.split.client.SplitFactoryBuilder;
import io.split.client.SplitClient;
import io.split.client.SplitClientConfig;
import io.split.client.SplitFactory;
public class SplitSD {
public static void main(String[] args) {
SplitClientConfig config = SplitClientConfig.builder()
.setBlockUntilReadyTimeout(10000)
.enableDebug()
.endpoint("http://localhost:3000", "http://localhost:3000")
.build();
try {
SplitFactory splitFactory = SplitFactoryBuilder.build("myownapikey", config);
SplitClient client = splitFactory.client();
client.blockUntilReady()
String treatment = client.getTreatment("user10","sample_feature");
if (treatment.equals("on")) {
System.out.print("Treatment is on");
} else if (treatment.equals("off")) {
System.out.print("Treatment is off");
} else {
System.out.print("SDK Not ready");
}
} catch (Exception e) {
System.out.print("Exception: "+e.getMessage());
}
}
}
Localhost mode
Features start their life on one developer's machine. A developer should be able to put code behind splits on their development machine without the SDK requiring network connectivity. To achieve this, the Split SDK can be started in localhost mode (aka off-the-grid mode). In this mode, the SDK neither polls nor updates Split servers. Instead, it uses an in-memory data structure to determine what treatments to show to the logged in customer for each of the features.
Here is how you can start the SDK in localhost mode.
Since version 3.1.0, our SDK supports a new type of localhost split definition file, using the YAML format.
This new format allows the user to map different keys to different treatments within a single split, and also add configurations to them.
The new format is a list of single-key maps (one per mapping split-keys-config), defined as follows:
# - feature_name:
# treatment: "treatment_applied_to_this_entry"
# keys: "single_key_or_list"
# config: "{\"desc\" : \"this applies only to ON treatment\"}"
- my_feature:
treatment: "on"
keys: "key"
config: "{\"desc\" : \"this applies only to ON treatment\"}"
- some_other_feature:
treatment: "off"
- my_feature:
treatment: "off"
- other_feature:
treatment: "off"
keys: ["key_1", "key_2"]
config: "{\"desc\" : \"this overrides multiple keys and returns off treatment for those keys\"}"
In the example above, we have 3 entries:
- The first one defines that for split
my_feature
, the keykey
will return the treatmenton
and theon
treatment will be tied to the configuration{"desc" : "this applies only to ON treatment"}
. - The second entry defines that the feature
some_other_feature
will always return theoff
treatment and no configuration. - The third entry defines that
my_feature
will always returnoff
for all keys that don't match another entry (in this case, any key other thankey
). - The forth entry shows how an example to override a treatment for a set of keys.
Use the SplitConfigBuilder object to set the location of the Split localhost YAML file as shown in the example below:
import io.split.client.SplitFactoryBuilder;
import io.split.client.SplitClient;
import io.split.client.SplitClientConfig;
SplitClientConfig config = SplitClientConfig.builder().splitFile("..../split.yaml").build();
SplitClient client = SplitFactoryBuilder.build("localhost", config).client();
If SplitClientConfig.splitFile() is not set, Split SDK maintains backward compatibility by trying to load the legacy file (.split), now deprecated, from $HOME/.split
.
The format of this legacy and deprecated file is three columns separated by whitespace. The columns, from left to right are:
- Split name
s
(Required) - Treatment
t
(Required) - Key
k
(Optional) - support for this third column added in v2.3.1
If k
is not provided, t
is returned for s for every customer. In other words getTreatment(*, s) = t
. If k
is provided, t
is only returned for the pair (k, s
). In other words getTreatment(k, s) = t
. If there are duplicate or conflicting lines, later lines take precedence over earlier lines.
Here is a sample .split
file.
# this is a comment
# sdk.getTreatment(user1, reporting_v2) will return 'off' however,
# sdk.getTreatment(*, reporting_v2) will return 'on'
reporting_v2 on
reporting_v2 off user1
double_writes_to_cassandra off
new-navigation v3
Any changes to this Splits file (YAML or legacy format) trigger a reload of active treatments in the Split Client and Split Manager to reflect the updated file. As a result, a developer does not have to restart their local application just to modify a split. When a file change occurs, the Split SDK identifies that change within 5 seconds and logs the reload to the console. Here is an example console message.
Detected change in Local Splits file - Splits Reloaded! file=<.split file path>
Manager
Use the Split Manager to get a list of features available to the Split client.
To instantiate a Manager in your code base, use the same factory that you used for your client.
SplitFactory splitFactory = SplitFactoryBuilder.build("YOUR_API_KEY");
SplitManager manager = splitFactory.manager();
The Manager then has the following methods available.
/**
* Retrieves the Splits that are currently registered with the
* SDK.
*
* @return a List of SplitView or empty.
*/
List<SplitView> splits();
/**
* Returns the Splits registered with the SDK of this name.
*
* @return SplitView or null
*/
SplitView split(String SplitName);
/**
* Returns the names of features (or Splits) registered with the SDK.
*
* @return a List of String (Split Feature Names) or empty
*/
List<String> splitNames();
The SplitView
object that you see referenced above has the following structure.
public class SplitView {
public String name;
public String trafficType;
public boolean killed;
public List<String> treatments;
public long changeNumber;
}
Listener
Split SDKs send impression data back to Split servers periodically and as a result of evaluating splits. To additionally send this information to a location of your choice, define and attach an impression listener.
The SDK sends the generated impressions to the impression listener right away. As a result, be careful while implementing handling logic to avoid blocking the main thread. As the second parameter, specify the size of the queue acting as a buffer (see the snippet below).
If the impression listener is slow at processing the incoming data, the queue fills up and any subsequent impressions are dropped.
SplitClientConfig config = SplitClientConfig.builder()
.integrations(
IntegrationsConfig.builder()
.impressionsListener(new MyImpressionListener(), 500)
.build())
.build();
SplitFactoryBuilder.build("<API_TOKEN>").client();
// Custom Impression listener class
static class MyImpressionListener implements ImpressionListener {
@Override
public void log(Impression impression) {
// Send this data somewhere. Printing to console for now.
System.out.println(impression);
}
@Override
public void close() {
// Do something
}
}
Logging
The Java SDK uses slf4j-api for logging. If you do not provide an implementation for slf4j, you see the following error in your logs.
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
You can get the SDK to log by providing a concrete implementation for SLF4J. For instance, if you are using log4j, you should import the following dependency.
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.21</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.21</version>
</dependency>
If you have a log4j.properties in your classpath, the SDK log is visible. Here is an example of log4j.properties entry.
log4j.rootLogger=DEBUG, A1
log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n
Here is an example of initializing the logger object in Java.
import io.split.client.SplitFactoryBuilder;
import io.split.client.SplitClient;
import io.split.client.SplitClientConfig;
import io.split.client.SplitFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SplitSD {
final static Logger logger = LoggerFactory.getLogger(SplitSD.class);
public static void main(String[] args) {
SplitClientConfig config = SplitClientConfig.builder()
.setBlockUntilReadyTimeout(10000)
.enableDebug()
.build();
Integrations
New Relic
The New Relic integration annotates New Relic transactions with Split feature flags information that can be used to correlate application metrics with feature flag changes.
This integration is implemented as a synchronous impression listener and it can be enabled as shown below:
SplitClientConfig config = SplitClientConfig.builder()
.integrations(
IntegrationsConfig.builder()
.newRelicImpressionListener()
.build())
.build();
SplitFactoryBuilder.build("<API_TOKEN>").client();
This integration will only be enabled if Split SDK detects the New Relic agent in the classpath. If the agent is not detected, the following error will be displayed in the logs (if logging is enabled):
WARN [main] (IntegrationsConfig.java:72) - New Relic agent not found. Continuing without it
Network proxy
If you need to use a network proxy, you can configure proxies by setting the proxyHost
and proxyPort
options in the SDK configuration (see Configuration section for details). The SDK reads those variables and uses them to perform the server request.
Advanced: WebLogic container
WebLogic as well as the Split Java SDK contain a reference to Google Guava. If you are currently deploying a web application that contains our Java SDK into WebLogic, instruct the container to load Guava from the app classpath and not from the container.
If you have an existing weblogic.xml file in your deployment, add: <package-name>com.google.common.*</package-name>
under the <prefer-application-packages>
tag. If you do not, create the file and place it under the directory WEB-INF
.
Here is a sample of a weblogic.xml file that includes the previously mentioned Guava classpath loading instruction.
<?xml version="1.0" encoding="UTF-8"?>
<weblogic-web-app xmlns="http://xmlns.oracle.com/weblogic/weblogic-web-app">
<context-root>/testing-java</context-root>
<container-descriptor>
<prefer-web-inf-classes>false</prefer-web-inf-classes>
<prefer-application-packages>
<package-name>com.google.common.*</package-name>
</prefer-application-packages>
</container-descriptor>
</weblogic-web-app>
Comments
0 comments
Please sign in to leave a comment.