在SpringBoot2中開發特徵切換功能
翻譯:吳嘉俊,叩丁狼高階講師。
眾所周知,軟體開發是一個協同的活動。開發中的整合工作往往都被視為邪惡的,開發人員會像魔鬼一樣對待這個過程。為了應付這種情況,有非常多的方法和規則被開發出來。特徵切換就是其中之一。在本文中,你將會看到什麼是特徵切換,瞭解特徵標記,以及如何在你的Springboot應用中使用。
什麼是特徵切換
簡單來說,特徵切換是控制程式按照某種流程執行的一個變數開關。不用修改程式碼,就能切換到不同的執行場景的方式。
根據自身需求,用於特徵切換的變數值可以在應用啟動的時候就設定好,或者在執行期間根據需要進行調整。針對後一種情況,只需要把特徵切換變數值持久化下來,在實際執行的時候去獲取即可。
你應該瞭解過特徵標誌的另一種實現方案:按照特徵程式碼分支。在實際的開發中,這兩種技術可以組合起來使用。比如,針對一個新的使用者故事(User Story),可以使用特徵分支,同時,也可以使用特徵切換來控制在針對不同的場景的使用者需求。
不過,雖然特徵切換有很多適用場景,但是它仍然有自己的缺點。最大的一個問題就在於它的複雜性。如果沒有一個良好的管理策略,特徵切換會很容易失去控制,變成專案的一個噩夢。幸運的是,如果你遵循幾個最佳實踐,使用特徵切換會變得非常容易。
使用特徵切換來選擇合適的Bean
在Springboot的應用中,使用特徵切換最常見的場景是,給定一個介面,根據特徵值切換到一個不同的實現類。我們來簡單看下這種場景:
考慮一個場景:在一個web服務端中,提供了從資料庫中篩選一個貨品列表的功能。你現在的目標就是建立一個特徵切換,允許動態的選擇不同的資料庫源提供給web服務。
想要使用特徵切換,第一個要做的事情,就是把提供給其他類使用的依賴,使用介面抽象。
下面的程式碼展示了一個商品的REST介面,依賴於ProductRepository介面:
@RestController @RequestMapping("/products") class ProductController { private final ProductRepository productRepository; ProductController(ProductRepository productRepository) { this.productRepository = productRepository; } @GetMapping Collection<Product> getAll() { return productRepository.findAll(); } }
目前,我們只有一個ProductRepository的實現,接下來,當我們要新增另一個ProductRepository實現的時候,我們就需要考慮特徵切換了。
@Repository
class DbProductRepository implements ProductRepository {
//...
}
在application.properties中新增特徵切換配置
我們知道Springboot應用使用application.properties檔案來完成配置。這個檔案是一個非常好的用於放置特徵標誌的地方。
feature.toggles.productsFromWebService=true
在提交程式碼的時候,設定該標誌值為false。在這種情況下,團隊組員預設情況下,是關閉了這個功能的。如果誰想使用這個功能,只需要在本地開發環境中,把值設定為true即可。
下一步要做的就是建立另一個ProductRepository實現,並且這個實現通過特徵切換來啟用。你可以使用Springboot提供的@ConditionalOnProperty註解來實現bean和配置檔案中的屬性的繫結,只需要在註解中設定好配置的屬性名稱和值即可。
@Repository
@ConditionalOnProperty(
name = "feature.toggles.productsFromWebService",
havingValue = "true"
)
class WebServiceProductRepository implements ProductRepository {
//...
}
在啟動應用之前,先把DbProductRepository禁用,否則,我們就會得到一個“multiple active implementations of the interface”的異常。我們回到之前的DbProductRepository的實現,同樣修改程式碼:
@Repository
@ConditionalOnProperty(
name = "feature.toggles.productsFromWebService",
havingValue = "false",
matchIfMissing = true
)
class DbProductRepository implements ProductRepository {
兩個實現類使用同一個特徵切換名稱(feature.toggles.productsFromWebService),只是對應的值做了區分。設定matchIfMissing屬性值是可選的,這個配置表明,即使在application.properties檔案中刪除了feature.toggles.productsFromWebService這個配置項,DbProductRepository實現會作為預設實現被選中。
使用特徵切換控制Controller
當然,我們可以使用相同的策略來控制所有的Controller。你不需要建立一個額外的介面,因為對Controller的控制,只需要切換某一個Controller是否可用。
@RestController
@RequestMapping("/coupons")
@ConditionalOnProperty(name = "feature.toggles.coupons", havingValue = "true")
class CouponController {
//...
}
在application.properties檔案裡面應該包含如下內容:
feature.toggles.coupons=true
如果這個值設定為false,那麼SpringMVC不會去初始化這個Controller,如果訪問/coupons,得到404異常。
可惜的是,@ConditionalOnProperty註解不能用於單個的方法之上,所以,如果想要單獨控制某個請求,只能將這個請求方法獨立到一個Controller中。另一種方案,我們可以把特徵切換值直接注入到請求方法引數列表中,通過該值來控制。但是這種方案需要及其小心,後面會介紹。
private final boolean couponsToggled;
CouponController(@Value("${feature.toggles.coupons}") boolean couponsToggled) {
this.couponsToggled = couponsToggled;
}
@GetMapping
List<String> listCouponNames() {
if (!couponsToggled) {
throw new NotSupportedException();
}
//...
}
多個特徵切換管理
As you can read about feature toggles on Martin Fowler’s bliki, feature flags have a tendency to spread across the codebase and can quickly get unmanageable. Even if you have just a few feature toggles in your application, it’s better to abstract the storage of your flags from decision points in which they are used.
在Martin Flower的部落格中有提到過,特徵標誌極有可能貫穿整個程式碼庫,並且容易快速趨於不可控。所以,即使你應用中只有幾個特徵切換,最好把特徵標誌從具體的應用點抽取出來。
CouponController(@Value("${feature.toggles.coupons}") boolean couponsToggled) {
this.couponsToggled = couponsToggled;
}
在上文這段程式碼案例中,我們是直接把標誌值從application.properties檔案中注入到方法中,並沒有抽象出任何的儲存層。如果你在應用其他地方同樣使用了這個標記(比如另一個.properties檔案),那麼這個注入會出現衝突的異常。
作為替換的方案,我們可以把所有的特徵值全部放到一個單獨的類中,把這個類作為唯一的依賴。使用額外的一個類,會給與應用更多的靈活性,比如你可以在這個類中,從資料庫中載入特徵切換值,而不用從.properties檔案中獲取,這樣我們就可以做到在執行時動態切換特徵。
當你把特徵切換值抽取到獨立的bean中,我們就可以使用@ConfigurationProperties註解輕鬆的從application.properties中載入所有的值。下面是一個簡單的示例:
@Component
@ConfigurationProperties("feature")
public class FeatureDecisions {
private Map<String, Boolean> toggles = new HashMap<>();
public Map<String, Boolean> getToggles() {
return toggles;
}
public boolean couponEnabled() {
return toggles.getOrDefault("coupons", false);
}
}
上面的程式碼中,我們會把所有application.properties檔案中以feature.toggles開頭的值自動注入到toggles這個Map中。在類中,我們提供了一個couponEnabled()方法來抽象出特徵切換點,通過這個方法,把具體的特徵切換值的獲取邏輯封裝了起來。
當然,你需要引入一個額外的依賴來處理@ConfigurationProperties。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
在Actuator中監控特徵切換
在討論了特徵切換在普通情況下的使用,接下來我們簡單看一下在一個Actuator端點中監控我們的特徵切換狀態。如下程式碼所示:
@Component
@Endpoint(id = "feature-toggles")
class FeatureToggleInfoEndpoint {
private final FeatureDecisions featureDecisions;
FeatureToggleInfoEndpoint(FeatureDecisions featureDecisions) {
this.featureDecisions = featureDecisions;
}
@ReadOperation
public Map<String, Boolean> featureToggles() {
return featureDecisions.getToggles();
}
}
如果你使用的是Springboot2預設的Actuator設定,這個端點是不會被髮布為HTTP服務的。我們需要把這個endpoint的id新增到applicaiton.properties配置中的include filter中:
management.endpoints.web.exposure.include=health,info,feature-toggles
啟動應用,訪問 http://localhost:8080/actuator/feature-toggles地址,就可以看到我們的特徵切換值狀態了:
當然,根據自己的需求,我們也可以使用@WriteOperation建立一個可用於在執行時修改特徵切換值的Actuator端點。這個例子中只演示了監控。
小結
在本篇文章中,你可以學到如何在Springboot應用中應用特徵切換。我們從一個最基礎的例子開始,覆蓋到了大部分的特徵切換需求。接著,我們針對一個特殊的情況,介紹了特徵值的抽取和管理,最後,我們介紹瞭如何通過釋出一個Actuator端點來監控應用的特徵切換值狀態。
原文:http://dolszewski.com/spring/feature-toggle-spring-boot/