Java
Install the latest version
<dependency>
<groupId>com.reforge</groupId>
<artifactId>sdk</artifactId>
<version>LATEST</version>
</dependency>
Dependency-Reduced Version
There's an optional uber-jar with shaded and relocated guava and failsafe dependencies
<dependency>
<groupId>com.reforge</groupId>
<artifactId>client</artifactId>
<version>0.3.20</version>
<classifier>uberjar</classifier>
</dependency>
Initialize the client
Basic Usage
import com.reforge.sdk.Options;
import com.reforge.sdk.Sdk;
final Sdk reforge = new Sdk(new Options());
Typical Usage
We recommend using the Sdk
as a singleton in your application. This is the most common way to use the SDK.
import com.reforge.sdk.Options;
import com.reforge.sdk.Sdk;
// Micronaut Factory
@Factory
public class ReforgeFactory {
@Singleton
public Sdk reforge() {
return new Sdk(new Options());
}
@Singleton
public FeatureFlagClient featureFlagClient(Sdk reforge) {
return reforge.featureFlagClient();
}
@Singleton
public ConfigClient configClient(Sdk reforge) {
return reforge.configClient();
}
}
Feature Flags
For boolean flags, you can use the featureIsOn
convenience method:
public class MyClass {
// assumes you have setup a singleton
@Inject
private FeatureFlagClient featureFlagClient;
public String test(String key){
boolean val = featureFlagClient.featureIsOn(key);
return "Feature flag value of %s is %s".formatted(key, val);
}
}
Feature flags don't have to return just true or false. You can get other data types using get
:
public class MyClass {
// assumes you have setup a singleton
@Inject
private FeatureFlagClient featureFlagClient;
public String test(String key){
Optional<Prefab.ConfigValue> val = featureFlagClient.get(key);
return "Feature flag value of %s is %s".formatted(key, val.orElse("no value found"));
}
}
Context
To finely-target configuration rule evaluation, we accept contextual information globally, request-scoped (thread-locally) with the ContextStore which will affect all featureflag and config lookups.
Global Context
Use global context for information that doesn't change - for example, your application's key, availability-zone etc. Set it in the client's options as below
import com.reforge.sdk.Options;
import com.reforge.sdk.Sdk;
Context deploymentContext = Context
.newBuilder("application")
.put("key", "my-api")
.put("az", "1a")
.put("type", "web")
.build();
Options options = new Options()
.setGlobalContext(ContextSet.from(deploymentContext));
final Sdk reforge = new Sdk(options);
Thread-local (Request-scoped)
// set the thread-local context
reforge.configClient().getContextStore().addContext(
Context.newBuilder("User")
.put("name", user.getName())
.put("key", user.getKey())
.build());
// or using an autoclosable scope helper
// this will replace any-existing threadlocal context until the try-with-resources block exits
ContextHelper prefabContextHelper = new ContextHelper(
reforge.configClient()
);
try (
ContextHelper.ContextScope ignored = prefabContextHelper.performWorkWithAutoClosingContext(
Context.newBuilder("User")
.put("name", user.getName())
.put("key", user.getKey())
.build());
) {
// do config/flag operations
}
When thread-local context is set, log levels and feature flags will evaluate in that context. Here are details on setting thread-local context:
- Micronaut
- Dropwizard
Add a filter to add a Reforge context based on the currently "logged in" user.
@Filter(Filter.MATCH_ALL_PATTERN)
public class ContextFilter implements HttpFilter {
private final ConfigClient configClient;
@Inject
ContextFilter(ConfigClient configClient) {
this.configClient = configClient;
}
@Override
public Publisher<? extends HttpResponse<?>> doFilter(HttpRequest<?> request, FilterChain chain) {
request.getUserPrincipal(Authentication.class).ifPresent(authentication ->
{
User user = (User) authentication.getAttributes().get(ExampleAuthenticationProvider.USER_ATTR);
configClient.getContextStore()
.addContext(
Context.newBuilder("user")
.put("id", user.id())
.put("country", user.country())
.put("email", user.email())
.build()
);
}
);
return chain.proceed(request);
}
@Override
public int getOrder() {
return ServerFilterPhase.SECURITY.after() + 1;
// run after the DefaultLoginFilter
}
}
Reforge Context uses ThreadLocals by default. In event-based frameworks like micronaut, that won't work so configure the Reforge Context store to use ServerRequestContextStore
instead.
options.setContextStore(new ServerRequestContextStore());
Learn more with the Reforge + Micronaut example app
Use a ContainerRequestFilter
to set the context for your request when the request begins
public class ContextAddingRequestFilter implements ContainerRequestFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(ContextAddingRequestFilter.class);
private final ConfigClient configClient;
@Inject
public ContextAddingRequestFilter(ConfigClient configClient) {
this.configClient = configClient;
}
@Override
public void filter(ContainerRequestContext containerRequestContext) throws IOException {
final SecurityContext securityContext =
containerRequestContext.getSecurityContext();
if (securityContext != null) {
Principal principal = securityContext.getUserPrincipal();
if (principal instanceof User) {
User user = (User) principal;
LOGGER.info("will add pf context for {}", user);
configClient.getContextStore().addContext(
Context.newBuilder("User")
.put("name", user.getName())
.build());
}
}
}
}
Then we'll add another ContainerResponseFilter
to clear the context from the ThreadLocal when the request finishes.
public class PrefabContexClearingResponseFilter implements ContainerResponseFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(PrefabContexClearingResponseFilter.class);
private final ConfigClient configClient;
@Inject
PrefabContexClearingResponseFilter(ConfigClient configClient) {
this.configClient = configClient;
}
@Override
public void filter(ContainerRequestContext containerRequestContext, ContainerResponseContext containerResponseContext) throws IOException {
configClient.getContextStore().clearContexts();
LOGGER.info("Cleared context");
}
}
Learn more with the Reforge + Dropwizard example app
Just-in-time Context
You can also provide context information inline when making a get request. If you provide just-in-time context to your FF or config evaluations, it will be merged with the global context.
featureFlagClient.featureIsOn(
"features.example-flag",
Context.newBuilder("customer")
.put("group", "beta")
.build()
)
reforge.configClient().get("the.key",
Context.newBuilder("user")
.put("name", "james")
.put("tier", "gold")
.put("customerMonths", 12)
.build()
)
See contexts for more information
Dynamic Config
final Optional<Prefab.ConfigValue> configValue = reforge.configClient().get("the.key");
if(configValue.isPresent()){
System.out.println(configValue.get().getString());
}
Live Values
Live values are a convenient and clear way to use configuration throughout your system. Inject a Reforge client and get live values for the configuration keys you need.
In code, .get()
will return the value. These values will update automatically when the configuration is updated in Reforge Launch.
import java.util.function.Supplier;
class MyClass {
private Supplier<String> sampleString;
private Supplier<Long> sampleLong;
@Inject
public MyClass(ConfigClient configClient) {
this.sampleString = configClient.liveString("sample.string");
this.sampleLong = configClient.liveLong("sample.long");
}
public String test(){
return "I got %s and %d from Reforge Launch.".formatted(sampleString.get(), sampleLong.get());
}
}
Telemetry
By default, Reforge uploads telemetry that enables a number of useful features. You can alter or disable this behavior using the following options:
Name | Description | Default |
---|---|---|
collectEvaluationSummaries | Send counts of config/flag evaluation results back to Reforge to view in web app | true |
collectLoggerCounts | Send counts of logger usage back to Reforge to power log-levels configuration screen | true |
contextUploadMode | Upload either context "shapes" (the names and data types your app uses in Reforge contexts) or periodically send full example contexts | PERIODIC_EXAMPLE |
If you want to change any of these options, you can pass an options
object when initializing the Reforge client.
Options options = new Options()
.setCollectEvaluationSummaries(true)
.setCollectLoggerCounts(true)
.setContextUploadMode(Options.CollectContextMode.PERIODIC_EXAMPLE);
Testing
Reforge suggests testing with generous usage of Mockito. We also provide a useful FixedValue
for testing Live Values.
@Test
void testReforge(){
ConfigClient mockConfigClient = mock(ConfigClient.class);
when(mockConfigClient.liveString("sample.string")).thenReturn(FixedValue.of("test value"));
when(mockConfigClient.liveLong("sample.long")).thenReturn(FixedValue.of(123L));
MyClass myClass = new MyClass(mockConfigClient);
// test business logic
}
Reference
Options
Options options = new Options()
.setConfigOverrideDir(System.getProperty("user.home"))
.setSdkKey(System.getenv("REFORGE_BACKEND_SDK_KEY"))
.setPrefabDatasource(Options.Datasources.ALL) // Option: Datasources.LOCAL_ONLY
.setOnInitializationFailure(Options.OnInitializationFailure.RAISE) // Option Options.OnInitializationFailure.UNLOCK
.setInitializationTimeoutSec(10)
.setGlobalContext(ContextSet.from(Context
.newBuilder("application")
.put("key", "my-api")
.put("az", "1a")
.put("type", "web")
.build())
);
Option Definitions
Name | Description | Default |
---|---|---|
collectEvaluationSummaries | Send counts of config/flag evaluation results back to Reforge to view in web app | true |
collectLoggerCounts | Send counts of logger usage back to Reforge to power log-levels configuration screen | true |
contextUploadMode | Upload either context "shapes" (the names and data types your app uses in Reforge contexts) or periodically send full example contexts | PERIODIC_EXAMPLE |
onInitializationFailure | Choose to crash or continue with local data only if unable to fetch config data from Reforge at startup | RAISE (crash) |
prefabDatasources | Use either only-local data or local + API data | ALL |
globalContext | set a static context to be used as the base layer in all configuration evaluation | EMPTY |