# Switch管理平台

有时候,一段业务逻辑往往需要保存两种实现(逻辑),而是否进行逻辑切换,则由业务人员或者开发运维人员负责,此时最佳实践则是利用功能开关实现逻辑切换。并且在企业内往往拥有很多系统(子系统),因此也需要对开关进行集中式的管理。

避免造轮子,本文将介绍开源的ff4j如何引入应用,并进行集中式管理。

# 如何集成switch

我们选择ff4j作为switch平台,是由于其功能轻量,源代码不复杂,并且包含web控制台。

# 引入对应依赖

演示项目使用maven集成

<dependency>
    <groupId>org.ff4j</groupId>
    <artifactId>ff4j-core</artifactId>
    <version>1.8.2</version>
</dependency>
<!--为ff4j提供aop支持-->
<dependency>
    <groupId>org.ff4j</groupId>
    <artifactId>ff4j-aop</artifactId>
    <version>1.8.2</version>
</dependency>
<!--为数据源持久化提供redis支持-->
<dependency>
    <groupId>org.ff4j</groupId>
    <artifactId>ff4j-store-redis</artifactId>
    <version>1.8.2</version>
</dependency>

# 配置ff4j实例

# 枚举实现ff4j单例
public enum FF4jSingleton {
    ff4j;

    FF4jSingleton(){
        this.instance = new FF4j().autoCreate();
        //从Spring Context获取jedisPool实例,并构造,然后注入ff4j实例中。
        JedisPool jedisPool = SpringContextUtils.getBean(JedisPool.class);
        RedisConnection redisConnection = new RedisConnection(jedisPool);
        instance.setFeatureStore(new FeatureStoreRedis(redisConnection));
        instance.setPropertiesStore(new PropertyStoreRedis(redisConnection));
        instance.setEventRepository(new EventRepositoryRedis(redisConnection));
    }

    private FF4j instance;

    public FF4j get() {
        return instance;
    }

}
# 将ff4j单例纳入spring容器管理
@Configuration("FF4jConfiguration")
@DependsOn({"redisConfig", "redisPoolFactory", "springContextUtils"})
public class FF4jConfiguration {
    @Bean("ff4j")
    public FF4j builderFF4j(){
        return FF4jSingleton.ff4j.get();
    }
}

# 如何使用ff4j

# 类中注入其实例
@Autowired
@Qualifier("ff4j")
private FF4j ff4j;
# 配置开关
if(ff4j.check(switchId)){
    //switch is on, do something...
}else{
    //switch is off, do other something...
}

# switch集中式web控制台

由于ff4j自带了web console,用于更便捷的,集中的管控switch的使用,以下将对其集成和使用做简单介绍。

# 启动web console

ff4j的web控制台是提供了一个servlet,因此需要只需要集成到一个web项目中便可使用。

使用spring集成时,需要将其注册为一个servlet即可。

# 引入依赖

<dependency>
    <groupId>org.ff4j</groupId>
    <artifactId>ff4j-core</artifactId>
    <version>1.8.2</version>
</dependency>
<!--web console依赖-->
<dependency>
    <groupId>org.ff4j</groupId>
    <artifactId>ff4j-web</artifactId>
    <version>1.8.2</version>
</dependency>
<!--与springboot集成的配置依赖-->
<dependency>
    <groupId>org.ff4j</groupId>
    <artifactId>ff4j-spring-boot-autoconfigure</artifactId>
    <version>1.8.2</version>
</dependency>
<dependency>
    <groupId>org.ff4j</groupId>
    <artifactId>ff4j-store-redis</artifactId>
    <version>1.8.2</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.1.3.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
    <version>2.1.3.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.1</version>
    <optional>true</optional>
</dependency>

# 配置

建议web console做成一个独立的平台,因此相当于单独构建一个项目,以下有重复内容,如果已经了解请自行忽略。

  1. 配置redis
  2. 构造ff4j单例实例

# 注册ff4j提供的servlet

直接上代码(spring框架):

//servlet
public class ConsoleServletProd extends ConsoleServlet {
}
public class FF4jProviderProd implements FF4jProvider {
    @Override
    public FF4j getFF4j() {
        return FF4jSingleton.FF4J_PROD.get();
    }
}
@Configuration
public class FF4jWebConfigurationProd {

    @Bean("prod")
    public ServletRegistrationBean<ConsoleServletProd> prod(){
        ServletRegistrationBean<ConsoleServletProd> consoleServletServletRegistrationBean = new ServletRegistrationBean<>(new ConsoleServletProd(), "/prod");
        consoleServletServletRegistrationBean.setInitParameters(Collections.singletonMap("ff4jProvider", "com.xxx.xxx.console.ff4j.prod.FF4jProviderProd"));
        consoleServletServletRegistrationBean.setLoadOnStartup(3);
        return consoleServletServletRegistrationBean;
    }

}

以上便完成了ff4j的web控制台集成,直接启动项目,访问'/prod'即可。

# switch控制台的使用

很简单,只是开关的控制而已,自己上摸索测试一下便知。

# switch的应用

这里简单介绍其发挥的作用,除去显而易见的业务逻辑开关功能,还可以用来控制rocketmq暂停消费。由于项目在部署期间需要避免mq正在消费导致的脏数据,因此使用switch来控制mq暂停消费。

全部代码如下:

@Component
@DependsOn("FF4jConfiguration")
@Slf4j
//实现ApplicationRunner,在Spring应用启动时执行其run()
public class OpsManager implements ApplicationRunner {

    @Autowired
    @Qualifier("ff4j")
    private FF4j ff4j;

    private static final String ALL_APP_OPS_SWITCH = "allAppOpSwitch";

    private boolean onOff = true;
    
	//缓存所有rocketmq的consumer类,其类可以控制客户端消费的启动与停止。
    private Map<String, DefaultMQPushConsumer> beansOfType;

    /**
     * 应用启动时执行
     */
     @Override
     public void run(ApplicationArguments args) {
        init();
		//创建定时线程,检查开关状态,根据开关动态改变consumer是否继续消费。
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
        executor.scheduleAtFixedRate(() -> {
            boolean currentValue = ff4j.check(ALL_APP_OPS_SWITCH);
            log.info("Ops switch running :{}", currentValue);
            if(currentValue != onOff){
                onOff = currentValue;
                beansOfType.forEach((key, value)->{
                    try {
                        if(onOff){
                            //恢复消费
                            value.resume();
                        }else{
                            //挂起消费
                            value.suspend();
                        }
                        log.info("rocket mq switch {}: {}", key, onOff);
                    }catch (Exception e){
                        log.error("OpsManager occur exception", e);
                    }
                });

            }
        }, 30, 15, TimeUnit.SECONDS);

    }

    private void init(){
        //通过Spring context获得所有consumer类实例,并缓存起来。
        beansOfType = SpringContextUtils.getBeansOfType(DefaultMQPushConsumer.class);

        while (!ff4j.check(ALL_APP_OPS_SWITCH)){
            try {
                log.info("OpsManager initial sleeping");
                TimeUnit.SECONDS.sleep(15);
            } catch (InterruptedException e) {
                log.error("OpsManager occur exception", e);
            }
        }
        this.start();
    }

    private void start(){
        beansOfType.forEach((key, value)->{
            try {
                //启动rocketmq的consumer
                value.start();
                log.info("rocket mq started: {}", key);
            }catch (Exception e){
                log.error("OpsManager occur exception", e);
            }
        });
    }

}
修改于: 8/11/2022, 3:17:56 PM