# 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做成一个独立的平台,因此相当于单独构建一个项目,以下有重复内容,如果已经了解请自行忽略。
- 配置redis
- 构造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);
}
});
}
}