This is a port of Featurevisor Javascript SDK v3.x to Java, providing a way to evaluate feature flags, variations, and variables in your Java applications.
This SDK supports Featurevisor v3 behavior and v2 datafiles. Generated datafiles continue to carry schemaVersion: "2".
- Installation
- Public API
- Initialization
- Evaluation types
- Context
- Check if enabled
- Getting variation
- Getting variables
- Getting all evaluations
- Sticky
- Setting datafile
- Diagnostics
- Events
- Evaluation details
- Modules
- Child instance
- Close
- CLI usage
- Development of this package
- License
In your Java application, update pom.xml to add the following:
For finding GitHub Package (public package):
<repositories>
<repository>
<id>github</id>
<name>GitHub Packages</name>
<url>https://maven.pkg.github.com/featurevisor/featurevisor-java</url>
</repository>
</repositories>Add Featurevisor Java SDK as a dependency with your desired version:
<dependencies>
<dependency>
<groupId>com.featurevisor</groupId>
<artifactId>featurevisor-java</artifactId>
<version>0.1.0</version>
</dependency>
</dependencies>Find latest version here: https://github.com/featurevisor/featurevisor-java/packages
To authenticate with GitHub Packages, in your ~/.m2/settings.xml file, add the following:
<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
http://maven.apache.org/xsd/settings-1.0.0.xsd">
<servers>
<server>
<id>github</id>
<username>YOUR_GITHUB_USERNAME</username>
<password>YOUR_GITHUB_TOKEN</password>
</server>
</servers>
</settings>You can generate a new GitHub token with read:packages scope here: https://github.com/settings/tokens
See example application here: https://github.com/featurevisor/featurevisor-example-java
The main runtime API is Featurevisor.createFeaturevisor():
import com.featurevisor.sdk.FeaturevisorLogLevel;
Featurevisor f = Featurevisor.createFeaturevisor(
new Featurevisor.FeaturevisorOptions().datafile(datafileContent)
);Most applications only need Featurevisor.createFeaturevisor, the Featurevisor instance type, and Featurevisor.FeaturevisorOptions. Public extension and observability types include FeaturevisorModule, FeaturevisorDiagnostic, and the datafile model types.
The SDK can be initialized by passing datafile content directly:
import com.featurevisor.sdk.Featurevisor;
// Load datafile content
String datafileUrl = "https://cdn.yoursite.com/datafile.json";
String datafileContent = "..." // load your datafile content
// Create SDK instance
Featurevisor f = Featurevisor.createFeaturevisor(
new Featurevisor.FeaturevisorOptions().datafile(datafileContent)
);or by constructing a Featurevisor.FeaturevisorOptions object:
Featurevisor f = Featurevisor.createFeaturevisor(new Featurevisor.FeaturevisorOptions()
.datafile(datafileContent)
);We will learn about several different options in the next sections.
We can evaluate 3 types of values against a particular feature:
- Flag (
boolean): whether the feature is enabled or not - Variation (
Object): the variation of the feature (if any) - Variables: variable values of the feature (if any)
These evaluations are run against the provided context.
Contexts are attribute values that we pass to SDK for evaluating features against.
Think of the conditions that you define in your segments, which are used in your feature's rules.
They are plain maps:
Map<String, Object> context = new HashMap<>();
context.put("userId", "123");
context.put("country", "nl");
// ...other attributesContext can be passed to SDK instance in various different ways, depending on your needs:
You can set context at the time of initialization:
Map<String, Object> initialContext = new HashMap<>();
initialContext.put("deviceId", "123");
initialContext.put("country", "nl");
Featurevisor f = Featurevisor.createFeaturevisor(new Featurevisor.FeaturevisorOptions()
.datafile(datafileContent)
.context(initialContext));This is useful for values that don't change too frequently and available at the time of application startup.
You can also set more context after the SDK has been initialized:
Map<String, Object> additionalContext = new HashMap<>();
additionalContext.put("userId", "123");
additionalContext.put("country", "nl");
f.setContext(additionalContext);This will merge the new context with the existing one (if already set).
If you wish to fully replace the existing context, you can pass true in second argument:
Map<String, Object> newContext = new HashMap<>();
newContext.put("deviceId", "123");
newContext.put("userId", "234");
newContext.put("country", "nl");
newContext.put("browser", "chrome");
f.setContext(newContext, true); // replace existing contextYou can optionally pass additional context manually for each and every evaluation separately, without needing to set it to the SDK instance affecting all evaluations:
Map<String, Object> context = new HashMap<>();
context.put("userId", "123");
context.put("country", "nl");
boolean isEnabled = f.isEnabled("my_feature", context);
String variation = f.getVariation("my_feature", context);
String variableValue = f.getVariableString("my_feature", "my_variable", context);When manually passing context, it will merge with existing context set to the SDK instance before evaluating the specific value.
Further details for each evaluation types are described below.
Once the SDK is initialized, you can check if a feature is enabled or not:
String featureKey = "my_feature";
boolean isEnabled = f.isEnabled(featureKey);
if (isEnabled) {
// do something
}You can also pass additional context per evaluation:
Map<String, Object> additionalContext = new HashMap<>();
// ...additional context
boolean isEnabled = f.isEnabled(featureKey, additionalContext);If your feature has any variations defined, you can evaluate them as follows:
String featureKey = "my_feature";
String variation = f.getVariation(featureKey);
if ("treatment".equals(variation)) {
// do something for treatment variation
} else {
// handle default/control variation
}Additional context per evaluation can also be passed:
String variation = f.getVariation(featureKey, additionalContext);Your features may also include variables, which can be evaluated as follows:
String variableKey = "bgColor";
String bgColorValue = f.getVariableString(featureKey, variableKey);Additional context per evaluation can also be passed:
String bgColorValue = f.getVariableString(featureKey, variableKey, additionalContext);Next to generic getVariable() methods, there are also type specific methods available for convenience:
f.getVariableBoolean(featureKey, variableKey, context);
f.getVariableString(featureKey, variableKey, context);
f.getVariableInteger(featureKey, variableKey, context);
f.getVariableDouble(featureKey, variableKey, context);
f.getVariableArray(featureKey, variableKey, context);
f.<Map<String, Object>>getVariableObject(featureKey, variableKey, context);
f.<MyCustomClass>getVariableObject(featureKey, variableKey, context);
f.<Map<String, Object>>getVariableJSON(featureKey, variableKey, context);
f.<MyCustomClass>getVariableJSON(featureKey, variableKey, context);
f.getVariableJSONNode(featureKey, variableKey, context);Type specific methods do not coerce values. getVariableInteger() returns null for the string "1", and boolean getters return null for non-boolean values.
For strongly typed decoding, additional overloads are available:
import com.fasterxml.jackson.core.type.TypeReference;
// Array decoding using Class<T>
List<MyItem> items = f.getVariableArray(featureKey, variableKey, context, MyItem.class);
// Array decoding using TypeReference
List<Map<String, Object>> rows = f.getVariableArray(
featureKey,
variableKey,
context,
new TypeReference<List<Map<String, Object>>>() {}
);
// Object decoding using Class<T>
MyConfig config = f.getVariableObject(featureKey, variableKey, context, MyConfig.class);
// Object decoding using TypeReference
Map<String, List<MyConfig>> nested = f.getVariableObject(
featureKey,
variableKey,
context,
new TypeReference<Map<String, List<MyConfig>>>() {}
);Typed overloads are additive and non-breaking. If decoding fails for the requested target type, these methods return null.
For dynamic JSON values with unknown shape, use getVariableJSONNode:
import com.fasterxml.jackson.databind.JsonNode;
JsonNode node = f.getVariableJSONNode(featureKey, variableKey, context);
if (node != null && node.isObject()) {
String nested = node.path("key").path("nested").asText(null);
}If a variable schema type is json and the resolved value is a malformed stringified JSON, JSON parsing fails safely and these methods return null:
getVariable(...)getVariableJSONNode(...)getVariableJSON(...)
You can get evaluations of all features available in the SDK instance:
import com.featurevisor.sdk.EvaluatedFeatures;
import com.featurevisor.sdk.EvaluatedFeature;
EvaluatedFeatures allEvaluations = f.getAllEvaluations(context);
// Access the evaluations map
Map<String, EvaluatedFeature> evaluations = allEvaluations.getValue();
System.out.println(evaluations);
// {
// "myFeature": {
// "enabled": true,
// "variation": "control",
// "variables": {
// "myVariableKey": "myVariableValue"
// }
// },
//
// "anotherFeature": {
// "enabled": true,
// "variation": "treatment"
// }
// }This is handy especially when you want to pass all evaluations from a backend application to the frontend.
For the lifecycle of the SDK instance in your application, you can set some features with sticky values, meaning that they will not be evaluated against the fetched datafile:
Sticky values belong to an SDK or child instance. Evaluation options do not accept sticky overrides; use new Featurevisor.SpawnOptions().sticky(...) when a child needs its own sticky state.
Map<String, Object> stickyFeatures = new HashMap<>();
Map<String, Object> myFeatureSticky = new HashMap<>();
myFeatureSticky.put("enabled", true);
myFeatureSticky.put("variation", "treatment");
Map<String, Object> myVariables = new HashMap<>();
myVariables.put("myVariableKey", "myVariableValue");
myFeatureSticky.put("variables", myVariables);
stickyFeatures.put("myFeatureKey", myFeatureSticky);
Map<String, Object> anotherFeatureSticky = new HashMap<>();
anotherFeatureSticky.put("enabled", false);
stickyFeatures.put("anotherFeatureKey", anotherFeatureSticky);
Featurevisor f = Featurevisor.createFeaturevisor(new Featurevisor.FeaturevisorOptions()
.datafile(datafile)
.sticky(stickyFeatures));Once initialized with sticky features, the SDK will look for values there first before evaluating the targeting conditions and going through the bucketing process.
You can also set sticky features after the SDK is initialized:
Map<String, Object> stickyFeatures = new HashMap<>();
// ... build sticky features map
f.setSticky(stickyFeatures, true); // replace existing sticky featuresYou may also initialize the SDK without passing datafile, and set it later on:
f.setDatafile(datafileContent);By default, setDatafile(datafile) merges the incoming datafile with the SDK's stored datafile. Incoming top-level metadata is used, and incoming segments/features override existing segments/features with the same keys.
This means you can call setDatafile more than once with different datafiles, and the SDK instance accumulates their features and segments together. This is what makes loading datafiles on demand possible.
To replace the stored datafile entirely, pass true:
f.setDatafile(datafileContent, true);Because merging is the default, a single SDK instance can start with a small datafile and load more datafiles later as your application needs them, instead of downloading every feature upfront.
This pairs well with targets, where each target produces a smaller datafile for a specific part of your application:
Featurevisor f = Featurevisor.createFeaturevisor(new Featurevisor.FeaturevisorOptions());
void loadDatafile(String target) throws Exception {
String url = "https://cdn.yoursite.com/production/featurevisor-" + target + ".json";
String json = fetchJson(url); // use your HTTP client of choice
DatafileContent datafile = DatafileContent.fromJson(json);
// merges into whatever was loaded before
f.setDatafile(datafile);
}
loadDatafile("products");
// later, when the user reaches checkout
loadDatafile("checkout");You can set the datafile as many times as you want in your application, which will result in emitting a datafile_set event that you can listen and react to accordingly.
The triggers for setting the datafile again can be:
- periodic updates based on an interval (like every 5 minutes), or
- reacting to:
- a specific event in your application (like a user action), or
- an event served via websocket or server-sent events (SSE)
Here's an example of using interval-based update:
// Using ScheduledExecutorService for periodic updates
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
// Fetch new datafile content
String newDatafileContent = // ... fetch from your CDN
DatafileContent newDatafile = DatafileContent.fromJson(newDatafileContent);
// Merge into the SDK's existing datafile
f.setDatafile(newDatafile);
}, 0, 5, TimeUnit.MINUTES);By default, Featurevisor reports diagnostics to the console for info level and above with a [Featurevisor] prefix.
Available diagnostic levels are FATAL, ERROR, WARN, INFO, and DEBUG.
Set the level during initialization or update it afterwards:
Featurevisor f = Featurevisor.createFeaturevisor(
new Featurevisor.FeaturevisorOptions().logLevel(FeaturevisorLogLevel.DEBUG)
);
f.setLogLevel(FeaturevisorLogLevel.INFO);Use onDiagnostic to send structured diagnostics to your observability system:
Featurevisor f = Featurevisor.createFeaturevisor(new Featurevisor.FeaturevisorOptions()
.logLevel(FeaturevisorLogLevel.INFO)
.onDiagnostic(diagnostic -> {
System.out.println(diagnostic.getLevel() + ": " + diagnostic.getCode());
}));Every diagnostic has level, code, message, and an object-shaped details map. Optional module, moduleName, and originalError fields describe provenance. Evaluation metadata belongs in details.
Diagnostic handlers are isolated from SDK behavior. An exception in a handler does not stop other handlers or evaluations.
Featurevisor SDK implements a simple event emitter that allows you to listen to events that happen in the runtime.
You can listen to these events that can occur at various stages in your application:
Runnable unsubscribe = f.on("datafile_set", (event) -> {
String revision = (String) event.get("revision"); // new revision
String previousRevision = (String) event.get("previousRevision");
Boolean revisionChanged = (Boolean) event.get("revisionChanged"); // true if revision has changed
// list of feature keys that have new updates,
// and you should re-evaluate them
@SuppressWarnings("unchecked")
List<String> features = (List<String>) event.get("features");
// handle here
});
// stop listening to the event
unsubscribe.run();The features array will contain keys of features that have either been:
- added, or
- updated, or
- removed
compared to the previous datafile content that existed in the SDK instance.
Runnable unsubscribe = f.on("context_set", (event) -> {
Boolean replaced = (Boolean) event.get("replaced"); // true if context was replaced
@SuppressWarnings("unchecked")
Map<String, Object> context = (Map<String, Object>) event.get("context"); // the new context
System.out.println("Context set");
});Runnable unsubscribe = f.on("sticky_set", (event) -> {
Boolean replaced = (Boolean) event.get("replaced"); // true if sticky features got replaced
@SuppressWarnings("unchecked")
List<String> features = (List<String>) event.get("features"); // list of all affected feature keys
System.out.println("Sticky features set");
});Emitter.UnsubscribeFunction unsubscribe = f.on(Emitter.EventName.ERROR, (event) -> {
FeaturevisorDiagnostic diagnostic = (FeaturevisorDiagnostic) event.get("diagnostic");
System.err.println(diagnostic.getMessage());
});The error event is emitted for diagnostics whose level is ERROR.
Besides logging with debug level enabled, you can also get more details about how the feature variations and variables are evaluated in the runtime against given context:
// flag
Map<String, Object> evaluation = f.evaluateFlag(featureKey, context);
// variation
Map<String, Object> evaluation = f.evaluateVariation(featureKey, context);
// variable
Map<String, Object> evaluation = f.evaluateVariable(featureKey, variableKey, context);The returned object will always contain the following properties:
featureKey: the feature keyreason: the reason how the value was evaluated
And optionally these properties depending on whether you are evaluating a feature variation or a variable:
bucketValue: the bucket value between 0 and 100,000ruleKey: the rule keyerror: the error objectenabled: if feature itself is enabled or notvariation: the variation objectvariationValue: the variation valuevariableKey: the variable keyvariableValue: the variable valuevariableSchema: the variable schema
Modules allow you to intercept the evaluation process and customize it further as per your needs.
A module is a FeaturevisorModule with a unique name and optional lifecycle functions:
If setup throws, the module is not registered. Featurevisor removes subscriptions created during setup, reports module_setup_error, and calls close when present.
FeaturevisorModule myCustomModule = new FeaturevisorModule("my-custom-module")
.setup(api -> {
System.out.println("Current revision: " + api.getRevision());
})
// before evaluation
.before(options -> {
Map<String, Object> context = new HashMap<>(options.getContext());
context.put("someAdditionalAttribute", "value");
return options.copy().context(context);
})
// configure bucket key
.bucketKey(options -> {
String bucketKey = options.getBucketKey();
return bucketKey;
})
// configure bucket value (between 0 and 100,000)
.bucketValue(options -> {
int bucketValue = options.getBucketValue();
return bucketValue;
})
// after evaluation
.after((evaluation, options) -> evaluation)
// called by f.close()
.close(() -> {
// clean up resources
});You can register modules at the time of SDK initialization:
List<FeaturevisorModule> modules = new ArrayList<>();
modules.add(myCustomModule);
Featurevisor f = Featurevisor.createFeaturevisor(new Featurevisor.FeaturevisorOptions()
.datafile(datafile)
.modules(modules));Or after initialization:
Runnable removeModule = f.addModule(myCustomModule);
// removeModule.run();
// or:
f.removeModule("my-custom-module");Modules receive an API during setup and can subscribe to diagnostics or report their own:
FeaturevisorModule module = new FeaturevisorModule("diagnostic-module")
.setup(api -> {
Runnable unsubscribe = api.onDiagnostic(diagnostic -> {
// observe diagnostics from the SDK and other modules
});
api.reportDiagnostic(new FeaturevisorDiagnostic()
.level(FeaturevisorLogLevel.WARN)
.code("custom_module_warning")
.message("Something notable happened"));
});When dealing with purely client-side applications, it is understandable that there is only one user involved, like in browser or mobile applications.
But when using Featurevisor SDK in server-side applications, where a single server instance can handle multiple user requests simultaneously, it is important to isolate the context for each request.
That's where child instances come in handy:
Map<String, Object> childContext = new HashMap<>();
childContext.put("userId", "123");
ChildInstance childF = f.spawn(childContext);Now you can pass the child instance where your individual request is being handled, and you can continue to evaluate features targeting that specific user alone:
boolean isEnabled = childF.isEnabled("my_feature");
String variation = childF.getVariation("my_feature");
String variableValue = childF.getVariableString("my_feature", "my_variable");Similar to parent SDK, child instances also support several additional methods:
setContextsetStickyisEnabledgetVariationgetVariablegetVariableBooleangetVariableStringgetVariableIntegergetVariableDoublegetVariableArraygetVariableObjectgetVariableJSONgetVariableJSONNodegetAllEvaluationsonclose
Both primary and child instances support a .close() method, that removes forgotten event listeners (via on method) and cleans up any potential memory leaks.
f.close();This package also provides a CLI tool for running your Featurevisor project's test specs and benchmarking against this Java SDK:
All three commands accept repeatable --target=<target> options. test builds only the selected Target datafiles and runs untargeted assertions plus assertions for those targets. benchmark and assess-distribution run independently against every selected Target datafile. Without --target, existing project-wide behavior is preserved. Project definitions, test specs, Target discovery, and datafile generation continue to come from the Node.js CLI.
Learn more about testing here.
$ mvn exec:java -Dexec.mainClass="com.featurevisor.cli.CLI" -Dexec.args="test --projectDirectoryPath=/absolute/path/to/your/featurevisor/project"Additional options that are available:
$ mvn exec:java -Dexec.mainClass="com.featurevisor.cli.CLI" -Dexec.args="test --projectDirectoryPath=/absolute/path/to/your/featurevisor/project --quiet --onlyFailures --keyPattern=myFeatureKey --assertionPattern=#1 --showDatafile --inflate=1"The test runner builds base datafiles and Target datafiles, then uses a Target datafile when an assertion contains target.
Learn more about benchmarking here.
$ mvn exec:java -Dexec.mainClass="com.featurevisor.cli.CLI" -Dexec.args="benchmark --projectDirectoryPath=/absolute/path/to/your/featurevisor/project --environment=production --feature=myFeatureKey --context='{\"country\": \"nl\"}' --n=1000"Learn more about assessing distribution here.
$ mvn exec:java -Dexec.mainClass="com.featurevisor.cli.CLI" -Dexec.args="assess-distribution --projectDirectoryPath=/absolute/path/to/your/featurevisor/project --environment=production --feature=foo --variation --context='{\"country\": \"nl\"}' --populateUuid=userId --populateUuid=deviceId --n=1000"Clone the repository, and install the dependencies using Maven:
$ mvn install$ mvn test- Manually create a new release on GitHub
- Tag it with a prefix of
v, likev1.0.0 - GitHub Actions is set up to automatically publish the package to GitHub Packages
MIT © Fahad Heylaal