摘要
为了快速发布开发完成的功能,现代的互联网企业通常会以比较快的迭代周期来持续的发布。但是有时候因为技术或者业务上的原因,需要在发布的时候将某些功能隐藏起来。一种解决方案是,在独立的分支上开发新功能,全部开发测试完成之后,才合并回主干,准备发布。这也就是我们经常提到的功能分支(feature branch)。本文将介绍如何使用功能开关(feature toggle)来更好地解决这个问题,及其在一个典型Spring web应用程序中的具体实现,最后讨论了功能开关和持续集成如何协同工作。
功能分支的问题
功能分支可以帮助我们同时开发多个新功能,而不对发布的节奏造成影响,这解决我们一开始提到的那个持续发布的需求,但是它也会引入很多问题。在Martin Fowler的文章中已经很全面的阐述了这些问题,简单总结如下:
分支分出去时间长了往主干合并的时候会出现很多的代码冲突。
在一个分支中修改了函数名字,但是如果在其它分支中大量使用修改前的函数名,则会引入大量编译错误。这点被称为语义冲突(semantic conflict)
为了减少语义冲突,会尽量少做重构。而重构是持续改进代码质量的手段。如果在开发的过程中持续不断的存在功能分支,就会阻碍代码质量的改进。
一旦代码库中存在了分支,也就不再是真正的持续集成了。当然你可以给每个分支建立一个对应的CI,但它只能测试当前分支的正确性。如果在一个分支中修改了函数功能,但是在另一个分支还是按照原来的假设在使用,在合并的时候会引入bug,需要大量的时间来修复这些bug。
功能开关
第一原则,代码库中不再引入任何分支,所有的代码都提交到同一个主线(mainline),在开始开发一个新功能的时候,引入一个布尔值的配置项,使得在该配置项为假时,应用程序的外部行为和没有引入该功能之前保持一致;而在配置项为真时,应用程序才展现出那些新开发的功能。
实现的方式也很直观。在所有跟该功能相关的代码中都会读取该配置项的值,如果配置项值为真,则使用新功能,如果为假,则保持以前的逻辑。我们把在某处代码使用到该布尔配置项称为该处代码使用了该开关。
对于一个典型的Spring的web项目,代码库中会包括Java代码、JSP代码,IOC配置文件,还有CSS和JS文件。这些都是代码,根据不同的业务需求,这些代码都有可能会用到开关。为了能够在这些代码中方便地获取开关的值,使用开关,我们需要一些基础设施来支持。
如上图所示。需要在“功能开关”的模块中实现所需要的基础设施,然后配合配置文件的内容来对应用程序的行为进行控制。下面我们就配置文件和基础设施做一些讨论。
功能开关配置文件
Spring中使用MessageSource来实现国际化,其本质上就是从一系列的properties文件中读取键值对。我们这里使用这些properties文件来存储功能开关的配置项,如这样的项:
featureA.isActivated=true
在MessageSource之上我们封装了一层ApplicationConfig,用来提供便利的方法(如getMessageAsBoolean等)来获取配置项的值。
功能开关基础设施
为了在代码中使用到功能开关配置文件的内容。我们需要实现一些基础设施。
Java代码中
将ApplicationConfig的实例bean注入到需要应用开关的其他bean中,然后在其它bean中读取相关配置项。这种注入可以很容易的使用Spring来完成。
JSP中
自定义一个JSP Tag来在JSP中使用配置文件中的配置项,其使用方法如下:
在调用过该tag之后,就可以使用featureValue这个变量来引用对应配置项的值了。
IOC配置文件中
在Spring的IOC配置文件中,同样可以使用自定义的Tag来动态选取bean的实例。其原理如下图所示:
类A依赖于B接口,bean1和bean2是在Spring配置文件中定义好的两个实例bean,他们的类型都是B接口的实现类,因此他们都可以被注入到A的实例bean中。通过开关的控制,可以把不同的实例注入到类A的实例bean中。
关于CSS和JS,我们并没有再引入更多的基础设施,通过JSP中的控制就可以完成对CSS/JS的控制。
例子一
问题:开发了一个新的功能,而该功能需要通过主页上的一个链接访问。
利用上述的基础设施,可以这么实现:
在资源文件中定义该功能开关的状态。
//feature-config.properties
show.link.feature=true
在JSP中使用自定义的ns:config Tag来读取配置项的值,根据该值决定是否显示链接。
//index.jsp
link to new function
在Controller代码中读取开关的值,如果开关状态为关闭,则在访问该功能时直接返回404。
//NewFunctionController.java
......
protected ModelAndView handle(HttpServletRequest request, HttpServletResponse
response, Object command, BindException bindingResult) throws Exception {
if(!applicationConfig.getMessageAsBoolean("show.link.feature")) {
return new ModelAndView("404.jsp");
}
//normal logic
}
......
例子二
问题:我们的产品已经在使用google map API V2的服务,现在要升级到V3。
首先还是要引入一个功能配置项:feature.googleV3Service.isActivated。
google map API V2相关的逻辑全部存在于一个具体类型GoogleMapV2Service中。而SearchLocationService直接依赖于GoogleMapV2Service这个具体类型,现在为了方便替换,引入一个接口作为抽象层。
原文转自:http://www.ltesting.net/deltestingadmindd/