Advanced Concepts - ff4j/ff4j GitHub Wiki

Feature Groups

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

Aspect Oriented Programming

Overview

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.

Illustrate with example

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

Audit & Monitoring

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:

Caching

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

Spring integration

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

Spring-Boot

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");
    }
}
⚠️ **GitHub.com Fallback** ⚠️