簡易RPC框架-SPI
案例
我們所熟悉的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思想的組件擴展機制。客戶端可以擴展任意的限流機制去替換。
本文源碼
- RPC消費端
- RPC服務端
- RPC組件
文中代碼是依賴上述項目的,如果有不明白的可下載源碼
簡易RPC框架-SPI