如何系列 如何使用ff4j实现功能迭代

news2024/12/25 14:21:33

文章目录

    • 功能开关是什么
    • 为什么需要功能开关?
    • 功能
    • 流程
    • 组件
      • 业务接入端
        • 常用Api
      • 功能开关管理端
    • 高级
      • 面向切面 AOP
      • 审计和监控
      • 缓存
      • 微服务中使用
    • 概念
      • 功能 Feature
      • 功能存储 FeatureStore
      • 属性 Property
      • 属性存储 PropertyStore
      • FF4J架构
      • FF4J使用
      • 开关策略 FlippingStrategy
      • 功能组 Feature Groups
    • 参考

功能开关是什么

FF4J,Java的功能切换(Feature Flipping for Java),是一个用Java编写的功能切换(Feature Toggle)提案。

功能切换(也称为功能翻转、功能标志或功能位)是程序或应用程序在运行时启用和禁用功能的能力

切换可以通过编程方式、通过Web控制台、通过API、通过命令行甚至通过JMX来操作。

源代码包括多条路径,根据标志/功能的值来执行或不执行。

一个功能代表了一个潜在地跨越应用程序的各个层级,从用户界面到数据访问的业务逻辑。因此,为了实现功能切换机制,我们必须在每个层级中提供帮助。

img

为什么需要功能开关?

功能开关(Feature Toggle)是一种在运行时启用和禁用功能的机制,它提供了在不进行部署的情况下控制功能的能力。功能开关可以以编程方式、通过Web控制台、通过API、通过命令行甚至通过JMX进行操作。功能开关的核心思想是根据标志或特性的值来决定是否执行代码的不同路径。

功能开关的应用场景包括:

  1. 应对未完成功能: 如果在当前迭代中无法完成某项功能,您可以使用功能开关将其关闭,以防止未完成的功能被发布到生产环境中。

  2. 快速回滚: 如果新的功能实现或用户体验不受欢迎,您可以通过关闭功能开关迅速返回到旧的实现。

  3. A/B测试和金丝雀发布: 功能开关使得可以根据用户角色或群组来启用或禁用功能,从而支持A/B测试和金丝雀发布(Canary Release)等策略。

  4. 灵活部署: 功能开关允许您动态启用或禁用功能,而无需重新部署应用程序。

  5. 监控和审计: 通过功能开关,您可以监控功能的使用情况并记录事件,以生成仪表板或分析功能的使用趋势。

  6. 快速切换功能分支: 结合功能开关和功能分支可以更灵活地管理代码的版本和功能切换。

总之,功能开关是一种强大的工具,可以帮助团队更灵活地管理和发布功能,以满足不同的需求和情况。通过控制功能的启用和禁用,您可以更安全、更可控地进行软件开发和部署。

功能

  • 功能切换(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注解来实现基于功能开关的方法切换。以下是步骤的说明:

  1. 首先,将ff4j-aop的依赖添加到您的项目中,以便使用FF4j的AOP功能。在项目的Maven或Gradle配置中添加以下依赖:
<dependency>
  <groupId>org.ff4j</groupId>
  <artifactId>ff4j-aop</artifactId>
  <version>${ff4j.version}</version>
</dependency>
  1. 定义一个示例接口(GreetingService),并在方法上使用@Flip注解,该注解指定了功能开关的名称(name)以及备用的Bean名称(alterBean)。
public interface GreetingService {
 @Flip(name="language-french", alterBean="greeting.french")
 String sayHello(String name);
}
  1. 定义第一个实现类(GreetingServiceEnglishImpl),该实现类用于在英语中打招呼,并使用@Component注解指定Bean的名称(“greeting.english”)。
@Component("greeting.english")
public class GreetingServiceEnglishImpl implements GreetingService {
 
 public String sayHello(String name) {
  return "Hello " + name;
 }
}
  1. 定义第二个实现类(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 中看到指标:

img img

缓存

为了确保最佳一致性,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);

然后调用cacheff4j中的方法:

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

您现在应该能够在控制台中看到“清除缓存”按钮和相应的徽标。img

这是一个说明缓存行为的单元测试。请注意,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;FeatureStoreHttpPropertyStoreHttp

  • 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是实现自定义逻辑的谓词,用于评估是否应将功能视为已切换或未切换。只有在功能已启用的情况下,它们才会被评估。无论如何,它们都不会改变功能的状态。它们在以下流程图中表示为“检查规则”框:

img

下面是一个示例代码来理解逻辑:

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/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1068721.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【赠书活动】如何让AI在企业多快好省的落地

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;公众号&#x1f448;&#xff1a;测试开发自动化【获取源码商业合作】 &#x1f449;荣__誉&#x1f448;&#xff1a;阿里云博客专家博主、5…

优化理论笔记

目录 一、前言 二、优化问题的基本要素 三、优化问题分类 四、最优值类型 五、最优化方法分类 六、非约束优化 1、问题定义 2、优化算法 1&#xff09;一般局部搜索过程 2&#xff09;集束搜索 3&#xff09;禁忌搜索 4&#xff09;模拟退火 5&#xff09;蛙跳算法…

使用Resnet进行图像分类训练

本文仅给出最基础的baseline进行图像分类训练&#xff0c;后续可在此代码基础上对模型结构进行修改。 一、图像分类数据集 现有一份图像类别数据集&#xff0c;类别为Y和N&#xff0c;数据目录如下&#xff1a; /datasets/data/ |-- train/ | |-- Y/ | |-- N/划分训练集…

超自动化加速落地,助力运营效率和用户体验显著提升|爱分析报告

RPA、iPaaS、AI、低代码、BPM、流程挖掘等在帮助企业实现自动化的同时&#xff0c;也在构建一座座“自动化烟囱”。自动化工具尚未融为一体&#xff0c;协同价值没有得到释放。Gartner于2019年提出超自动化&#xff08;Hyperautomation&#xff09;概念&#xff0c;主要从技术组…

法律战爆发:“币安退出俄罗斯引发冲击波“

币安是全球最大的加密货币交易所之一&#xff0c;经历了几个月的艰难时期&#xff0c;面临着各种法律挑战&#xff0c;最近将其俄罗斯分公司的所有资产出售给了一家几天前才成立的公司。 这家主要交易所的麻烦始于 6 月份&#xff0c;当时美国证券交易委员会 (SEC)起…

PyTorch Lightning - LightningModule 训练逻辑 (training_step) 异常处理 try-except

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/133673820 在使用 LightningModule 框架训练模型时&#xff0c;因数据导致的训练错误&#xff0c;严重影响训练稳定性&#xff0c;因此需要使用 t…

消费者的力量:跨境电商如何满足新一代的需求

当代跨境电商行业正处于高速发展的阶段&#xff0c;而新一代消费者正在塑造这一行业的未来。他们的需求和消费行为发生了巨大变化&#xff0c;对于跨境电商来说&#xff0c;满足这一新一代消费者的需求至关重要。本文将探讨新一代消费者的需求以及跨境电商如何满足这些需求的方…

【Bond随你温故Azure Architecture】之HADR篇

上次复盘数据保护策略还是在《数据需要找回怎么办&#xff1f;我们如何选择正确的恢复/退回方式&#xff1f;》探讨了在application&DB层面上&#xff0c;不同level的数据保护有不同策略。而它也恰好是今天HA&DR版图的一角&#xff08;RDBMS部分&#xff09;&#xff0…

【机器学习】svm

参考 sklearn中SVC中的参数说明与常用函数_sklearn svc参数-CSDN博客https://blog.csdn.net/transformed/article/details/90437821 参考PYthon 教你怎么选择SVM的核函数kernel及案例分析_clfsvm.svc(kernel)-CSDN博客https://blog.csdn.net/c1z2w3456789/article/details/10…

【Python_PySide2学习笔记(十六)】多行文本框QPlainTextEdit类的的基本用法

多行文本框QPlainTextEdit类的的基本用法 前言正文1、创建多行文本框2、多行文本框获取文本3、多行文本框获取选中文本4、多行文本框设置提示5、多行文本框设置文本6、多行文本框在末尾添加文本7、多行文本框在光标处插入文本8、多行文本框清空文本9、多行文本框拷贝文本到剪贴…

什么是EJB以及和Spring Framework的区别

&#x1f454; 前言 EJB&#xff0c;对于新生代程序员来说&#xff0c;是一个既熟悉又陌生的名词&#xff0c;EJB&#xff0c;大家都听说过&#xff0c;但是不一定都了解过&#xff0c;EJB是一种开发规范&#xff0c;而不是像Spring Framework一样是一个开源框架&#xff0c;E…

卫星/RedCap/高算力/解决方案/创新金奖……移远通信为IOTE 2023再添新活力

9月20日&#xff0c;IOTE 2023第二十届国际物联网展深圳场震撼来袭。 作为IOTE多年的“老朋友”&#xff0c;移远通信在参展当天&#xff0c;不仅有5G RedCap、卫星通信、高算力、车载等高性能产品及终端展出&#xff0c;还携智慧出行、智慧生活、智慧能源、工业互联网等多领域…

redis集群是符合cap中的ap还是cp

近期忽然间考虑到了这个问题。 cap 理论 cap是实现分布式系统的思想。 由3个元素组成。 Consistency&#xff08;一致性&#xff09; 在任何对等 server 上读取的数据都是最新版&#xff0c;不会读取出旧数据。比如 zookeeper 集群&#xff0c;从任何一台节点读取出来的数据…

SpringBoot 如何配置 OAuth2 认证

在Spring Boot中配置OAuth2认证 OAuth2是一种用于授权的开放标准&#xff0c;允许应用程序安全地访问用户的资源。Spring Boot提供了强大的支持&#xff0c;使得在应用程序中配置OAuth2认证变得非常容易。本文将介绍如何在Spring Boot中配置OAuth2认证&#xff0c;以便您可以在…

ThreeJS-3D教学六-物体位移旋转

之前文章其实也有涉及到这方面的内容&#xff0c;比如在ThreeJS-3D教学三&#xff1a;平移缩放物体沿轨迹运动这篇中&#xff0c;通过获取轨迹点物体动起来&#xff0c;其它几篇文章也有旋转的效果&#xff0c;本篇我们来详细看下&#xff0c;另外加了tween.js知识点&#xff0…

基于SpringBoot的靓车汽车销售网站

目录 前言 一、技术栈 二、系统功能介绍 用户信息管理 车辆展示管理 车辆品牌管理 用户交流管理 购物车 用户交流 我的订单管理 三、核心代码 1、登录模块 2、文件上传模块 3、代码封装 前言 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息系统的…

除静电离子风刀的工作原理及应用

除静电离子风刀是一种能够产生高速气流并带有离子的设备&#xff0c;主要用于去除物体表面的静电。它的工作原理是通过离子产生器产生大量负离子&#xff0c;并通过高压电场将离子加速&#xff0c;使其成为一股高速气流&#xff0c;从而将静电荷从物体表面中除去。 除静电离子…

阿里云 linux tomcat 无法访问方法

1、阿里云放行tomcat端口 例如7077端口号 2、linux 命令行防火墙 设置端口打开 以下命令查看是否开启指定端口 firewall-cmd --list-ports以下命令添加指定端口让防火墙放行 firewall-cmd --zonepublic --add-port3306/tcp --permanent以下命令重新启动防火墙 systemctl re…

聊一下读完“优势成长”这本书后感

&#xff08;优势成长上&#xff09; (优势成长下) 最近读完了一本个人觉得还可以的书,这本书是一位新东方老师,帅键翔老师写的 整本书概括起来,最重要一点就是找到自己的优势,然后利用自己的优势,去挖掘自己的潜力,发现新大陆 能适应时代变化的&#xff0c;是“新木桶原理”&a…

JAVA中解析package、import、class、this关键字

一、前言 代码写的多了有时候我们就慢慢忽视了最简单&#xff0c;最基本的东西。比如一个类中最常见出现的package、import、class、this关键字。我们平时很少追究它的含义或者从来不会深究为什么需要这些关键字。不需要这些关键字&#xff0c;又会怎样。这边博文就简单介绍一下…