案例 我們所熟悉的jbdc是一種用於執行SQL語句的Java API,可以為多種關係資料庫提供統一訪問,提供了一種基準,據此可以構建更高級的工具和介面。 如上圖所示,任意的一個資料庫廠商只要去實現jdbc的介面,就可以輕鬆的對接jbdc從而為應用開發人員所服務。 SPI 上面的jdbc的設計理念叫S ...
案例
我們所熟悉的jbdc是一種用於執行SQL語句的Java API,可以為多種關係資料庫提供統一訪問,提供了一種基準,據此可以構建更高級的工具和介面。
如上圖所示,任意的一個資料庫廠商只要去實現jdbc的介面,就可以輕鬆的對接jbdc從而為應用開發人員所服務。
SPI
上面的jdbc的設計理念叫SPI,它的全名是Service Provider Interface。它的理念是對某類功能進行抽象,確保應用程式依賴抽象而不是具體的某種實現,通過配置服務實現者的方式來達到面向介面編程以及擴展的目的。比如我們項目中需要用到日誌組件,有的項目喜歡logback,有的喜歡log4j,有的喜歡common-log等等,如果在項目中直接依賴這些日誌介面,那麼後續如果需要對日誌組件重新選型,對現有項目的影響會非常大,所有後來就有了slfj,它抽象了日誌介面但不包含任何的實現,具體實現全部依賴於不同的廠商。
傳統的java spi一般做法是在resources/META-INF/services/目錄下麵創建一個以服務介面命名的文件,該文件內容就是實現該服務介面的具體實現類的完全限定名。當程式載入的時候,就能通過resources/META-INF/services/里的配置文件找到具體的實現類名,並載入實例化。 通過這個機制就能找到服務介面的實現類,而不需要再代碼里寫死。
java.util.ServiceLoader這個就是java spi中用來載入服務實現類的工具,本文不對它的具體用法做過多介紹。
主題:限流策略如何擴展
本文要討論的問題是,rpc框架中的限流過濾器擴展問題(可參考之前的文章 :簡易RPC框架-客戶端限流配置),之前介紹的限流實現是採用了guava提供的RateLimit,當時客戶端限流的實現是在框架中寫好的不允許修改,不同項目如果需要不同的限流策略那麼就需要針對原有方案進行擴展,如果擴展呢?
Spring-boot 中的SPI
我在spring-boot項目中按傳統的spi方式配置後,發現ServiceLoader載入指定介面找不到具體的實現類,後來發現spring-boot有自己的spi實現。它是在resources/META-INF/spring.factories中配置相關的介面,而且這個類的配置方式與傳統的spi也有所不同,它採用了key=value方式,這點有點類似dubbo的spi機制。文件目錄如下:
下麵給出我調整之後的方案:
客戶端限流介面
定義一個限流的介面,因為限流會有些參數控制,所以就增加RpcInvocation來協助完成。
public interface AccessLimitService {
void acquire(RpcInvocation invocation);
}
客戶端限流介面實現
本文只是為了簡單實現,所以直接將原有寫在rpc框架中的限流方式抽取出來,並沒有重新採用一種新的限流策略。
public class AccessLimitServiceImpl implements AccessLimitService {
@Override
public void acquire(RpcInvocation invocation) {
AccessLimitManager.acquire(invocation);
}
static class AccessLimitManager{
private final static Object lock=new Object();
private final static Map<String,RateLimiter> rateLimiterMap= Maps.newHashMap();
public static void acquire(RpcInvocation invocation){
if(!rateLimiterMap.containsKey(invocation.getClassName())) {
synchronized (lock) {
if(!rateLimiterMap.containsKey(invocation.getClassName())) {
final RateLimiter rateLimiter = RateLimiter.create(invocation.getMaxExecutesCount());
rateLimiterMap.put(invocation.getClassName(), rateLimiter);
}
}
}
else {
RateLimiter rateLimiter=rateLimiterMap.get(invocation.getClassName());
rateLimiter.acquire();
}
}
}
}
客戶端限流過濾器調整
既然限流的實現抽取成了介面,所以此處的具體實現調整為從服務提供者中找對應的實現。
@Override
public Object invoke(RpcInvoker invoker, RpcInvocation invocation) {
logger.info("before acquire,"+new Date());
List<AccessLimitService> accessLimitServiceLoader = SpringFactoriesLoader.loadFactories(AccessLimitService.class, null);
if(!CollectionUtils.isEmpty(accessLimitServiceLoader)){
AccessLimitService accessLimitService=accessLimitServiceLoader.get(0);
accessLimitService.acquire(invocation);
}
Object rpcResponse=invoker.invoke(invocation);
logger.info("after acquire,"+new Date());
return rpcResponse;
}
目前還不支持同一個項目中多種限流策略,目前版本只允許存在一種,如果配置了多種實現,也只會選擇第一個。如果需要支持也是可以的,通過配置一個名稱來指定即可,但感覺價值並不大。
SpringFactoriesLoader就是spring-boot實現的類似java.util.ServiceLoader的一種服務載入工具,它負責從resources/META-INF/spring.factories中讀取相應的配置,並對其載入實例化。總共包含兩個核心方法:
-
loadFactoryNames
這個方法就是載入某個介面的所有指定實現類名,它可以服務於下麵的loadFactories方法。
-
loadFactories 首先通過loadFactoryNames方法從配置文件中獲取介面與實現類的關係,然後一個一個實例化服務實現類。
經過以上幾步的調整,就基本實現了一個簡單的基於SPI思想的組件擴展機制。客戶端可以擴展任意的限流機制去替換。
本文源碼
文中代碼是依賴上述項目的,如果有不明白的可下載源碼