利用Spring的Conditional注解来实现FeatureToggle

最近一个使用Spring的项目中需要进行性能调优。方式基本上是编写新的代码实现原来一样的业务逻辑,只是实现方式有一些调整,例如增加cache,优化算法等等。

一开始大家希望直接在原有代码基础上修改,但是这样一来,就要跟上每周一次的发布节奏,一周搞定难度太大。于是决定拷贝出的package来重构。在没启用之前这个package下都是dead code。这样做的好处有几点:

  • 在调优后的code启用前,业务至少不会受影响。
  • 利用docker的特性,可以实现灰度发布,比如启动两个docker,一个是老的code,一个启用新的code,利用nginx实现分流。
  • 灰度发布后发现有紧急bug,只需要devOps修改一点配置,重启docker可以再切回老的code。

出发点

既然要实现上述第三点,也就是利用配置来实现切换,那么这个Enable的flag就不应该写到代码里,甚至是配置文件里,因为项目启动都是在docker中通过spring-boot的cmd直接启动的。DevOps是不允许进入docker进行操作的。

实现

想到我们的整个部署架构是基于Kubernetes的,可以通过修改工程的deployment.yaml文件来实现。原理就是deployment里面设置一个docker的Env,Key是FeatureToggle,Value可以是这样FeatureA,FeatureB ,当docker启动时,JVM(Java代码)可以通过System.getenv()来获得环境变量,从来知道这个Feature是需要启用还是不启用。如上的写法表示FeatureA和FeatureB是启用的。

我们可以写一个简单的接口实现来判断:

1
2
3
4
public boolean isFeatureEnable(String featureName) {
// 用System.getenv("FeatureToggle")读取环境变量判断是否包含参数的featureName
// ...
}

进一步使用

虽然我们有了判断方法,但是因为项目组的人都有洁癖,我们不希望代码中到处都是

1
2
3
4
5
if(isFeatureEnable(featureA)) {
// new code
} else {
// old code
}

这样实在是太ugly了。

我们需要利用spring的IoC特性来切换implementations。Spring从4.0开始提供Conditional的注解。结合@Configuration就可以实现app启动时的不同Bean的注入。

写一个FeatureA的Condition Class

1
2
3
4
5
6
7
public class FeatureACondition implements Condition{

@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return isFeatureEnable("featureA")
}
}

再写一个Spring的Configuration来使用这个Condition

1
2
3
4
5
6
7
8
9
10
@Configuration
@Conditional(FeatureACondition.class)
public class FeatureAConfiguration {

@Bean(name="bizService")
public BizService bizService(){
return new EnhancedBizService();
}

}

当然如果要实现互斥的切换,即启用FeatureA另一个Bean就不能加载的话,那么再写一个NotFeatureA的Configuration就可以了。

1
2
3
4
5
6
7
8
9
10
@Configuration
@Conditional(NotFeatureACondition.class)
public class NotFeatureAConfiguration {

@Bean(name="bizService")
public BizService bizService(){
return new OldBizService();
}

}

这样一来,当FeatureA启用时BizService这个interface的实现就是EnhancedBizService,反之它的实现就是OldBizService。
当然你在configuration上用@ComponentScan,@Import等等都是没问题的,在启动时都会最先判断Conditional,如果不满足spring根本不会继续下面的扫描或者加载操作。

最后启用这两个Config
在项目启动入口

1
2
3
4
5
6
7
8
9
@SpringBootApplication
@Import({NotFeatureAConfiguration.class, FeatureAConfiguration.class})
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

}

小结

通过上述几步,在spring项目启动时通过conditional注解的条件判断,实现不同Bean的装配,从而启用不同的Feature。
对于Devops而言,只需要在deployment里面修改Env的内容,再重启deploy这个app就可以实现Feature Toggle了。即使你不使用Kubernetes,docker-compose也是一样的道理。
通过修改docker-compose.yml实现:

1
2
3
environment:
- FeatureToggle=FeatureA,FeatureB
- SESSION_SECRET

总而言之就是充分利用OO语言的优势,实现可拔插的FeatureToggle。接下来我们还会继续研究如何Runtime的启用Feature,我也发现了一个已有的轮子togglz。如果有朋友用过,欢迎反馈使用感受。