Merge branch 'master' into http-clients

This commit is contained in:
Pratik Das
2021-11-05 17:21:26 +05:30
committed by GitHub
79 changed files with 3907 additions and 1 deletions

View File

@@ -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>

View File

@@ -22,4 +22,6 @@ public interface FeatureFlagService {
*/
Boolean isUserActionTargetedFeatureActive();
Boolean isNewServiceEnabled();
}

View File

@@ -33,4 +33,9 @@ public class FF4JFeatureFlagService implements FeatureFlagService {
return null;
}
@Override
public Boolean isNewServiceEnabled() {
return null;
}
}

View File

@@ -0,0 +1,9 @@
package io.reflectoring.featureflags.implementations;
public interface FeatureFlagService {
Boolean featureOne();
Integer featureTwo();
}

View File

@@ -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;
}
}

View File

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

View File

@@ -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;
}
}

View File

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

View File

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

View File

@@ -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;
}
}

View File

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

View File

@@ -4,11 +4,13 @@ import com.launchdarkly.sdk.LDUser;
import com.launchdarkly.sdk.server.LDClient;
import io.reflectoring.featureflags.FeatureFlagService;
import io.reflectoring.featureflags.web.UserSession;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Component("launchdarkly")
@Primary
public class LaunchDarklyFeatureFlagService implements FeatureFlagService {
private final LDClient launchdarklyClient;
@@ -56,6 +58,11 @@ public class LaunchDarklyFeatureFlagService implements FeatureFlagService {
return launchdarklyClient.boolVariation("user-clicked-flag", getLaunchdarklyUserFromSession(), false);
}
@Override
public Boolean isNewServiceEnabled() {
return true;
}
private LDUser getLaunchdarklyUserFromSession() {
return new LDUser.Builder(userSession.getUsername())
.custom("clicked", userSession.hasClicked())

View File

@@ -0,0 +1,22 @@
package io.reflectoring.featureflags.patterns.ifelse;
import io.reflectoring.featureflags.FeatureFlagService;
import org.springframework.stereotype.Component;
@Component
class Service {
private final FeatureFlagService featureFlagService;
public Service(FeatureFlagService featureFlagService) {
this.featureFlagService = featureFlagService;
}
public int doSomething() {
if (featureFlagService.isNewServiceEnabled()) {
return 42;
} else {
return 1;
}
}
}

View File

@@ -0,0 +1,42 @@
package io.reflectoring.featureflags.patterns.replacebean;
import org.springframework.beans.factory.FactoryBean;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.function.Supplier;
public class FeatureFlagFactoryBean<T> implements FactoryBean<T> {
private final Class<T> targetClass;
private final Supplier<Boolean> featureFlagEvaluation;
private final T beanWhenTrue;
private final T beanWhenFalse;
public FeatureFlagFactoryBean(Class<T> targetClass, Supplier<Boolean> featureFlagEvaluation, T beanWhenTrue, T beanWhenFalse) {
this.targetClass = targetClass;
this.featureFlagEvaluation = featureFlagEvaluation;
this.beanWhenTrue = beanWhenTrue;
this.beanWhenFalse = beanWhenFalse;
}
@Override
public T getObject() {
InvocationHandler invocationHandler = (proxy, method, args) -> {
if (featureFlagEvaluation.get()) {
return method.invoke(beanWhenTrue, args);
} else {
return method.invoke(beanWhenFalse, args);
}
};
Object proxy = Proxy.newProxyInstance(targetClass.getClassLoader(), new Class[]{targetClass}, invocationHandler);
return (T) proxy;
}
@Override
public Class<?> getObjectType() {
return targetClass;
}
}

View File

@@ -0,0 +1,16 @@
package io.reflectoring.featureflags.patterns.replacebean;
import io.reflectoring.featureflags.FeatureFlagService;
import org.springframework.stereotype.Component;
@Component("replaceBeanFeatureFlaggedService")
class FeatureFlaggedService extends FeatureFlagFactoryBean<Service> {
public FeatureFlaggedService(FeatureFlagService featureFlagService) {
super(
Service.class,
featureFlagService::isNewServiceEnabled,
new NewService(),
new OldService());
}
}

View File

@@ -0,0 +1,8 @@
package io.reflectoring.featureflags.patterns.replacebean;
class NewService implements Service {
@Override
public int doSomething() {
return 42;
}
}

View File

@@ -0,0 +1,8 @@
package io.reflectoring.featureflags.patterns.replacebean;
class OldService implements Service {
@Override
public int doSomething() {
return 1;
}
}

View File

@@ -0,0 +1,7 @@
package io.reflectoring.featureflags.patterns.replacebean;
interface Service {
int doSomething();
}

View File

@@ -0,0 +1,30 @@
package io.reflectoring.featureflags.patterns.replacemethod;
import io.reflectoring.featureflags.FeatureFlagService;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
@Component("replaceMethodFeatureFlaggedService")
@Primary
class FeatureFlaggedService implements Service {
private final FeatureFlagService featureFlagService;
private final NewService newService;
private final OldService oldService;
public FeatureFlaggedService(FeatureFlagService featureFlagService, NewService newService, OldService oldService) {
this.featureFlagService = featureFlagService;
this.newService = newService;
this.oldService = oldService;
}
@Override
public int doSomething() {
if (featureFlagService.isNewServiceEnabled()) {
return newService.doSomething();
} else {
return oldService.doSomething();
}
}
}

View File

@@ -0,0 +1,12 @@
package io.reflectoring.featureflags.patterns.replacemethod;
import org.springframework.stereotype.Component;
@Component
class NewService implements Service {
@Override
public int doSomething() {
return 42;
}
}

View File

@@ -0,0 +1,16 @@
package io.reflectoring.featureflags.patterns.replacemethod;
import org.springframework.stereotype.Component;
@Component
class OldService implements Service {
@Override
public int doSomething() {
return 1;
}
public int doSomethingElse(){
return 2;
}
}

View File

@@ -0,0 +1,7 @@
package io.reflectoring.featureflags.patterns.replacemethod;
interface Service {
int doSomething();
}

View File

@@ -0,0 +1,31 @@
package io.reflectoring.featureflags.patterns.replacemodule;
import io.reflectoring.featureflags.FeatureFlagService;
import io.reflectoring.featureflags.patterns.replacebean.FeatureFlagFactoryBean;
import io.reflectoring.featureflags.patterns.replacemodule.newmodule.NewService1;
import io.reflectoring.featureflags.patterns.replacemodule.newmodule.NewService2;
import io.reflectoring.featureflags.patterns.replacemodule.oldmodule.OldService1;
import io.reflectoring.featureflags.patterns.replacemodule.oldmodule.OldService2;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
class FeatureFlaggedServiceModule {
private final FeatureFlagService featureFlagService;
public FeatureFlaggedServiceModule(FeatureFlagService featureFlagService) {
this.featureFlagService = featureFlagService;
}
@Bean("replaceModuleService1")
FeatureFlagFactoryBean<Service1> service1() {
return new FeatureFlagFactoryBean<>(Service1.class, featureFlagService::isNewServiceEnabled, new NewService1(), new OldService1());
}
@Bean("replaceModuleService2")
FeatureFlagFactoryBean<Service2> service2() {
return new FeatureFlagFactoryBean<>(Service2.class, featureFlagService::isNewServiceEnabled, new NewService2(), new OldService2());
}
}

View File

@@ -0,0 +1,7 @@
package io.reflectoring.featureflags.patterns.replacemodule;
public interface Service1 {
int doSomething();
}

View File

@@ -0,0 +1,7 @@
package io.reflectoring.featureflags.patterns.replacemodule;
public interface Service2 {
int doSomethingElse();
}

View File

@@ -0,0 +1,21 @@
package io.reflectoring.featureflags.patterns.replacemodule;
import io.reflectoring.featureflags.patterns.replacemodule.oldmodule.OldService1;
import io.reflectoring.featureflags.patterns.replacemodule.oldmodule.OldService2;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
class ServiceModule {
@Bean
Service1 service1() {
return new OldService1();
}
@Bean
Service2 service2() {
return new OldService2();
}
}

View File

@@ -0,0 +1,12 @@
package io.reflectoring.featureflags.patterns.replacemodule.newmodule;
import io.reflectoring.featureflags.patterns.replacemodule.Service1;
import org.springframework.stereotype.Component;
@Component
public class NewService1 implements Service1 {
@Override
public int doSomething() {
return 42;
}
}

View File

@@ -0,0 +1,13 @@
package io.reflectoring.featureflags.patterns.replacemodule.newmodule;
import io.reflectoring.featureflags.patterns.replacemodule.Service2;
import org.springframework.stereotype.Component;
@Component
public class NewService2 implements Service2 {
@Override
public int doSomethingElse() {
return 42;
}
}

View File

@@ -0,0 +1,12 @@
package io.reflectoring.featureflags.patterns.replacemodule.oldmodule;
import io.reflectoring.featureflags.patterns.replacemodule.Service1;
import org.springframework.stereotype.Component;
@Component
public class OldService1 implements Service1 {
@Override
public int doSomething() {
return 1;
}
}

View File

@@ -0,0 +1,13 @@
package io.reflectoring.featureflags.patterns.replacemodule.oldmodule;
import io.reflectoring.featureflags.patterns.replacemodule.Service2;
import org.springframework.stereotype.Component;
@Component
public class OldService2 implements Service2 {
@Override
public int doSomethingElse() {
return 1;
}
}

View File

@@ -26,4 +26,9 @@ public class TooglzFeatureFlagService implements FeatureFlagService {
return Features.USER_ACTION_TARGETED_FEATURE.isActive();
}
@Override
public Boolean isNewServiceEnabled() {
return false;
}
}

View File

@@ -15,4 +15,8 @@ togglz:
enabled: true
secured: false
path: /togglz
use-management-port: false
use-management-port: false
features:
featureOne: true
featureTwo: 42

View File

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

View File

@@ -0,0 +1,40 @@
package io.reflectoring.featureflags.patterns.replacebean;
import io.reflectoring.featureflags.FeatureFlagService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.mockito.BDDMockito.given;
@SpringBootTest
public class ReplaceBeanTest {
@MockBean
private FeatureFlagService featureFlagService;
@Autowired
private Service service;
@BeforeEach
void resetMocks() {
Mockito.reset(featureFlagService);
}
@Test
void oldServiceTest() {
given(featureFlagService.isNewServiceEnabled()).willReturn(false);
assertThat(service.doSomething()).isEqualTo(1);
}
@Test
void newServiceTest() {
given(featureFlagService.isNewServiceEnabled()).willReturn(true);
assertThat(service.doSomething()).isEqualTo(42);
}
}

View File

@@ -0,0 +1,46 @@
package io.reflectoring.featureflags.patterns.replacemethod;
import io.reflectoring.featureflags.FeatureFlagService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.mockito.BDDMockito.given;
@SpringBootTest
public class ReplaceMethodTest {
@MockBean
private FeatureFlagService featureFlagService;
@Autowired
private Service service;
@Autowired
private OldService oldService;
@BeforeEach
void resetMocks() {
Mockito.reset(featureFlagService);
}
@Test
void oldServiceTest() {
given(featureFlagService.isNewServiceEnabled()).willReturn(false);
assertThat(service.doSomething()).isEqualTo(1);
assertThat(oldService.doSomethingElse()).isEqualTo(2);
}
@Test
void newServiceTest() {
given(featureFlagService.isNewServiceEnabled()).willReturn(true);
assertThat(service.doSomething()).isEqualTo(42);
// doSomethingElse() is not behind a feature flag, so it should return the same value independant of the feature flag
assertThat(oldService.doSomethingElse()).isEqualTo(2);
}
}