ff4j 一些核心概念
了解ff4j 的一些核心概念我们就可以更好的学习以及使用ff4j,以下是一些学习,整理
Feature
Feature 主要是用表示应用的一个功能,通过一个唯一的id标示(uid),主要目的是在运行时可以按需启用以及禁用
特性,FF4j 添加了一些属性(比如描述,可选的grouoname)访问控制列表,以及一些flipping 策略,同时我们也可以
添加自己的自定义属性
- 参考代码使用
// Simplest declaration
Feature f1 = new Feature("f1");
// Declare with description and initial state
Feature f2 = new Feature("f2", false, "sample description");
// Illustrate ACL & Group
Set < String > permission = new HashSet<String>();
permission.add("BETA-TESTER");
permission.add("VIP");
Feature f3 = new Feature("f3", false, "sample description", "GROUP_1", permission);
// Custom properties
Feature f4 = new Feature("f4");
f4.addProperty(new PropertyString("p1", "v1"));
f4.addProperty(new PropertyDouble("pie", Math.PI));
f4.addProperty(new PropertyInt("myAge", 12));
// Flipping Strategy
Feature f5 = new Feature("f5");
Calendar nextReleaseDate = Calendar.getInstance();
nextReleaseDate.set(Calendar.MONTH, Calendar.SEPTEMBER);
nextReleaseDate.set(Calendar.DAY_OF_MONTH, 1);
f5.setFlippingStrategy(new ReleaseDateFlipStrategy(nextReleaseDate.getTime()));
Feature f6 = new Feature("f6");
f6.setFlippingStrategy(new DarkLaunchStrategy(0.2d));
Feature f7 = new Feature("f7");
f7.setFlippingStrategy(new WhiteListStrategy("localhost"));
FeatureStore
FeatureStore 的目的是实现持久化存储的,提供了一个通用的crud,可以方便集成各类后端存储
- 参考代码使用
InMemoryFeatureStore fStore = new InMemoryFeatureStore();
// Operations on features
fStore.create(f5);
fStore.exist("f1");
fStore.enable("f1");
// Operations on permissions
fStore.grantRoleOnFeature("f1","BETA");
// Operation on groups
fStore.addToGroup("f1", "g1");
fStore.enableGroup("g1");
Map < String, Feature > groupG1 = fStore.readGroup("g1");
// Read all informations
Map < String, Feature > mapOfFeatures = fStore.readAll();
- 说明
对于FeatureStore的访问我们应该通过ff4j
Property
Property 是一个实体,可以包含各类的值,同时ff4j提供了一个通用的泛型类型我们可以自由扩展
- 参考使用
PropertyBigDecimal p01 = new PropertyBigDecimal();
PropertyBigInteger p02 = new PropertyBigInteger("d2", new BigInteger("1"));
PropertyBoolean p03 = new PropertyBoolean("d2", true);
PropertyByte p04 = new PropertyByte("d2", "1");
PropertyCalendar p05 = new PropertyCalendar("d2", "2015-01-02 13:00");
PropertyDate p06 = new PropertyDate("d2", "2015-01-02 13:00:00");
PropertyDouble p07 = new PropertyDouble("d2", 1.2);
PropertyFloat p08 = new PropertyFloat("d2", 1.1F);
PropertyInt p09 = new PropertyInt("d2", 1);
PropertyLogLevel p10 = new PropertyLogLevel("DEBUG");
PropertyLong p11 = new PropertyLong("d2", 1L);
PropertyShort p12 = new PropertyShort("d2", new Short("1"));
PropertyString p13 = new PropertyString("p1");
- 自定义扩展
import org.ff4j.property.Property;
import org.ff4j.test.property.CardinalPoint.Point;
public class CardinalPoint extends Property<Point> {
private static final long serialVersionUID = 1792311055570779010L;
public static enum Point {NORTH, SOUTH, EAST, WEST};
public CardinalPoint(String uid, Point lvl) {
super(uid, lvl, Point.values());
}
/** {@inheritDoc} */
public Point fromString(String v) { return Point.valueOf(v); }
public void north() { setValue(Point.NORTH); }
public void south() { setValue(Point.SOUTH); }
public void east() { setValue(Point.EAST); }
public void west() { setValue(Point.WEST); }
}
PropertyStore
类似于FeatureStore,目的是存储Property
- 参考使用
PropertyStore pStore = new InMemoryPropertyStore();
// CRUD
pStore.existProperty("a");
pStore.createProperty(new PropertyDate("a", new Date()));
Property<Date> pDate = (Property<Date>) pStore.readProperty("a");
pDate.setValue(new Date());
pStore.updateProperty(pDate);
pStore.deleteProperty("a");
// Several
pStore.clear();
pStore.readAllProperties();
pStore.listPropertyNames();
- 操作
// Access Property Store (with all its proxy : Audit, Cache, AOP....)
PropertyStore pStore1 = ff4j.getPropertiesStore();
// Access concrete class and implementation of the property store
PropertyStore pStore2 = ff4j.getConcretePropertyStore();
- 一些语法糖
ff4j.getProperties();
ff4j.createProperty(new PropertyString("p1", "v1"));
ff4j.getProperty("p1");
ff4j.deleteProperty("p1");
ff4j 架构概览
ff4j的设计是所有的操作都应通过ff4j 类,这样可以隐藏底层
- 细节
* FeatureStore and PropertyStore 主要是关于存储的通用crud
* EventRepository 主要是方便事件监控
* 如果 `audit` 设置为true, 对于存储的访问会通过包装的代理类`FeatureStoreAuditProxy` 以及`PropertyStoreAuditProxy` 进行数据访问,同时每个操作都会同时`EventRepository`的 EventPublisher
参考内部代码
// Publish feature usage to repository
private void publishCheck(String uid, boolean checked) {
if (isEnableAudit()) {
getEventPublisher().publish(new EventBuilder(this)
.feature(uid)
.action(checked ? ACTION_CHECK_OK : ACTION_CHECK_OFF)
.build());
}
}
// PropertyStoreAuditProxy : Publish create operation to repository
public < T > void createProperty(Property<T> prop) {
long start = System.nanoTime();
target.createProperty(prop);
ff4j.getEventPublisher().publish(new EventBuilder(ff4j)
.action(ACTION_CREATE)
.property(prop.getName())
.value(prop.asString())
.duration(System.nanoTime() - start)
.build());
}
- FF4jCacheProxy
主要目的是加速数据的获取,方便处理数据库以及http 类型的数据操作,底层依赖FF4JCacheManager
对于需要分布式一致性的场景可以使用 Terracotta, HazelCast or Redis (even if eh-cache is available).
参考代码
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;
}
public void delete(String featureId) {
// Access target store
getTargetFeatureStore().delete(featureId);
// even is not present, evict won't failed
getCacheManager().evictFeature(featureId);
}
- AuthorizationsManager
主要是处理feature的权限,但是ff4j不自己创建role,底层依赖shiro以及spring security 等实现
参考代码
public boolean isAllowed(Feature featureName) {
// No authorization manager, returning always true
if (getAuthorizationsManager() == null) {
return true;
}
// if no permissions, the feature is public
if (featureName.getPermissions().isEmpty()) {
return true;
}
Set<String> userRoles = getAuthorizationsManager().getCurrentUserPermissions();
for (String expectedRole : featureName.getPermissions()) {
if (userRoles.contains(expectedRole)) {
return true;
}
}
return false;
}
ff4j 的使用
- init
我们是需要通过FF4j 初始化的
一些细节
* If a proxy is not explicitly declared it won't be enabled (Cache, Audit)
* If stores are not explicitly defined, ff4j will use in-memory implementations (features, properties, events)
同时已经包含了一个灵活的基于内存以及文件的访问包装
FF4j ff4j = new FF4j("ff4j.xml");
以下高级配置
// Default constructor
FF4j ff4j = new FF4j();
// Initialized stores with JDBC
BasicDataSource dbcpDataSource = new BasicDataSource();
dbcpDataSource.setDriverClassName("org.hsqldb.jdbcDriver");
dbcpDataSource.setUsername("sa");
dbcpDataSource.setPassword("");
dbcpDataSource.setUrl("jdbc:hsqldb:mem:.");
ff4j.setFeatureStore(new JdbcFeatureStore(dbcpDataSource));
ff4j.setPropertiesStore(new JdbcPropertyStore(dbcpDataSource));
ff4j.setEventRepository(new JdbcEventRepository(dbcpDataSource));
// Enable Audit Proxy
ff4j.audit();
// Enable Cache Proxy
ff4j.cache(new InMemoryCacheManager());
// Explicite import
XmlConfig xmlConfig = ff4j.parseXmlConfig("ff4j.xml");
ff4j.getFeatureStore().importFeatures(xmlConfig.getFeatures().values());
ff4j.getPropertiesStore().importProperties(xmlConfig.getProperties().values());
当ff4j 初始化之后,使用方法
FF4j ff4j = new FF4j("ff4j.xml");
if (ff4j.check("foo") {
// do something
}
自动创建模式
@Test
public void createFeatureDynamically() {
// Given : Initialize as empty store
FF4j ff4j = new FF4j();
// When: Dynamically register new features
ff4j.create("f1").enable("f1");
// Then
assertTrue(ff4j.exist("f1"));
assertTrue(ff4j.check("f1"));
}
默认对于不包含的feature会触发异常,但是可以通过autocreate 为true,避免
@Test(expected = FeatureNotFoundException.class)
public void readFeatureNotFound() {
// Given
FF4j ff4j = new FF4j();
// When
ff4j.getFeature("i-dont-exist");
// Then, expect error...
}
@Test
public void readFeatureNotFoundAutoCreate() {
// Given
FF4j ff4j = new FF4j();
ff4j.autoCreate(true);
assertFalse(ff4j.exist("foo"));
// When
ff4j.check("foo");
// Then
assertTrue(ff4j.exist("foo"));
assertFalse(ff4j.check("foo"));
}
权限以及安全
很对时候对于特性的启用,可能是部分用户,但是ff4j不进行权限的管理,实际的处理需要依赖外部的权限以及安全实现
- AuthorizationManager
基于spring security 的一个开箱即用的实现
参考实现
public class CustomAuthorizationManager implements AuthorizationsManager {
public static ThreadLocal<String> currentUserThreadLocal = new ThreadLocal<String>();
private static final Map<String, Set<String>> permissions = new HashMap<String, Set<String>>();
static {
permissions.put("userA", new HashSet<String>(Arrays.asList("user", "admin", "beta")));
permissions.put("userB", new HashSet<String>(Arrays.asList("user")));
permissions.put("userC", new HashSet<String>(Arrays.asList("user", "beta")));
}
/** {@inheritDoc} */
@Override
public Set<String> getCurrentUserPermissions() {
String currentUser = currentUserThreadLocal.get();
return permissions.containsKey(currentUser) ? permissions.get(currentUser) : new HashSet<String>();
}
/** {@inheritDoc} */
@Override
public Set<String> listAllPermissions() {
Set<String> allPermissions = new HashSet<String>();
for (Set<String> subPersmission : permissions.values()) {
allPermissions.addAll(subPersmission);
}
return allPermissions;
}
}
配置
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration>
<features>
<feature uid="sayHello" description="my first feature" enable="true">
<security>
<role name="admin" />
</security>
</feature>
<feature uid="sayGoodBye" description="null" enable="true">
<security>
<role name="beta" />
<role name="user" />
</security>
</feature>
</features>
代码集成
@Test
public void sampleSecurityTest() {
// Create FF4J
FF4j ff4j = new FF4j("ff4j-security.xml");
// Add the Authorization Manager Filter
AuthorizationsManager authManager = new CustomAuthorizationManager();
ff4j.setAuthorizationsManager(authManager);
// Given : Feature exist and enable
assertTrue(ff4j.exist("sayHello"));
assertTrue(ff4j.getFeature("sayHello").isEnable());
// Unknow user does not have any permission => check is false
CustomAuthorizationManager.currentUserThreadLocal.set("unknown-user");
System.out.println(authManager.getCurrentUserPermissions());
assertFalse(ff4j.check("sayHello"));
// userB exist but he has not role Admin
CustomAuthorizationManager.currentUserThreadLocal.set("userB");
System.out.println(authManager.getCurrentUserPermissions());
assertFalse(ff4j.check("sayHello"));
// userA is admin
CustomAuthorizationManager.currentUserThreadLocal.set("userA");
System.out.println(authManager.getCurrentUserPermissions());
assertTrue(ff4j.check("sayHello"));
}
Flipping Strategy
主要是用来处理特性是否开启的
- 参考处理逻辑
参考代码
public class YaFF4jTest{
@Test
public void sampleFlippingStrategy() {
// Given
//default, in memory and empty.
FF4j ff4j = new FF4j();
Feature f1 = new Feature("f1", true);
ff4j.getFeatureStore().create(f1);
//The feature is enabled, no flipping strategy
Assert.assertTrue(ff4j.check("f1"));
// Let's add a flipping strategy (yyyy-MM-dd-HH:mm)
f1.setFlippingStrategy(new ReleaseDateFlipStrategy("2027-03-01-00:00"));
ff4j.getFeatureStore().update(f1);
// Even is feature is enabledn as strategy is false...
Assert.assertFalse(ff4j.check("f1"));
}
}
- FlippingStrategy 接口
参考图
参考资料
https://github.com/ff4j/ff4j/wiki/Core-Concepts
https://github.com/ff4j/ff4j/wiki/Flipping-Strategies