example feature flag implementations
This commit is contained in:
@@ -21,10 +21,18 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-jdbc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- TOGGLZ -->
|
||||
<dependency>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package io.reflectoring.featureflags.implementations;
|
||||
|
||||
public interface FeatureFlagService {
|
||||
|
||||
Boolean featureOne();
|
||||
|
||||
Integer featureTwo();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package io.reflectoring.featureflags.implementations.code;
|
||||
|
||||
import io.reflectoring.featureflags.implementations.FeatureFlagService;
|
||||
|
||||
public class CodeBackedFeatureFlagService implements FeatureFlagService {
|
||||
@Override
|
||||
public Boolean featureOne() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer featureTwo() {
|
||||
return 42;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package io.reflectoring.featureflags.implementations.contextsensitive;
|
||||
|
||||
import io.reflectoring.featureflags.implementations.FeatureFlagService;
|
||||
import io.reflectoring.featureflags.implementations.contextsensitive.Feature.RolloutStrategy;
|
||||
import io.reflectoring.featureflags.web.UserSession;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
public class ContextSensitiveFeatureFlagService implements FeatureFlagService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final UserSession userSession;
|
||||
|
||||
public ContextSensitiveFeatureFlagService(JdbcTemplate jdbcTemplate, UserSession userSession) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
this.userSession = userSession;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean featureOne() {
|
||||
Feature feature = getFeatureFromDatabase();
|
||||
if (feature == null) {
|
||||
return Boolean.FALSE;
|
||||
}
|
||||
return feature.evaluateBoolean(userSession.getUsername());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer featureTwo() {
|
||||
Feature feature = getFeatureFromDatabase();
|
||||
if (feature == null) {
|
||||
return null;
|
||||
}
|
||||
return feature.evaluateInt(userSession.getUsername());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Feature getFeatureFromDatabase() {
|
||||
return jdbcTemplate.query("select targeting, value, defaultValue, percentage from features where feature_key='FEATURE_ONE'", resultSet -> {
|
||||
if (!resultSet.next()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
RolloutStrategy rolloutStrategy = Enum.valueOf(RolloutStrategy.class, resultSet.getString(1));
|
||||
String value = resultSet.getString(2);
|
||||
String defaultValue = resultSet.getString(3);
|
||||
int percentage = resultSet.getInt(4);
|
||||
|
||||
return new Feature(rolloutStrategy, value, defaultValue, percentage);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package io.reflectoring.featureflags.implementations.contextsensitive;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class Feature {
|
||||
|
||||
public enum RolloutStrategy {
|
||||
GLOBAL,
|
||||
PERCENTAGE;
|
||||
}
|
||||
|
||||
private final RolloutStrategy rolloutStrategy;
|
||||
|
||||
private final int percentage;
|
||||
private final String value;
|
||||
private final String defaultValue;
|
||||
|
||||
public Feature(RolloutStrategy rolloutStrategy, String value, String defaultValue, int percentage) {
|
||||
this.rolloutStrategy = rolloutStrategy;
|
||||
this.percentage = percentage;
|
||||
this.value = value;
|
||||
this.defaultValue = defaultValue;
|
||||
}
|
||||
|
||||
public boolean evaluateBoolean(String userId) {
|
||||
switch (this.rolloutStrategy) {
|
||||
case GLOBAL:
|
||||
return this.getBooleanValue();
|
||||
case PERCENTAGE:
|
||||
if (percentageHashCode(userId) <= this.percentage) {
|
||||
return this.getBooleanValue();
|
||||
} else {
|
||||
return this.getBooleanDefaultValue();
|
||||
}
|
||||
}
|
||||
|
||||
return this.getBooleanDefaultValue();
|
||||
}
|
||||
|
||||
public Integer evaluateInt(String userId) {
|
||||
switch (this.rolloutStrategy) {
|
||||
case GLOBAL:
|
||||
return this.getIntValue();
|
||||
case PERCENTAGE:
|
||||
if (percentageHashCode(userId) <= this.percentage) {
|
||||
return this.getIntValue();
|
||||
} else {
|
||||
return this.getIntDefaultValue();
|
||||
}
|
||||
}
|
||||
|
||||
return this.getIntDefaultValue();
|
||||
}
|
||||
|
||||
double percentageHashCode(String text) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] encodedhash = digest.digest(
|
||||
text.getBytes(StandardCharsets.UTF_8));
|
||||
double INTEGER_RANGE = 1L << 32;
|
||||
return (((long) Arrays.hashCode(encodedhash) - Integer.MIN_VALUE) / INTEGER_RANGE) * 100;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public RolloutStrategy getTargeting() {
|
||||
return rolloutStrategy;
|
||||
}
|
||||
|
||||
public int getPercentage() {
|
||||
return percentage;
|
||||
}
|
||||
|
||||
public int getIntValue() {
|
||||
return Integer.parseInt(this.value);
|
||||
}
|
||||
|
||||
public int getIntDefaultValue() {
|
||||
return Integer.parseInt(this.defaultValue);
|
||||
}
|
||||
|
||||
|
||||
public boolean getBooleanValue() {
|
||||
return Boolean.parseBoolean(this.value);
|
||||
}
|
||||
|
||||
public boolean getBooleanDefaultValue() {
|
||||
return Boolean.parseBoolean(this.defaultValue);
|
||||
}
|
||||
|
||||
public String getStringValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public String getDefaultValue() {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package io.reflectoring.featureflags.implementations.database;
|
||||
|
||||
import io.reflectoring.featureflags.implementations.FeatureFlagService;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
public class DatabaseBackedFeatureFlagService implements FeatureFlagService {
|
||||
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Override
|
||||
public Boolean featureOne() {
|
||||
return jdbcTemplate.query("select value from features where feature_key='FEATURE_ONE'", resultSet -> {
|
||||
if(!resultSet.next()){
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean value = Boolean.parseBoolean(resultSet.getString(1));
|
||||
return value ? Boolean.TRUE : Boolean.FALSE;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer featureTwo() {
|
||||
return jdbcTemplate.query("select value from features where feature_key='FEATURE_TWO'", resultSet -> {
|
||||
if(!resultSet.next()){
|
||||
return null;
|
||||
}
|
||||
|
||||
return Integer.valueOf(resultSet.getString(1));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package io.reflectoring.featureflags.implementations.launchdarkly;
|
||||
|
||||
import com.launchdarkly.sdk.LDUser;
|
||||
import com.launchdarkly.sdk.server.LDClient;
|
||||
import io.reflectoring.featureflags.implementations.FeatureFlagService;
|
||||
import io.reflectoring.featureflags.web.UserSession;
|
||||
|
||||
public class LaunchDarklyFeatureFlagService implements FeatureFlagService {
|
||||
|
||||
private final LDClient launchdarklyClient;
|
||||
private final UserSession userSession;
|
||||
|
||||
public LaunchDarklyFeatureFlagService(LDClient launchdarklyClient, UserSession userSession) {
|
||||
this.launchdarklyClient = launchdarklyClient;
|
||||
this.userSession = userSession;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean featureOne() {
|
||||
return launchdarklyClient.boolVariation("feature-one", getLaunchdarklyUserFromSession(), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer featureTwo() {
|
||||
return launchdarklyClient.intVariation("feature-two", getLaunchdarklyUserFromSession(), 0);
|
||||
}
|
||||
|
||||
private LDUser getLaunchdarklyUserFromSession() {
|
||||
return new LDUser.Builder(userSession.getUsername())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package io.reflectoring.featureflags.implementations.properties;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@ConfigurationProperties("features")
|
||||
public class FeatureProperties {
|
||||
|
||||
private boolean featureOne;
|
||||
private int featureTwo;
|
||||
|
||||
public FeatureProperties() {
|
||||
}
|
||||
|
||||
public boolean getFeatureOne() {
|
||||
return featureOne;
|
||||
}
|
||||
|
||||
public void setFeatureOne(boolean featureOne) {
|
||||
this.featureOne = featureOne;
|
||||
}
|
||||
|
||||
public int getFeatureTwo() {
|
||||
return featureTwo;
|
||||
}
|
||||
|
||||
public void setFeatureTwo(int featureTwo) {
|
||||
this.featureTwo = featureTwo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package io.reflectoring.featureflags.implementations.properties;
|
||||
|
||||
|
||||
import io.reflectoring.featureflags.implementations.FeatureFlagService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class PropertiesBackedFeatureFlagService implements FeatureFlagService {
|
||||
|
||||
private final FeatureProperties featureProperties;
|
||||
|
||||
public PropertiesBackedFeatureFlagService(FeatureProperties featureProperties) {
|
||||
this.featureProperties = featureProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean featureOne() {
|
||||
return featureProperties.getFeatureOne();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer featureTwo() {
|
||||
return featureProperties.getFeatureTwo();
|
||||
}
|
||||
}
|
||||
@@ -15,4 +15,8 @@ togglz:
|
||||
enabled: true
|
||||
secured: false
|
||||
path: /togglz
|
||||
use-management-port: false
|
||||
use-management-port: false
|
||||
|
||||
features:
|
||||
featureOne: true
|
||||
featureTwo: 42
|
||||
@@ -0,0 +1,19 @@
|
||||
package io.reflectoring.featureflags.implementations.contextsensitive;
|
||||
|
||||
import io.reflectoring.featureflags.implementations.contextsensitive.Feature.RolloutStrategy;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.offset;
|
||||
|
||||
public class FeatureTest {
|
||||
|
||||
@Test
|
||||
void testHashCode(){
|
||||
Feature feature = new Feature(RolloutStrategy.PERCENTAGE, "true", "false", 50);
|
||||
assertThat(feature.percentageHashCode("1")).isCloseTo(27.74d, offset(0.01d));
|
||||
assertThat(feature.percentageHashCode("2")).isCloseTo(81.12d, offset(0.01d));
|
||||
assertThat(feature.percentageHashCode("3")).isCloseTo(21.69d, offset(0.01d));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user