ff4j是一款开源的实现特性功能切换的框架。简单来说通过aop和各种配置,去替代用硬代码if…else
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>
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,顾名思义,就是特性,通过唯一标识来代表一个特性
// 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"));
同理,特征持久化抽象(内存、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();
属性,特性的属性
// 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 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()));
如果持久化方式为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;
}
老生常谈,认证和授权。可以用于线上做灰度,新特性功能只有灰度用户可以体验
FF4J集成了springSecurity和ApacheShiro
FlippingStrategy的evaluate方法判断是否放开特性。自定义的strategy可以extends AbstractFlipStrategy
自实现
// 内置的各种策略
BlackList
ClientFilter
DarkLaunch
Drools
Expression
OfficeHour
Ponderation
ReleaseDate
ServerFilter
WhiteList
最常见的就是ReleaseDateFlipStrategy. 先开发验证好“国庆活动”等国庆当天再生效
分组,将特性打包分组,把多个开关合成一个开关
前面提到了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>
主要是基于RDBMS,FF4J内置了JdbcFeatureStore、JdbcPropertyStore等,同时
提供了基于关系型数据库的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);
非关系型的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>
1. spring框架整合, 可以通过@Flip注解的方式松耦合
2. 内置的web管理页面,一般叫它dashboard
3. 事件处理,观察者模式
4. FF4j这个类相当于一个门面,通过它可以操作上面提到的核心组件,并且做一些代理的事情