最近一个使用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是启用的。
我们可以写一个简单的接口实现来判断:
public boolean isFeatureEnable(String featureName) {
// 用System.getenv("FeatureToggle")读取环境变量判断是否包含参数的featureName
// ...
}
进一步使用
虽然我们有了判断方法,但是因为项目组的人都有洁癖,我们不希望代码中到处都是
if(isFeatureEnable(featureA)) {
// new code
} else {
// old code
}
这样实在是太ugly了。
我们需要利用spring的IoC特性来切换implementations。Spring从4.0开始提供Conditional的注解。结合@Configuration
就可以实现app启动时的不同Bean的注入。
写一个FeatureA的Condition Class
public class FeatureACondition implements Condition{
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return isFeatureEnable("featureA")
}
}
再写一个Spring的Configuration来使用这个Condition
@Configuration
@Conditional(FeatureACondition.class)
public class FeatureAConfiguration {
@Bean(name="bizService")
public BizService bizService(){
return new EnhancedBizService();
}
}
当然如果要实现互斥的切换,即启用FeatureA另一个Bean就不能加载的话,那么再写一个NotFeatureA的Configuration就可以了。
@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
在项目启动入口
@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
实现:
environment:
- FeatureToggle=FeatureA,FeatureB
- SESSION_SECRET
总而言之就是充分利用OO语言的优势,实现可拔插的FeatureToggle。接下来我们还会继续研究如何Runtime的启用Feature,我也发现了一个已有的轮子togglz。如果有朋友用过,欢迎反馈使用感受。