Skip to main content

Java

Install the latest version

Github | Maven Repository

<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>sdk</artifactId>
<version>LATEST</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 typed methods:

public class MyClass {
// assumes you have setup a singleton
@Inject
private FeatureFlagClient featureFlagClient;

public String test(String key){
String val = featureFlagClient.getString(key, "default value");
return "Feature flag value of %s is %s".formatted(key, val);
}
}

Available typed methods: getString(), getLong(), getDouble(), getBoolean(), getStringList(), getJSON(), and getDuration().

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:

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

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()
)

String value = reforge.configClient().getString(
"the.key",
"default-value",
Context.newBuilder("user")
.put("name", "james")
.put("tier", "gold")
.put("customerMonths", 12)
.build()
)

See contexts for more information

Dynamic Config

Use typed methods to retrieve configuration values:

// Get a string config value with a default
String value = reforge.configClient().getString("the.key", "default-value");
System.out.println(value);

// Get a long config value with a default
long count = reforge.configClient().getLong("max.count", 100L);

Available typed methods: getString(), getLong(), getDouble(), getBoolean(), getStringList(), getJSON(), and getDuration().

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());
}
}

Dynamic Log Levels

The Reforge SDK provides seamless integration with popular Java logging frameworks, enabling you to dynamically manage log levels across your application in real-time without restarting.

Features

  • Centrally manage log levels - Control logging across your entire application from the Reforge dashboard
  • Real-time updates - Change log levels without restarting your application
  • Context-aware logging - Different log levels for different loggers based on runtime context
  • Performance - Efficient filtering happens before log message construction
  • Framework support - Works with Logback and Log4j2

Logback Integration

Add the Logback integration dependency:

<dependency>
<groupId>com.reforge</groupId>
<artifactId>sdk-logback</artifactId>
<version>LATEST</version>
</dependency>

Install the filter during application startup:

import com.reforge.sdk.Sdk;
import com.reforge.sdk.Options;
import com.reforge.sdk.logback.ReforgeLogbackTurboFilter;

public class MyApplication {
public static void main(String[] args) {
// Initialize the Reforge SDK
Sdk sdk = new Sdk(new Options());

// Install the Logback turbo filter
ReforgeLogbackTurboFilter.install(sdk.loggerClient());

// Now all your logging will respect Reforge log levels
}
}

Compatibility: Logback 1.2.x, 1.3.x, 1.4.x, and 1.5.x

Log4j2 Integration

Add the Log4j2 integration dependency:

<dependency>
<groupId>com.reforge</groupId>
<artifactId>sdk-log4j2</artifactId>
<version>LATEST</version>
</dependency>

Install the filter during application startup:

import com.reforge.sdk.Sdk;
import com.reforge.sdk.Options;
import com.reforge.sdk.log4j2.ReforgeLog4j2Filter;

public class MyApplication {
public static void main(String[] args) {
// Initialize the Reforge SDK
Sdk sdk = new Sdk(new Options());

// Install the Log4j2 filter
ReforgeLog4j2Filter.install(sdk.loggerClient());

// Now all your logging will respect Reforge log levels
}
}

Compatibility: Log4j2 2.x

Configuration

Create a LOG_LEVEL_V2 config in your Reforge dashboard with key log-levels.default:

# Default to INFO for all loggers
default: INFO

# Set specific packages to DEBUG
rules:
- criteria:
reforge-sdk-logging.logger-path:
starts-with: "com.example.services"
value: DEBUG

# Only log errors in noisy third-party library
- criteria:
reforge-sdk-logging.logger-path:
starts-with: "com.thirdparty.noisy"
value: ERROR

You can customize the config key name using the setLoggerKey option. This is useful if you have multiple applications sharing the same Reforge project and want to isolate log level configuration per application:

Options options = new Options().setLoggerKey("myapp.log.levels");
Sdk sdk = new Sdk(options);

The SDK automatically includes lang: "java" in the evaluation context, which you can use in your rules to create Java-specific log level configurations:

# Different log levels for Java vs other languages
rules:
- criteria:
reforge-sdk-logging.lang: java
reforge-sdk-logging.logger-path:
starts-with: "com.example"
value: DEBUG

- criteria:
reforge-sdk-logging.lang: python
reforge-sdk-logging.logger-path:
starts-with: "myapp"
value: INFO

Targeted Log Levels

You can use rules and segmentation to change your log levels based on the current user/request/device context. This allows you to increase log verbosity for specific users, environments, or conditions without affecting your entire application.

The log level evaluation has access to all context that is available during evaluation, not just the reforge-sdk-logging context. This means you can create rules combining:

  • SDK logging context (reforge-sdk-logging.*) - Logger name and language
  • Global context - Application name, environment, availability zone, etc.
  • Thread-local context - User, team, device, request information from request-scoped context

For example, you can create rules like:

# Enable DEBUG logs only for specific application in staging
rules:
- criteria:
application.key: "myapp"
application.environment: "staging"
reforge-sdk-logging.logger-path:
starts-with: "com.example"
value: DEBUG

# Enable DEBUG logs for a specific user across all applications
- criteria:
user.email: "developer@example.com"
value: DEBUG

# Lower verbosity in production
- criteria:
application.environment: "production"
value: WARN

This allows you to increase log verbosity for specific users, specific applications, particular environments, or any combination of conditions without affecting your entire system.

How It Works

Once installed, the integration automatically intercepts all logging calls across your entire application:

  • Works with all loggers (no need to configure individual loggers)
  • Works with all appenders (console, file, syslog, etc.)
  • Filters happen before log messages are formatted (performance benefit)
  • No modification of your existing logging configuration needed

For more details, see:

Telemetry

By default, Reforge uploads telemetry that enables a number of useful features. You can alter or disable this behavior using the following options:

NameDescriptionDefault
collectEvaluationSummariesSend counts of config/flag evaluation results back to Reforge to view in web apptrue
collectLoggerCountsSend counts of logger usage back to Reforge to power log-levels configuration screentrue
contextUploadModeUpload either context "shapes" (the names and data types your app uses in Reforge contexts) or periodically send full example contextsPERIODIC_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

NameDescriptionDefault
collectEvaluationSummariesSend counts of config/flag evaluation results back to Reforge to view in web apptrue
collectLoggerCountsSend counts of logger usage back to Reforge to power log-levels configuration screentrue
contextUploadModeUpload either context "shapes" (the names and data types your app uses in Reforge contexts) or periodically send full example contextsPERIODIC_EXAMPLE
onInitializationFailureChoose to crash or continue with local data only if unable to fetch config data from Reforge at startupRAISE (crash)
prefabDatasourcesUse either only-local data or local + API dataALL
globalContextset a static context to be used as the base layer in all configuration evaluationEMPTY