当前位置: 首页 > 工具软件 > cb4j > 使用案例 >

FF4J(特性框架)简介及入门

鲜于允晨
2023-12-01

FF4J

什么是FF4J

ff4j是一款开源的实现特性功能切换的框架。简单来说通过aop和各种配置,去替代用硬代码if…else

简单入门

ff4j.xml

ff4j提供多种持久化方式(jdbc、redis、mongodb等)

<?xml version="1.0" encoding="UTF-8" ?>
<ff4j xmlns="http://www.ff4j.org/schema/ff4j"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://www.ff4j.org/schema/ff4j http://ff4j.org/schema/ff4j-1.4.0.xsd">

    <features>

        <!-- Will be "ON" if enable is set as true -->
        <feature uid="hello" enable="true"  description="This is my first feature" />

        <!-- Will be "ON" only if :
         (1) Enable is set as true
         (2) A security provider is defined
         (3) The current logged user has the correct permissions. -->
        <feature uid="mySecuredFeature" enable="true" >
            <security>
                <role name="USER" />
                <role name="ADMIN" />
            </security>
        </feature>

        <!-- Will be "ON" only if
         (1) Enable is set as true
         (2) Strategy predicate is true (here current > releaseDate) -->
        <feature uid="myFutureFeature" enable="true">
            <flipstrategy class="org.ff4j.strategy.time.ReleaseDateFlipStrategy" >
                <param name="releaseDate" value="2020-07-14-14:00" />
            </flipstrategy>
        </feature>

        <!-- Features can be grouped to be toggled in the same time -->
        <feature-group name="sprint_3">
            <feature uid="userStory3_1" enable="false" />
            <feature uid="userStory3_2" enable="false" />
        </feature-group>

    </features>
</ff4j>

Usage

public class FF4jHelloTest {
    @Test
    public void testMyFirst(){
        assertNotNull(getClass().getClassLoader().getResourceAsStream("ff4j.xml"));
    
        // When: init
        FF4j ff4j = new FF4j("ff4j.xml");
        
        assertEquals(5, ff4j.getFeatures().size());
        // 是否存在特性
        assertTrue(ff4j.exist("hello"));
        // 特性是否生效
        assertTrue(ff4j.check("hello"));
        
        // Usage
        if (ff4j.check("hello")){
            System.out.println("Hello FF4j !");
        }
        
        ff4j.disable("hello");
        assertFalse(ff4j.check("hello"));
    }
    
    @Test
    public void testAutoCreate(){
        assertNotNull(getClass().getClassLoader().getResourceAsStream("ff4j.xml"));
    
        // When: init
        FF4j ff4j = new FF4j("ff4j.xml");
        
        try {
            // 如果没有开始自动创建,会抛异常
            if (ff4j.check("notExistFeature")){
                // do nothing
            }
        } catch (Exception e){
            System.out.println("ff4j throw exception when feature not exist !");
        }
        
        // 开启后,如果不存在会自动创建,默认不生效
        ff4j.setAutocreate(true);
        if (!ff4j.check("notExistFeature")){
            System.out.println("feature toggle off or not exist");
        }
    }
    
    @Test
    public void testGroups(){
        assertNotNull(getClass().getClassLoader().getResourceAsStream("ff4j.xml"));
    
        // When: init
        FF4j ff4j = new FF4j("ff4j.xml");
        
        assertTrue(ff4j.exist("userStory3_1"));
        assertTrue(ff4j.exist("userStory3_2"));
        
        // 获取分组
        assertTrue(ff4j.getStore().readAllGroups().contains("sprint_3"));
        assertEquals("sprint_3", ff4j.getFeature("userStory3_1").getGroup());
        assertEquals("sprint_3", ff4j.getFeature("userStory3_2").getGroup());
        
        assertFalse(ff4j.check("userStory3_1"));
        assertFalse(ff4j.check("userStory3_2"));
        
        // 启用分组,这里用分组可以同时开启或关闭多个特性功能
        ff4j.getStore().enableGroup("sprint_3");
    
        assertTrue(ff4j.check("userStory3_1"));
        assertTrue(ff4j.check("userStory3_2"));
    }
}

核心组件

Feature

Feature,顾名思义,就是特性,通过唯一标识来代表一个特性

 // Simplest declaration
Feature f1 = new Feature("f1");

// Declaration with desc and init state
Feature f2 = new Feature("f2", false, "sample desc");

// ACL & Group
Set<String> permission = new HashSet<>(2);
permission.add("BETA-TESTER");
permission.add("VIP");
Feature f3 = new Feature("f3", false, "sample desc", "g1", permission);

// Custom Properties
Feature f4 = new Feature("f4");
f4.addProperty(new PropertyString("p1", "v1"));
f4.addProperty(new PropertyDouble("pie", Math.PI));

// Flipping Strategy
Feature f5 = new Feature("f5");
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.MONTH, Calendar.SEPTEMBER);
calendar.set(Calendar.DAY_OF_MONTH, 1);
f5.setFlippingStrategy(new ReleaseDateFlipStrategy(calendar.getTime()));

Feature f6 = new Feature("f6");
f6.setFlippingStrategy(new DarkLaunchStrategy(0.2D));

Feature f7 = new Feature("f7");
f7.setFlippingStrategy(new WhiteListStrategy("localhost"));

FeatureStore

同理,特征持久化抽象(内存、jdbc、redis等)

Feature f5 = new Feature("f5");
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.MONTH, Calendar.SEPTEMBER);
calendar.set(Calendar.DAY_OF_MONTH, 1);
f5.setFlippingStrategy(new ReleaseDateFlipStrategy(calendar.getTime()));

InMemoryFeatureStore store = new InMemoryFeatureStore();
store.create(f5);
store.exist("f1");
store.enable("f1");

// grant permission
store.grantRoleOnFeature("f1", "BETA");

store.addToGroup("f1", "g1");
store.enable("g1");
Map<String, Feature> g1 = store.readGroup("g1");

store.readAll();

Property

属性,特性的属性

 // wrap java type
 // 默认包装了很多Java类型,也可以通过extends Property<?>扩展
PropertyBigDecimal propertyBigDecimal = new PropertyBigDecimal();
PropertyBoolean f2 = new PropertyBoolean("f2", false);
PropertyByte b1 = new PropertyByte("b1", Byte.valueOf("1"));
PropertyDate d1 = new PropertyDate("d1", new Date());

PropertyStore

属性持久化

PropertyStore pStore = new InMemoryPropertyStore();
        
// CURD
pStore.existProperty("a");
pStore.createProperty(new PropertyDate("d1", new Date()));
Property<Date> pDate = (Property<Date>) pStore.readProperty("a");
pDate.setValue(new Date());
pStore.updateProperty(pDate);
pStore.deleteProperty("a");

pStore.clear();
pStore.readAllProperties();
pStore.listPropertyNames();

// get PropertyStore from ff4j
FF4j ff4j = new FF4j("ff4j.xml");
PropertyStore propertiesStore = ff4j.getPropertiesStore();

// Usage FF4J can wrap all operation
// proxy all, u know
ff4j.createProperty(new PropertyDate("ddd", new Date()));

Cache

如果持久化方式为jdbc等(数据在磁盘),可以考虑将Feature、Property等缓存到内存, 具体可参考FF4JCacheManager

public Feature read(String featureUid) {
    Feature fp = getCacheManager().getFeature(featureUid);
    // not in cache but may has been created from now
    if (null == fp) {
        fp = getTargetFeatureStore().read(featureUid);
        getCacheManager().putFeature(fp);
    }
    return fp;
}

Security

老生常谈,认证和授权。可以用于线上做灰度,新特性功能只有灰度用户可以体验

FF4J集成了springSecurity和ApacheShiro

Flipping Strategy

FlippingStrategy的evaluate方法判断是否放开特性。自定义的strategy可以extends AbstractFlipStrategy
自实现

// 内置的各种策略
BlackList
ClientFilter
DarkLaunch
Drools
Expression
OfficeHour
Ponderation
ReleaseDate
ServerFilter
WhiteList

最常见的就是ReleaseDateFlipStrategy. 先开发验证好“国庆活动”等国庆当天再生效

Group

分组,将特性打包分组,把多个开关合成一个开关

存储形式

前面提到了FF4J提供了很多持久化方式,包括内存、jdbc等

内存

默认的方式,以xml作为存储介质,然后解析xml将Feature等加载到内存。
该方式会在应用重启后复原,即之前做的改动都不存在了,会重新读xml到内存

<?xml version="1.0" encoding="UTF-8" ?>
<features xmlns="http://www.ff4j.org/schema/ff4j"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://www.ff4j.org/schema/ff4j http://ff4j.org/schema/ff4j.xsd">

 <!-- Simplest -->
 <feature uid="A" enable="true" />

 <!-- Add description -->
 <feature uid="B" description="Expect to say good bye..." enable="false" />

 <!-- Security stuff -->
 <feature uid="C" enable="false">
  <security>
   <role name="USER"  />
   <role name="ADMIN" />
  </security>
 </feature>

 <!-- Some strategies and a group -->
 <feature-group name="strategies">
  
  <feature uid="S1" enable="true">
   <flipstrategy class="org.ff4j.strategy.el.ExpressionFlipStrategy"> 
    <param name="expression" value="A | B" />
   </flipstrategy>
  </feature>
  
  <feature uid="S2" enable="true">
   <flipstrategy class="org.ff4j.strategy.ReleaseDateFlipStrategy">
    <param name="releaseDate" value="2013-07-14-14:00" />
   </flipstrategy>
  </feature>

  <feature uid="S3" description="null" enable="true">
    <flipstrategy class="org.ff4j.strategy.PonderationStrategy">
     <param name="weight" value="0.5" />
    </flipstrategy>
  </feature>
  
  <feature uid="S4" description="z" enable="true">
    <flipstrategy class="org.ff4j.strategy.ClientFilterStrategy"> 
     <param name="grantedClients" value="c1,c2" />
    </flipstrategy>
  </feature>

  <feature uid="S5" description="null" enable="true"> 
   <flipstrategy class="org.ff4j.strategy.ServerFilterStrategy">
    <param name="grantedServers" value="s1,s2" />
   </flipstrategy>
  </feature>

 </feature-group>
</features>

JDBC

主要是基于RDBMS,FF4J内置了JdbcFeatureStore、JdbcPropertyStore等,同时
提供了基于关系型数据库的DDL脚本

DDL脚本
-- Main Table to store Features
-- 存储特征
CREATE TABLE FF4J_FEATURES (
  "FEAT_UID"     	VARCHAR(100),
  "ENABLE"  		INTEGER NOT NULL,
  "DESCRIPTION" 	VARCHAR(1000),
  "STRATEGY"		VARCHAR(1000),
  "EXPRESSION"	    VARCHAR(255),
  "GROUPNAME"		VARCHAR(100),
  PRIMARY KEY("FEAT_UID")
);

-- Roles to store ACL, FK to main table
-- 存储ACL(访问控制列表)
CREATE TABLE FF4J_ROLES (
  "FEAT_UID"     VARCHAR(100) REFERENCES FF4J_FEATURES("FEAT_UID"),
  "ROLE_NAME"    VARCHAR(100),
  PRIMARY KEY("FEAT_UID", "ROLE_NAME")
);

-- Feature Internal Custom Properties
-- 存储自定义属性
CREATE TABLE FF4J_CUSTOM_PROPERTIES (
  "PROPERTY_ID"  VARCHAR(100) NOT NULL,
  "CLAZZ" 		 VARCHAR(255) NOT NULL,
  "CURRENTVALUE" VARCHAR(255),
  "FIXEDVALUES"	 VARCHAR(1000),
  "DESCRIPTION"	 VARCHAR(1000),
  "FEAT_UID"     VARCHAR(100) REFERENCES FF4J_FEATURES("FEAT_UID"),
  PRIMARY KEY("PROPERTY_ID", "FEAT_UID")
);

-- @PropertyStore (edit general properties)
-- 存储通用属性
CREATE TABLE FF4J_PROPERTIES (
  "PROPERTY_ID"  VARCHAR(100) NOT NULL,
  "CLAZZ" 		 VARCHAR(255) NOT NULL,
  "CURRENTVALUE" VARCHAR(255),
  "FIXEDVALUES"	 VARCHAR(1000),
  "DESCRIPTION"	 VARCHAR(1000),
  PRIMARY KEY("PROPERTY_ID")
);

-- @see JdbcEventRepository (audit event)
-- 存储审核事件
CREATE TABLE FF4J_AUDIT (
  "EVT_UUID" 	 VARCHAR(40)  NOT NULL,
  "EVT_TIME" 	 TIMESTAMP 	  NOT NULL,
  "EVT_TYPE" 	 VARCHAR(30)  NOT NULL,
  "EVT_NAME" 	 VARCHAR(30)  NOT NULL,
  "EVT_ACTION" 	 VARCHAR(30)  NOT NULL,
  "EVT_HOSTNAME" VARCHAR(100)  NOT NULL,
  "EVT_SOURCE" 	 VARCHAR(30)  NOT NULL,
  "EVT_DURATION" INTEGER,
  "EVT_USER" 	 VARCHAR(30),
  "EVT_VALUE" 	 VARCHAR(100),
  "EVT_KEYS" 	 VARCHAR(255),
  PRIMARY KEY("EVT_UUID", "EVT_TIME")
);
简单使用

jdbc

// Initialization of your DataSource
DataSource ds = ...

FF4j ff4j = new FF4j();
ff4j.setFeatureStore(new JdbcFeatureStore(ds));
ff4j.setPropertiesStore(new JdbcPropertyStore(ds));
ff4j.setEventRepository(new JdbcEventRepository(ds));

spring-jdbc

// Initialization of your DataSource
DataSource ds = ...

// Init the framework full in memory
FF4j ff4j = new FF4j();

// Feature States in a RDBMS
FeatureStoreSpringJdbc featureStore= new FeatureStoreSpringJdbc();
featureStore.setDataSource(ds);
ff4j.setFeatureStore(featureStore);

// Properties in RDBMS
PropertyStoreSpringJdbc propertyStore= new PropertyStoreSpringJdbc();
jdbcStore.setDataSource(ds);
ff4j.setPropertiesStore(propertyStore);

// Audit in RDBMS
// So far the implementation with SpringJDBC is not there, leverage on default JDBC
EventRepository auditStore = new JdbcEventRepository(ds);
ff4j.setEventRepository(eventRepository);

ff4j.audit(true);

Redis

非关系型的KV数据库, 内置了FeatureStoreRedis、PropertyStoreRedis等,相当于
将feature、property之类的存储在Redis

redis连接

//  Will use default value for REDIS (localhost/6379 @{@link `redis.clients.jedis.Protocol`})
new RedisConnection();
        
// enforce host and port
new RedisConnection("localhost", 6379);
        
// if password is enabled in redis
new RedisConnection("localhost", 6379, "requiredPassword");
        
// Defined your own pool with all capabilities of {@link redis.clients.jedis.JedisPool}
new RedisConnection(new JedisPool("localhost", 6379));
        
// Use the sentinel through specialized JedisPool {@link redis.clients.jedis.JedisSentinelPool}
new RedisConnection(new JedisSentinelPool("localhost", Util.set("master", "slave1")));

ff4j curd

// Initialization of FF4J
FF4j ff4j = new FF4j();
ff4j.setFeatureStore(new FeatureStoreRedis(redisConnection));
ff4j.setPropertiesStore(new PropertyStoreRedis(redisConnection));
ff4j.setEventRepository(new EventRepositoryRedis(redisConnection));

// Empty Store
ff4j.getFeatureStore().clear();
ff4j.getPropertiesStore().clear();

// Work a bit with CRUD
Feature f1 = new Feature("f1", true, "My firts feature", "Group1");
ff4j.getFeatureStore().create(f1);
        
PropertyString p1 = new PropertyString("p1", "v1");
ff4j.getPropertiesStore().createProperty(p1);
        
ff4j.check("f1");

存储形式

m127.0.0.1:6379> keys FF4J*
 1) "FF4J_PROPERTY_p1"
 2) "FF4J_FEATURE_f1"
 3) "FF4J_EVENT_-1467803617889-9ac83532-2480-4609-9a76-5793cdb21e1a"
 4) "FF4J_EVENT_-1467803617833-a856c1b9-cedc-4164-815b-55bf9dc3adef"
 5) "FF4J_EVENT_-1467803654139-03a9cb1e-b5b3-4d9f-91dd-0ca5b2cc5a01"
 6) "FF4J_EVENT_-1467803654144-cc591275-a98b-4efe-ab39-4bfd0839aa1e"
 7) "FF4J_EVENT_-1467803617829-4487077f-680a-44ba-b0e2-43e6685dc044"
 8) "FF4J_EVENT_-1467803654599-83af1ce2-05f6-4264-8b53-d1541e8e036c"

127.0.0.1:6379> get FF4J_FEATURE_f1
"{\"uid\":\"f1\",\"enable\":true,\"description\":\"My firts feature\",\"group\":\"Group1\",\"permissions\":[],\"flippingStrategy\":null,\"customProperties\":{}}"

127.0.0.1:6379> get FF4J_PROPERTY_p1
"{\"name\":\"p1\",\"description\":null,\"type\":\"org.ff4j.property.PropertyString\",\"value\":\"v1\",\"fixedValues\":null}"

127.0.0.1:6379> get FF4J_EVENT_-1467803654599-83af1ce2-05f6-4264-8b53-d1541e8e036c
"{\"id\": \"83af1ce2-05f6-4264-8b53-d1541e8e036c\", \"timestamp\":1467803654599, \"hostName\": \"mbp\", \"source\": \"JAVA_API\", \"name\": \"f1\", \"type\": \"feature\", \"action\": \"checkOn\", \"duration\":0}"

127.0.0.1:6379>

其他相关

参考:https://github.com/ff4j/ff4j/wiki

1. spring框架整合, 可以通过@Flip注解的方式松耦合
2. 内置的web管理页面,一般叫它dashboard
3. 事件处理,观察者模式
4. FF4j这个类相当于一个门面,通过它可以操作上面提到的核心组件,并且做一些代理的事情
 类似资料: