Advanced Concepts - ff4j/ff4j GitHub Wiki
Features can be gathered as group. It is then possible to toggle the whole group. This capability can be useful for instance, if you want to group all the "user stories" of sprint in the same release.
• Let's create a new XML fileff4j-group.xml to illustrate
<?xml version="1.0" encoding="UTF-8" ?>
<features>
<!-- Sample Feature Group -->
<feature-group name="release-2.3">
<feature uid="users-story1" enable="false" />
<feature uid="users-story2" enable="false" />
</feature-group>
<feature uid="featA" enable="true" />
<feature uid="featB" enable="false" />
</features>
• Here is a sample utilisation of groups.
@Test
public void myGroupTest() {
FF4j ff4j = new FF4j("ff4j-groups.xml");
// Check features loaded
assertEquals(4, ff4j.getFeatures().size());
assertTrue(ff4j.exist("featA"));
assertTrue(ff4j.exist("users-story1"));
assertTrue(ff4j.getStore().existGroup("release-2.3"));
System.out.println("Features loaded OK");
// Given
assertFalse(ff4j.check("users-story1"));
assertFalse(ff4j.check("users-story2"));
// When
ff4j.enableGroup("release-2.3");
// Then
assertTrue(ff4j.check("users-story1"));
assertTrue(ff4j.check("users-story2"));
}
• You can also access to all operation dynamically through the FeatureStore
@Test
public void workWithGroupTest() {
// Given
FF4j ff4j = new FF4j("ff4j-groups.xml");
assertTrue(ff4j.exist("featA"));
// When
ff4j.getStore().addToGroup("featA", "new-group");
// Then
assertTrue(ff4j.getStore().existGroup("new-group"));
assertTrue(ff4j.getStore().readAllGroups().contains("new-group"));
Map<String, Feature> myGroup = ff4j.getStore().readGroup("new-group");
assertTrue(myGroup.containsKey("featA"));
// A feature can be in a single group
// Here changing => deleting the last element of a group => deleting the group
ff4j.getStore().addToGroup("featA", "group2");
assertFalse(ff4j.getStore().existGroup("new-group"));
}
From the beginning of this guide, we use intrusive tests statements within source code to perform flipping like in :
if (ff4j.check("featA")) {
// new code
} else {
// legacy
}
This approach is quite intrusive into source code. You can nested different feature toggles at you may consider to clean often your code and remove obsolete features. A good alternative is to rely on Dependency Injection, also called Inversion of control (ioc) to choose the correct implementation of the service at runtime.
Ff4j provide the @Flip annotation to perform flipping on methods using AOP proxies. At runtime, the target service is proxified by the ff4j component which choose an implementation instead of another using feature status (enable/disable). It leverage on Spring AOP Framework.
In the following chapter, we modify the project created in getting started to illustrate flipping through aop
• Add the dependency to ff4j-aop in your project
<dependency>
<groupId>org.ff4j</groupId>
<artifactId>ff4j-aop</artifactId>
<version>${ff4j.version}</version>
</dependency>
• Define a sample interface with the annotation :
public interface GreetingService {
@Flip(name="language-french", alterBean="greeting.french")
String sayHello(String name);
}
• Define a first implementation, to tell hello in english
@Component("greeting.english")
public class GreetingServiceEnglishImpl implements GreetingService {
public String sayHello(String name) {
return "Hello " + name;
}
}
• Define a second implementation, to tell hello in french
@Component("greeting.french")
public class GreetingServiceFrenchImpl implements GreetingService {
public String sayHello(String name) {
return "Bonjour " + name;
}
}
• The AOP capability leverage on Spring Framework. To enable the Autoproxy, please ensure that the package org.ff4j.aop is scanned by spring at startup. The applicationContext-aop.xml should look like :
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<context:component-scan base-package="org.ff4j.aop, org.ff4j.sample"/>
<bean id="ff4j" class="org.ff4j.FF4j" >
<property name="store" ref="ff4j.store.inmemory" />
</bean>
<bean id="ff4j.store.inmemory" class="org.ff4j.store.InMemoryFeatureStore" >
<property name="location" value="ff4j-aop.xml" />
</bean>
</beans>
• Create a dedicated ff4j.xml file with the feature name language-french let's say ff4j-demo-aop.xml
<?xml version="1.0" encoding="UTF-8" ?>
<features>
<feature uid="language-french" enable="false" />
</features>
• Demonstrate how does it work through a test :
import junit.framework.Assert;
import org.ff4j.FF4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:*applicationContext-aop.xml")
public class FeatureFlippingThoughAopTest {
@Autowired
private FF4j ff4j;
@Autowired
@Qualifier("greeting.english")
private GreetingService greeting;
@Test
public void testAOP() {
Assert.assertTrue(greeting.sayHello("CLU").startsWith("Hello"));
ff4j.enable("language-french");
Assert.assertTrue(greeting.sayHello("CLU").startsWith("Bonjour"));
}
}
If you have designed your code to isolate features (which pretty on-trend nowadays, promoting by Agile developement mode and epic/stories) and use FF4j to perform toggle you have an opportunity. FF4j can log any feature invocation to help you counting hit ratio (per features, sources of customers) or building usage histograms.
To implement these audit capabilities simply invoke thoselines :
ff4j.setEventRepository(<HERE YOUR EVENT_REPOSITORY DEFINITION>);
ff4j.audit(true);
Under the hood it FF4j will create a "AuditProxy" pretty static
if (isEnableAudit()) {
if (fstore != null && !(fstore instanceof FeatureStoreAuditProxy)) {
this.fstore = new FeatureStoreAuditProxy(this, fstore);
}
if (pStore != null && !(pStore instanceof PropertyStoreAuditProxy)) {
this.pStore = new PropertyStoreAuditProxy(this, pStore);
}
Most of stores provide implementation for Audit but sometimes it does not make sense to store timeseries in target DB.(like Neo4j). Here are sample initialization of EventRepository
:
// JDBC
HikariDataSource hikariDataSource;
ff4j.setEventRepository(new EventRepositorySpringJdbc(hikariDataSource));
// ELASTICSEARCH
URL urlElastic = new URL("http://" + elasticHostName + ":" + elasticPort);
ElasticConnection connElastic = new ElasticConnection(ElasticConnectionMode.JEST_CLIENT, elasticIndexName, urlElastic);
ff4j.setEventRepository(new EventRepositoryElastic(connElastic));
// REDIS
RedisConnection redisConnection = new RedisConnection(redisHostName, redisPort, redisPassword);
ff4j.setEventRepository(new EventRepositoryRedis(redisConnection ));
// MONGODB
MongoClient mongoClient;
ff4j.setEventRepository(new EventRepositoryMongo(mongoClient, mongoDatabaseName));
// CASSANDRA
Cluster cassandraCluster;
CassandraConnection cassandraConnection = new CassandraConnection(cassandraCluster)
ff4j.setEventRepository(new EventRepositoryCassandra(cassandraConnection));
You should be able to start seeing metrics in the UI:
To ensure best consistency, the default behavior of FF4j is to check stores for each invocation of the check
method. Sometimes it raises some performance issue (specially if you rely on REST API and remote calls). For some technologies it may took some time (like HTTP) or other cannot easily handle this hit ratio (like Neo4j). This is the reason FF4j proposed a caching mechanism on top of stores. The cache strategy implemented is named "cache aside" and it is the more commonly used.
Several caching providers have been implemented for you (EhCache, Redis, Hazelcast, Terracotta or Ignite, JSR107)
First you have to create the proper cacheManager:
// REDIS (dependency: ff4j-store-redis)
RedisConnection redisConnection = new RedisConnection(redisHostName, redisPort, redisPassword);
FF4JCacheManager ff4jCache = new FF4jCacheManagerRedis(redisConnection );
// EHCACHE (dependency: ff4j-store-ehcache)
FF4JCacheManager ff4jCache = new FeatureCacheProviderEhCache();
// HAZELCAST (dependency: ff4j-store-hazelcast)
Config hazelcastConfig;
Properties systemProperties;
FF4JCacheManager ff4jCache = new CacheManagerHazelCast(hazelcastConfig, systemProperties);
// JHIPSTER
HazelcastInstance hazelcastInstance;
FF4JCacheManager ff4jCache = new JHipsterHazelcastCacheManager(hazelcastInstance);
And then to invoke the cache
method in ff4j:
ff4j.cache(ff4jCache);
https://github.com/clun/ff4j/blob/master/ff4j-core/src/main/java/org/ff4j/FF4j.java#L582-L587
public FF4j cache(FF4JCacheManager cm) {
FF4jCacheProxy cp = new FF4jCacheProxy(getFeatureStore(), getPropertiesStore(), cm);
setFeatureStore(cp);
setPropertiesStore(cp);
return this;
}
You should now be able to see the "clear cache" button in the console and the corresponding logo.
Here is a unit testing illustrating the behavior of the cache. Please note that EHCACHE
is relevant only for not distributed.
public class EhCacheCacheManagerTest2 {
private FF4j ff4j = null;
private FF4JCacheManager cacheManager = null;
@Before
/** Init cache. */
public void initialize() {
cacheManager = new FeatureCacheProviderEhCache();
// Stores in memory
ff4j = new FF4j();
ff4j.createFeature(new Feature("f1", false));
ff4j.createFeature(new Feature("f2", true));
// Enable caching using EHCACHE
ff4j.cache(cacheManager);
// This is how to access underlying cacheManager from FF4J instance
// ff4j.getCacheProxy().getCacheManager();
}
@Test
public void playWithCacheTest() {
// Update with Check
Assert.assertFalse(cacheManager.listCachedFeatureNames().contains("f1"));
ff4j.check("f1");
Assert.assertTrue(cacheManager.listCachedFeatureNames().contains("f1"));
// Updated for create/update
Assert.assertFalse(cacheManager.listCachedFeatureNames().contains("f3"));
ff4j.createFeature(new Feature("f3", false));
Assert.assertTrue(cacheManager.listCachedFeatureNames().contains("f3"));
ff4j.enable("f3");
// Ensure eviction from cache
Assert.assertFalse(cacheManager.listCachedFeatureNames().contains("f3"));
// Updated for deletion
ff4j.check("f3");
Assert.assertTrue(cacheManager.listCachedFeatureNames().contains("f3"));
ff4j.delete("f3");
Assert.assertFalse(cacheManager.listCachedFeatureNames().contains("f3"));
}
@After
public void clearCache() {
ff4j.getCacheProxy().clear();
}
The ff4j
component can be easily defined as a Spring Bean.
• Add Spring dependencies to your project
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency></programlisting>
• Add the following applicationContext.xml file to your src/test/resources
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<bean id="ff4j" class="org.ff4j.FF4j" >
<property name="store" ref="ff4j.store.inmemory" />
</bean>
<bean id="ff4j.store.inmemory" class="org.ff4j.store.InMemoryFeatureStore" >
<property name="location" value="ff4j.xml" />
</bean>
</beans>
• The features are registered within in-memory store.Write the following spring-oriented test
package org.ff4j.sample;
import static org.junit.Assert.fail;
import org.ff4j.FF4j; import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:*applicationContext.xml"})
public class CoreSpringTest {
@Autowired
private FF4j ff4j;
@Test
public void testWithSpring() {
// Test value at runtime
if (ff4j.check("sayHello")) {
// Feature ok !
System.out.println("Hello World !");
} else {
fail();
}
}
}
Add ff4j-jmx dependency to your project.
Example with maven :
<dependency>
<groupId>org.ff4j</groupId>
<artifactId>ff4j-jmx</artifactId>
</dependency>
Create Spring Configuration class, an instance of FF4J and scan org.ff4j.jmx package. Then Spring Boot will discover the FF4JMBean and export it automatically.
@Configuration
@ComponentScan(basePackages = "org.ff4j.jmx" )
public class FF4JConfiguration {
/**
* Create and configure FF4J
*/
@Bean
public FF4j getFF4j() {
return new FF4j("ff4j.xml");
}
}