利用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是啟用的。
我們可以寫一個簡單的接口實現來判斷:
publicbooleanisFeatureEnable(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
publicclassFeatureAConditionimplementsCondition{
@Override
publicbooleanmatches(ConditionContext context, AnnotatedTypeMetadata metadata){
returnisFeatureEnable("featureA")
}
}
再寫一個Spring的Configuration來使用這個Condition
@Configuration
@Conditional(FeatureACondition.class)
publicclassFeatureAConfiguration{
@Bean(name="bizService")
publicBizServicebizService(){
returnnewEnhancedBizService();
}
}
當然如果要實現互斥的切換,即啟用FeatureA另一個Bean就不能加載的話,那么再寫一個NotFeatureA的Configuration就可以了。
@Configuration
@Conditional(NotFeatureACondition.class)
publicclassNotFeatureAConfiguration{
@Bean(name="bizService")
publicBizServicebizService(){
returnnewOldBizService();
}
}
這樣一來,當FeatureA啟用時BizService這個interface的實現就是EnhancedBizService,反之它的實現就是OldBizService。
當然你在configuration上用 @ComponentScan , @Import 等等都是沒問題的,在啟動時都會最先判斷Conditional,如果不滿足spring根本不會繼續下面的掃描或者加載操作。
最后啟用這兩個Config
在項目啟動入口
@SpringBootApplication
@Import({NotFeatureAConfiguration.class, FeatureAConfiguration.class})
publicclassApplication{
publicstaticvoidmain(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 。
來自:http://www.deanwangpro.com/2016/10/30/spring-featuretoggle/