文章目录
- 功能开关是什么
- 为什么需要功能开关?
- 功能
- 流程
- 组件
- 业务接入端
- 常用Api
- 功能开关管理端
- 高级
- 面向切面 AOP
- 审计和监控
- 缓存
- 微服务中使用
- 概念
- 功能 Feature
- 功能存储 FeatureStore
- 属性 Property
- 属性存储 PropertyStore
- FF4J架构
- FF4J使用
- 开关策略 FlippingStrategy
- 功能组 Feature Groups
- 参考
功能开关是什么
FF4J,Java的功能切换(Feature Flipping for Java),是一个用Java编写的功能切换(Feature Toggle)提案。
功能切换(也称为功能翻转、功能标志或功能位)是程序或应用程序在运行时启用和禁用功能的能力。
切换可以通过编程方式、通过Web控制台、通过API、通过命令行甚至通过JMX来操作。
源代码包括多条路径,根据标志/功能的值来执行或不执行。
一个功能代表了一个潜在地跨越应用程序的各个层级,从用户界面到数据访问的业务逻辑。因此,为了实现功能切换机制,我们必须在每个层级中提供帮助。
为什么需要功能开关?
功能开关(Feature Toggle)是一种在运行时启用和禁用功能的机制,它提供了在不进行部署的情况下控制功能的能力。功能开关可以以编程方式、通过Web控制台、通过API、通过命令行甚至通过JMX进行操作。功能开关的核心思想是根据标志或特性的值来决定是否执行代码的不同路径。
功能开关的应用场景包括:
-
应对未完成功能: 如果在当前迭代中无法完成某项功能,您可以使用功能开关将其关闭,以防止未完成的功能被发布到生产环境中。
-
快速回滚: 如果新的功能实现或用户体验不受欢迎,您可以通过关闭功能开关迅速返回到旧的实现。
-
A/B测试和金丝雀发布: 功能开关使得可以根据用户角色或群组来启用或禁用功能,从而支持A/B测试和金丝雀发布(Canary Release)等策略。
-
灵活部署: 功能开关允许您动态启用或禁用功能,而无需重新部署应用程序。
-
监控和审计: 通过功能开关,您可以监控功能的使用情况并记录事件,以生成仪表板或分析功能的使用趋势。
-
快速切换功能分支: 结合功能开关和功能分支可以更灵活地管理代码的版本和功能切换。
总之,功能开关是一种强大的工具,可以帮助团队更灵活地管理和发布功能,以满足不同的需求和情况。通过控制功能的启用和禁用,您可以更安全、更可控地进行软件开发和部署。
功能
- 功能切换(Feature Toggle):在运行时启用和禁用功能 - 无需部署。在你的代码中,通过实现基于动态谓词的多个路径(if/then/else)来保护功能。
- 基于角色的功能切换(Role-based Toggling):不仅可以使用标志值启用功能,还可以使用角色和组(Canary Release)来控制访问。支持不同的框架,从Spring Security开始。
- 基于策略的功能切换(Strategy-based Toggling):实现自定义谓词(策略模式)来评估功能是否已启用。一些预置的策略包括白名单/黑名单、基于时间、基于表达式等。连接外部源,如Drools规则引擎。
- 基于AOP的功能切换(AOP-driven Toggling):保持代码干净可读:避免嵌套的if语句,而使用注解。借助Spring AOP目标实现在运行时选择,因此受功能状态驱动。
- 功能监控(Features Monitoring):对于每个功能的执行,ff4j评估谓词,因此可以收集和记录事件、指标,以计算漂亮的仪表板或绘制功能随时间的使用曲线。
- 审计跟踪(Audit Trail):可以追踪并保存每个操作(创建、更新、删除、切换)以进行故障排除。借助权限管理(AuthorizationManager),可以识别用户。
- Web控制台(Web Console):使用Web UI来管理FF4j(包括功能和属性)。作为库中的Servlet打包,您可以将其暴露在后端应用程序中。支持近10种语言。
- 广泛的数据库选择:我们自豪地支持20多种数据库技术,用于存储您的功能、属性和事件。相同的业务模型,多种实现。借助扩展点,轻松构建自己的解决方案。
- Spring Boot Starter:在您的微服务中导入ff4j-spring-boot-starter依赖项,以立即启用Web控制台和REST API。(用于后端应用程序。现在与Spring Boot 2.x兼容。
- REST API:通过WEB API操作FF4j。这是使用ff4j与其他语言(特别是JavaScript前端)的方式。(还可以利用FeatureStoreHttp来避免微服务直接连接到数据库。
- 属性(CMDB)存储:不仅存储功能状态,还存储任何属性值。创建可以在运行时更改的属性。它与最常用的框架集成,如Spring、Archaius、commons-config或Consul。
- (分布式)缓存:评估谓词可能对数据库施加压力(高命中率)。ff4j提供本地和分布式缓存以帮助减轻压力(编辑功能也会清除缓存)。利用JSR-107,它支持大多数缓存解决方案。
- 命令行界面:为了自动化操作或因为Web端口可能被阻止(您知道,生产环境…),您可以通过SSH使用我们的命令行界面(CLI)进行工作,或者使用我们的Shell #devOps。它将直接与存储进行交互。
- JMX和MBeans:可以通过JMX执行有限的操作。ff4j公开了一些Mbean以从外部工具(Nagios等)读取指标或切换功能。不是所有应用程序都基于Web(批处理、Shell、独立应用程序…)。
流程
组件
业务接入端
1.添加依赖
在pom.xml中指定的依赖项是ff4j-core(或ff4j-aop),您还可以添加额外的依赖项**ff4j-core-***来定义要使用的数据库技术。FeatureStore、PropertyStore 和 EventRepository不必使用相同的存储技术。
<dependency>
<groupId>org.ff4j</groupId>
<artifactId>ff4j-core</artifactId>
<version>2.0.0</version>
</dependency>
<!-- Spring 环境可以只引用下面这个即可-->
<dependency>
<groupId>org.ff4j</groupId>
<artifactId>ff4j-aop</artifactId>
<version>2.0.0</version>
</dependency>
2.定义FF4j对象
@Configuration
public class FF4jConfig {
@Bean
public FF4j ff4j() {
FF4j ff4j = new FF4j();
// 功能标志将在内存中进行存储,通常用于开发和测试环境。在生产环境中,你可能会选择不同的存储引擎,如数据库。
ff4j.setFeatureStore(new InMemoryFeatureStore());
// 属性信息,也是在内存中进行存储的。在实际应用中,你可能需要将属性信息存储在持久性存储中。
ff4j.setPropertiesStore(new InMemoryPropertyStore());
// 事件信息。这也是在内存中进行存储的,用于记录与功能标志相关的事件。在生产环境中,你可能会选择将事件存储在更持久的存储中,以进行审计和监控。
ff4j.setEventRepository(new InMemoryEventRepository());
// 启用了审计功能,这将记录功能标志的活动。在生产环境中,审计功能对于跟踪功能标志的状态变化和使用情况非常有用。
ff4j.audit(true);
// 不存在的功能标志时,FF4J 将自动创建这些功能标志但将其禁用。这可以减轻错误处理的负担,并允许在需要时轻松添加新功能。
ff4j.autoCreate(true);
// 定义RBAC权限
//ff4j.setAuthManager(...);
// 为了减轻数据库的负担,可以定义一个缓存层,并提供多种不同的实现方式。这个缓存层可以用来临时存储和管理应用程序中的数据,以便在需要时可以更快地访问,而不必每次都访问数据库。不同的实现方式可以根据应用程序的需求和性能要求来选择,以提高数据库访问的效率和响应速度。
//ff4j.cache([a cache Manager]);
return ff4j;
}
}
3.开关控制示例
@Autowired
private FF4j ff4j;
// 测试用,初始化开关
if (!ff4j.exist(FEATURE_SHOW_WEBCONSOLE)) {
ff4j.createFeature(new Feature(FEATURE_SHOW_WEBCONSOLE, true));
}
if (!ff4j.exist(FEATURE_SHOW_REST_API)) {
ff4j.createFeature(new Feature(FEATURE_SHOW_REST_API, true));
}
if (!ff4j.exist(FEATURE_SHOW_USERNAME)) {
ff4j.createFeature(new Feature(FEATURE_SHOW_USERNAME, true));
}
if (!ff4j.getPropertiesStore().existProperty(PROPERTY_USERNAME)) {
ff4j.createProperty(new PropertyString(PROPERTY_USERNAME, "cedrick"));
}
// 业务代码
@RestController
public class MyController {
@Autowired
private FF4j ff4j;
@GetMapping("/hello")
public String sayHello() {
// 检查功能标志是否启用
if (ff4j.check("my-feature")) {
return "Hello, Feature is enabled!";
} else {
return "Hello, Feature is disabled!";
}
}
}
常用Api
// When: init
FF4j ff4j = new FF4j("ff4j.xml");
// Then
assertEquals(5, ff4j.getFeatures().size());
assertTrue(ff4j.exist("hello"));
assertTrue(ff4j.check("hello"));
// Usage
if (ff4j.check("hello")) {
// hello is toggle on
System.out.println("Hello World !!");
}
// When: Toggle Off
ff4j.disable("hello");
// Then: expected to be off
assertFalse(ff4j.check("hello"));
// Given: feature not exist
FF4j ff4j = new FF4j("ff4j.xml");
assertFalse(ff4j.exist("does_not_exist"));
// When: use it
try {
if (ff4j.check("does_not_exist")) fail();
} catch (FeatureNotFoundException fnfe) {
// Then: exception
System.out.println("By default, feature not found throw exception");
}
// When: using autocreate
ff4j.setAutocreate(true);
// Then: no more exceptions
if (!ff4j.check("does_not_exist")) {
System.out.println("Auto created and set as false");
}
// Given: 2 features 'off' within existing group
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"));
// When: toggle group on
ff4j.getStore().enableGroup("sprint_3");
// Then: all features on
assertTrue(ff4j.check("userStory3_1"));
assertTrue(ff4j.check("userStory3_2"));
功能开关管理端
如果您的机器上已经安装了 Docker,您可以使用以下命令运行示例,它将在 http://localhost:8080 上启动:
docker run -p 8080:8080 ff4j/ff4j-sample-springboot2x:1.8.5
这将启动示例应用程序,并将其映射到本地的端口 8080。您可以在浏览器中访问 http://localhost:8080 来查看示例应用程序。
可以按照如下引入后自定义
<ff4j.version>1.8.6</ff4j.version>
<dependency>
<groupId>org.ff4j</groupId>
<artifactId>ff4j-spring-boot-starter</artifactId>
<version>${ff4j.version}</version>
</dependency>
<dependency>
<groupId>org.ff4j</groupId>
<artifactId>ff4j-web</artifactId>
<version>${ff4j.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf</artifactId>
</dependency>
就是一个web服务,提供管理功能
高级
面向切面 AOP
使用侵入式测试语句
if (ff4j.check("featA")) {
// new code
} else {
// legacy
}
这种方法在源代码中会比较入侵性。您可以在源代码中嵌套不同的功能切换,但可能需要定期清理代码并删除不再使用的功能。一个很好的替代方法是依赖注入,也称为控制反转(IoC),以在运行时选择正确的服务实现。
FF4j提供了@Flip注解,可通过AOP代理在方法上执行翻转。在运行时,目标服务由FF4j组件代理,根据功能状态(启用/禁用)选择一种实现而不是另一种实现。它利用了Spring AOP框架。
这种方法允许您将功能开关的逻辑从业务代码中分离出来,使代码更加干净和可维护。通过使用依赖注入和AOP,您可以更灵活地管理功能的状态和实现,而无需修改源代码。这对于实现功能切换和特性管理非常有用。
这段代码示例演示了如何在项目中使用FF4j和@Flip注解来实现基于功能开关的方法切换。以下是步骤的说明:
- 首先,将
ff4j-aop
的依赖添加到您的项目中,以便使用FF4j的AOP功能。在项目的Maven或Gradle配置中添加以下依赖:
<dependency>
<groupId>org.ff4j</groupId>
<artifactId>ff4j-aop</artifactId>
<version>${ff4j.version}</version>
</dependency>
- 定义一个示例接口(
GreetingService
),并在方法上使用@Flip
注解,该注解指定了功能开关的名称(name)以及备用的Bean名称(alterBean)。
public interface GreetingService {
@Flip(name="language-french", alterBean="greeting.french")
String sayHello(String name);
}
- 定义第一个实现类(
GreetingServiceEnglishImpl
),该实现类用于在英语中打招呼,并使用@Component
注解指定Bean的名称(“greeting.english”)。
@Component("greeting.english")
public class GreetingServiceEnglishImpl implements GreetingService {
public String sayHello(String name) {
return "Hello " + name;
}
}
- 定义第二个实现类(
GreetingServiceFrenchImpl
),该实现类用于在法语中打招呼,并使用@Component
注解指定Bean的名称(“greeting.french”)。
@Component("greeting.french")
public class GreetingServiceFrenchImpl implements GreetingService {
public String sayHello(String name) {
return "Bonjour " + name;
}
}
这样,您已经定义了一个带有功能开关的接口以及两个不同的实现类,每个实现类根据功能开关的状态返回不同的打招呼消息。在运行时,FF4j会根据功能的状态选择合适的实现类来执行方法。这使得您可以动态切换方法的行为,而无需修改代码。
审计和监控
如果您已经设计了可以隔离功能的代码(这是当今非常流行的,由敏捷开发模式和史诗/故事推动的),并使用FF4j来执行开关,那么您有机会进行审计。FF4j可以记录任何功能的调用,以帮助您计算命中率(按功能、客户来源等)或构建使用直方图。
要实现这些审计功能,只需调用这些行:
ff4j.setEventRepository(<HERE YOUR EVENT_REPOSITORY DEFINITION>);
ff4j.audit(true);
在底层,FF4j 将创建一个静态的“AuditProxy”
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);
}
大多数存储提供了审计的实现,但有时在目标数据库中存储时间序列可能没有意义(例如 Neo4j)。以下是 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));
您应该能够开始在 UI 中看到指标:
缓存
为了确保最佳一致性,FF4j 的默认行为是检查每次调用方法的存储check
。有时它会引发一些性能问题(特别是如果您依赖 REST API 和远程调用)。对于某些技术,可能需要一些时间(例如 HTTP),或者其他技术无法轻松处理此命中率(例如 Neo4j)。这就是 FF4j 在存储之上提出缓存机制的原因。所实现的缓存策略被称为“缓存旁路”,它是更常用的。
已为您实现了多个缓存提供程序(EhCache、Redis、Hazelcast、Terracotta 或 Ignite、JSR107)
首先,您必须创建正确的缓存管理器:
// 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);
然后调用cache
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;
}
您现在应该能够在控制台中看到“清除缓存”按钮和相应的徽标。
这是一个说明缓存行为的单元测试。请注意,EHCACHE
仅与未分发的情况相关。
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();
}
微服务中使用
使用流程的变化
业务方:
FF4j ff4jClient = new FF4j();
ff4jClient .setFeatureStore(new FeatureStoreHttp("http://localhost:9998/ff4j", "apiKey"));
管理端:
//[..] ff4j definition
ApiConfig apiCfg= new ApiConfig(ff4j);
apiCfg.setAuthenticate(true);
apiCfg.setAutorize(true);
// Sample to Create APIKey
boolean aclAllowRead = true;
boolean aclAllowWrite = true;
Set < String > setofRoles = new HashSet<>();
setofRoles .add("USER");
setofRoles .add("ADMIN");
apiCfg.createApiKey("sampleAPIKey1234567890", aclAllowRead , aclAllowWrite , setofRoles );
// Sample to Create User/password
apiCfg.createUser("myLogin", "myPassword", aclAllowRead , aclAllowWrite , setofRoles );
-
https://stackoverflow.com/questions/51555453/ff4j-rest-endpoint-as-a-feature-store
-
org.ff4j.web.jersey2.store.FeatureStoreHttp;FeatureStoreHttp
和
PropertyStoreHttp -
https://github.com/ff4j/ff4j/blob/8d04e5fc30a0a67b7ec5bf5a03627e11ebb2fefb/ff4j-webapi-jersey2x/src/main/java/org/ff4j/web/jersey2/store/FeatureStoreHttp.java#L67
概念
功能 Feature
功能术语用于表示应用程序中的功能或处理。它通过唯一标识符(UID)来识别。功能切换的目的是在运行时启用和禁用功能,并为每个功能维护一个状态,即布尔标志。在FF4J中,功能是一个具有多个额外属性的对象:
- 用于解释目的的文本描述(description )
- 可选的groupName,用于一次切换多个功能(FeatureGroup)
- 可选的权限集,以实施RBAC访问(Permissions)
- 可选的开关策略,以实现您的谓词(FlippingStrategy)
- 名为customProperties的键/值映射,用于创建一些上下文(key/value map)
代码示例
实际上Feature的创建是在ADMIN端,通过控制台或者页面由管理员创建,由使用者申请。
Feature f1 = new Feature("f1");
Feature f2 = new Feature("f2", false, "示例描述");
// 演示权限
Set<String> permissions = new HashSet<String>();
permissions.add("BETA-TESTER");
permissions.add("VIP");
Feature f3 = new Feature("f3", false, "示例描述", "GROUP_1", permissions);
// 创建自定义属性
Feature f4 = new Feature("f4");
f4.addProperty(new PropertyString("p1", "v1"));
f4.addProperty(new PropertyDouble("pie", Math.PI));
f4.addProperty(new PropertyInt("myAge", 12));
// 使用ReleaseDate翻转策略
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()));
// 使用DarkLaunch翻转策略
Feature f6 = new Feature("f6");
f6.setFlippingStrategy(new DarkLaunchStrategy(0.2d));
// 使用白名单翻转策略
Feature f7 = new Feature("f7");
f7.setFlippingStrategy(new WhiteListStrategy("localhost"));
功能存储 FeatureStore
FeatureStore是用于存储功能及其属性和状态的存储技术。您将找到用于与功能交互的CRUD操作和实用方法(如createSchema、grantPermissions等)。针对不同技术,这里有数十种不同的实现方式,如详细介绍所示。
InMemoryFeatureStore fStore = new InMemoryFeatureStore();
// 对功能进行操作
fStore.create(f5); // 创建功能f5
fStore.exist("f1"); // 检查功能"f1"是否存在
fStore.enable("f1"); // 启用功能"f1"
// 对权限进行操作
fStore.grantRoleOnFeature("f1", "BETA"); // 授予角色"BETA"对功能"f1"的权限
// 对组进行操作
fStore.addToGroup("f1", "g1"); // 将功能"f1"添加到组"g1"
fStore.enableGroup("g1"); // 启用组"g1"
Map<String, Feature> groupG1 = fStore.readGroup("g1"); // 读取组"g1"的所有功能
// 读取所有信息
Map<String, Feature> mapOfFeatures = fStore.readAll(); // 读取所有功能的信息
属性 Property
属性(Property)是表示任何类型的值或配置的实体。它具有唯一的名称和一个值。该值可以是任何类型,这就是为什么在ff4j中,我们选择使用泛型 Property<?> 来实现它的原因。对于每种Java原始类型,都有一个Property实现,如下面的示例所示。还有一些其他属性,例如描述(description)和可选的有效值集合(‘fixedValues’)。
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");
属性存储 PropertyStore
PropertyStore pStore = new InMemoryPropertyStore();
// CRUD
pStore.existProperty("a"); // 检查属性"a"是否存在
pStore.createProperty(new PropertyDate("a", new Date())); // 创建属性"a"并设置其值为日期
Property<Date> pDate = (Property<Date>) pStore.readProperty("a"); // 读取属性"a"的值
pDate.setValue(new Date()); // 设置属性"a"的值为新日期
pStore.updateProperty(pDate); // 更新属性"a"
pStore.deleteProperty("a"); // 删除属性"a"
// 多个操作
pStore.clear(); // 清空属性存储
pStore.readAllProperties(); // 读取所有属性
pStore.listPropertyNames(); // 列出所有属性名称
// 与FeatureStore一样,你不应该直接使用PropertyStore类,而应该仅使用FF4J。然而,要访问PropertyStore,你可以按照以下方式进行:
// 访问Property Store(包括其所有代理:Audit、Cache、AOP等)
PropertyStore pStore1 = ff4j.getPropertiesStore();
// 访问属性存储的具体类和实现
PropertyStore pStore2 = ff4j.getConcretePropertyStore();
// 请注意,属性存储的某些功能也暴露给FF4J,以提供更简便的语法:
ff4j.getProperties(); // 获取所有属性
ff4j.createProperty(new PropertyString("p1", "v1")); // 创建属性"p1"并设置其值为字符串"v1"
ff4j.getProperty("p1"); // 获取属性"p1"
ff4j.deleteProperty("p1"); // 删除属性"p1"
FF4J架构
FF4J框架设计的初衷是让您只使用 ‘org.ff4j.FF4j’ 类。然而,了解背后发生了什么是强制性的,以理解所有已实施的功能。
-
FeatureStore和PropertyStore是执行对Feature和Properties进行CRUD操作的接口。有多种实现这两个接口的方式,以便您可以在内存、数据库、缓存或NoSQL等不同的存储技术之间进行选择。
-
EventRepository是一个用于保存和搜索监视事件的接口。有多个可用于此接口的实现。为了提高性能,写入监视事件是异步进行的。EventPublisher将事件推送到阻塞队列,专用线程将调用EventRepository的’save()'方法并持久化事件。
-
如果ff4j类中设置了 ‘audit’ 标志为true,存储将使用FeatureStoreAuditProxy(用于功能)和PropertyStoreAuditProxy(用于属性)进行包装。每个操作将引发一个事件,通过EventPublisher发布到EventRepository以进行跟踪。
// 将功能使用情况发布到存储库
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:将创建操作发布到存储库
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客户端等)一起使用。它将存储包装成实现缓存旁路(cache-aside)机制:功能和属性被持久化到缓存技术中,以限制每次获取值时的开销。
该代理依赖于FF4JCacheManager。同样,有多个缓存的实现方式。为了确保在集群的多个节点之间保持一致性,建议使用分布式缓存提供程序,如Terracotta、HazelCast或Redis(即使eh-cache也可用)。
public Feature read(String featureUid) {
Feature fp = getCacheManager().getFeature(featureUid);
// 如果不在缓存中,但可能从现在开始已经创建
if (null == fp) {
fp = getTargetFeatureStore().read(featureUid);
getCacheManager().putFeature(fp);
}
return fp;
}
public void delete(String featureId) {
// 访问目标存储
getTargetFeatureStore().delete(featureId);
// 即使不存在,驱逐也不会失败
getCacheManager().evictFeature(featureId);
}
AuthorizationsManager用于处理对功能的权限。如详细说明所述,每个“Feature”可以定义一组角色,只有至少具有其中一个角色的人才能使用该功能。FF4J框架不创建角色,它依赖于专用技术,如Spring Security或Apache Shiro。
public boolean isAllowed(Feature featureName) {
// 没有授权管理器,始终返回true
if (getAuthorizationsManager() == null) {
return true;
}
// 如果没有权限,功能是公开的
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使用
初始化
要使用FF4j,您必须定义org.ff4j.FF4j bean并初始化存储。
- 如果没有明确声明代理,它将不会被启用(Cache、Audit)。
- 如果存储没有明确定义,ff4j将使用内存中的实现(features、properties、events)。
幸运的是,有一个构造函数可用于将Feature和Properties导入到内存测试中。以下是基本用法:
FF4j ff4j = new FF4j("ff4j.xml");
它也可以更加高级,如下所示。大多数情况下,此bean将通过控制反转与Spring等框架进行初始化。
// 默认构造函数
FF4j ff4j = new FF4j();
// 使用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));
// 启用审计代理
ff4j.audit();
// 启用缓存代理
ff4j.cache(new InMemoryCacheManager());
// 明确导入
XmlConfig xmlConfig = ff4j.parseXmlConfig("ff4j.xml");
ff4j.getFeatureStore().importFeatures(xmlConfig.getFeatures().values());
ff4j.getPropertiesStore().importProperties(xmlConfig.getProperties().values());
默认用法
创建了FF4J bean后,您将使用check()方法来测试功能是否已切换为启用或禁用。
FF4j ff4j = new FF4j("ff4j.xml");
if (ff4j.check("foo")) {
// 执行某些操作
}
自动创建模式
@Test
public void createFeatureDynamically() {
// 初始化为空存储
FF4j ff4j = new FF4j();
// 动态注册新功能
ff4j.create("f1").enable("f1");
// 断言功能已存在且已启用
assertTrue(ff4j.exist("f1"));
assertTrue(ff4j.check("f1"));
}
如果检查(String featureName)方法发现功能不存在,默认行为是引发FeatureNotFoundException异常,但您可以通过将autoCreate标志设置为true来覆盖:如果找不到功能,将会创建它,但切换为禁用。
@Test(expected = FeatureNotFoundException.class)
public void readFeatureNotFound() {
// 初始化
FF4j ff4j = new FF4j();
// 尝试读取不存在的功能
ff4j.getFeature("i-dont-exist");
// 预期引发错误...
}
@Test
public void readFeatureNotFoundAutoCreate() {
// 初始化
FF4j ff4j = new FF4j();
ff4j.autoCreate(true);
assertFalse(ff4j.exist("foo"));
// 尝试检查功能
ff4j.check("foo");
// 断言功能已创建但已禁用
assertTrue(ff4j.exist("foo"));
assertFalse(ff4j.check("foo"));
}
开关策略 FlippingStrategy
FlippingStrategy是实现自定义逻辑的谓词,用于评估是否应将功能视为已切换或未切换。只有在功能已启用的情况下,它们才会被评估。无论如何,它们都不会改变功能的状态。它们在以下流程图中表示为“检查规则”框:
下面是一个示例代码来理解逻辑:
public class YaFF4jTest{
@Test
public void sampleFlippingStrategy() {
// 默认情况下,内存为空。
FF4j ff4j = new FF4j();
Feature f1 = new Feature("f1", true);
ff4j.getFeatureStore().create(f1);
// 该功能已启用,没有翻转策略
Assert.assertTrue(ff4j.check("f1"));
// 添加翻转策略(yyyy-MM-dd-HH:mm),当日期到了2027-03-01-00:00后 才会是true
f1.setFlippingStrategy(new ReleaseDateFlipStrategy("2027-03-01-00:00"));
ff4j.getFeatureStore().update(f1);
// 即使功能已启用,由于策略为false...
Assert.assertFalse(ff4j.check("f1"));
}
}
常见策略
翻转策略(FlipStrategy) | 描述 |
---|---|
ReleaseDateFlipStrategy | 基于发布日期的策略,功能将在指定的日期之后启用。 |
DarkLaunchStrategy | 黑暗发布策略,控制用户对新功能的曝光率。 |
PonderationStrategy | 权重策略,基于功能的权重分配请求,用于A/B测试等情况。 |
ExpressionFlipStrategy | 表达式策略,根据自定义表达式的计算结果来控制功能的启用和禁用。 |
WhiteListStrategy | 白名单策略,只有在白名单上的主机或IP地址才能访问功能。 |
BlackListStrategy | 黑名单策略,禁止在黑名单上的主机或IP地址访问功能。 |
PropertyFlipStrategy | 属性策略,根据功能的属性值来控制启用和禁用状态。 |
CustomFlipStrategy | 自定义策略,允许开发人员实现自定义的翻转逻辑。 |
OfficeHourStrategy | 当且仅当在办公时间(假设为 09:00 至 18:00)期间发出请求时,我们才会切换功能。 |
自定义策略
功能组 Feature Groups
功能可以被组合成组。然后,可以对整个组进行切换。这种功能可能会很有用,例如,如果您想要将一个迭代中的所有“用户故事”分组在同一个发布中。这允许您在需要时一次性启用或禁用整个功能组,以简化功能管理和控制。这对于组织和跟踪大量功能时特别有用。
参考
-
https://featureflags.io/feature-flag-introduction/
-
https://github.com/ff4j/ff4j/wiki
-
https://ff4j.github.io/