1. 程式人生 > >在SpringBoot2中開發特徵切換功能

在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即可。

建立條件控制Bean

下一步要做的就是建立另一個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 blikifeature 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檔案中獲取,這樣我們就可以做到在執行時動態切換特徵。

在Springboot中提取特徵切換值

當你把特徵切換值抽取到獨立的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地址,就可以看到我們的特徵切換值狀態了:

image.png

當然,根據自己的需求,我們也可以使用@WriteOperation建立一個可用於在執行時修改特徵切換值的Actuator端點。這個例子中只演示了監控。

小結

在本篇文章中,你可以學到如何在Springboot應用中應用特徵切換。我們從一個最基礎的例子開始,覆蓋到了大部分的特徵切換需求。接著,我們針對一個特殊的情況,介紹了特徵值的抽取和管理,最後,我們介紹瞭如何通過釋出一個Actuator端點來監控應用的特徵切換值狀態。

原文:http://dolszewski.com/spring/feature-toggle-spring-boot/