摘要
為了快速發布開發完成的功能,現代的互聯網企業通常會以比較快的迭代周期來持續的發布。但是有時候因為技術或者業務上的原因,需要在發布的時候將某些功能隱藏起來。一種解決方案是,在獨立的分支上開發新功能,全部開發測試完成之后,才合并回主干,準備發布。這也就是我們經常提到的功能分支(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.anti-gravitydesign.com/deltestingadmindd/