利用Spring的Conditional注解来实现FeatureToggle

TiffinyPEJJ 3年前
   <p>最近一个使用Spring的项目中需要进行性能调优。方式基本上是编写新的代码实现原来一样的业务逻辑,只是实现方式有一些调整,例如增加cache,优化算法等等。</p>    <p>一开始大家希望直接在原有代码基础上修改,但是这样一来,就要跟上每周一次的发布节奏,一周搞定难度太大。于是决定拷贝出的package来重构。在没启用之前这个package下都是dead code。这样做的好处有几点:</p>    <ul>     <li>在调优后的code启用前,业务至少不会受影响。</li>     <li>利用docker的特性,可以实现灰度发布,比如启动两个docker,一个是老的code,一个启用新的code,利用nginx实现分流。</li>     <li>灰度发布后发现有紧急bug,只需要devOps修改一点配置,重启docker可以再切回老的code。</li>    </ul>    <h3><strong>出发点</strong></h3>    <p>既然要实现上述第三点,也就是利用配置来实现切换,那么这个Enable的flag就不应该写到代码里,甚至是配置文件里,因为项目启动都是在docker中通过spring-boot的cmd直接启动的。DevOps是不允许进入docker进行操作的。</p>    <h3><strong>实现</strong></h3>    <p>想到我们的整个部署架构是基于Kubernetes的,可以通过修改工程的deployment.yaml文件来实现。原理就是deployment里面设置一个docker的Env,Key是 FeatureToggle ,Value可以是这样 FeatureA,FeatureB ,当docker启动时,JVM(Java代码)可以通过 System.getenv() 来获得环境变量,从来知道这个Feature是需要启用还是不启用。如上的写法表示FeatureA和FeatureB是启用的。</p>    <p>我们可以写一个简单的接口实现来判断:</p>    <pre>  <code class="language-java">publicbooleanisFeatureEnable(String featureName){  // 用System.getenv("FeatureToggle")读取环境变量判断是否包含参数的featureName  // ...  }  </code></pre>    <h3><strong>进一步使用</strong></h3>    <p>虽然我们有了判断方法,但是因为项目组的人都有洁癖,我们不希望代码中到处都是</p>    <pre>  <code class="language-java">if(isFeatureEnable(featureA)) {  // new code  } else{  // old code  }  </code></pre>    <p>这样实在是太ugly了。</p>    <p>我们需要利用spring的IoC特性来切换implementations。Spring从4.0开始提供 <a href="/misc/goto?guid=4959723312263837316" rel="nofollow,noindex">Conditional</a> 的注解。结合 @Configuration 就可以实现app启动时的不同Bean的注入。</p>    <p><strong>写一个FeatureA的Condition Class</strong></p>    <pre>  <code class="language-java">publicclassFeatureAConditionimplementsCondition{    @Override  publicbooleanmatches(ConditionContext context, AnnotatedTypeMetadata metadata){  returnisFeatureEnable("featureA")   }  }  </code></pre>    <p><strong>再写一个Spring的Configuration来使用这个Condition</strong></p>    <pre>  <code class="language-java">@Configuration  @Conditional(FeatureACondition.class)  publicclassFeatureAConfiguration{    @Bean(name="bizService")  publicBizServicebizService(){  returnnewEnhancedBizService();   }    }  </code></pre>    <p>当然如果要实现互斥的切换,即启用FeatureA另一个Bean就不能加载的话,那么再写一个NotFeatureA的Configuration就可以了。</p>    <pre>  <code class="language-java">@Configuration  @Conditional(NotFeatureACondition.class)  publicclassNotFeatureAConfiguration{    @Bean(name="bizService")  publicBizServicebizService(){  returnnewOldBizService();   }    }  </code></pre>    <p>这样一来,当FeatureA启用时BizService这个interface的实现就是EnhancedBizService,反之它的实现就是OldBizService。</p>    <p>当然你在configuration上用 @ComponentScan , @Import 等等都是没问题的,在启动时都会最先判断Conditional,如果不满足spring根本不会继续下面的扫描或者加载操作。</p>    <p><strong>最后启用这两个Config</strong></p>    <p>在项目启动入口</p>    <pre>  <code class="language-java">@SpringBootApplication  @Import({NotFeatureAConfiguration.class, FeatureAConfiguration.class})  publicclassApplication{    publicstaticvoidmain(String[] args){   SpringApplication.run(Application.class, args);   }    }  </code></pre>    <h3><strong>小结</strong></h3>    <p>通过上述几步,在spring项目启动时通过conditional注解的条件判断,实现不同Bean的装配,从而启用不同的Feature。</p>    <p>对于Devops而言,只需要在deployment里面修改Env的内容,再重启deploy这个app就可以实现Feature Toggle了。即使你不使用Kubernetes,docker-compose也是一样的道理。</p>    <p>通过修改 docker-compose.yml 实现:</p>    <pre>  <code class="language-java">environment:   - FeatureToggle=FeatureA,FeatureB   - SESSION_SECRET  </code></pre>    <p>总而言之就是充分利用OO语言的优势,实现可拔插的FeatureToggle。接下来我们还会继续研究如何Runtime的启用Feature,我也发现了一个已有的轮子 togglz 。</p>    <p> </p>    <p>来自:http://www.deanwangpro.com/2016/10/30/spring-featuretoggle/</p>    <p> </p>