什么是微內核架構?
微內核是一種典型的架構模式 ,區別於普通的設計模式,架構模式是一種高層模式,用於描述系統級的結構組成、相互關系及相關約束。微內核架構在開源框架中的應用也比較廣泛,除了 ShardingSphere 之外,在主流的 PRC 框架 Dubbo 中也實現了自己的微內核架構。那么,在介紹什么是微內核架構之前,我們有必要先闡述這些開源框架會使用微內核架構的原因。
為什么要使用微內核架構?
微內核架構本質上是為了提高系統的擴展性 。所謂擴展性,是指系統在經歷不可避免的變更時所具有的靈活性,以及針對提供這樣的靈活性所需要付出的成本間的平衡能力。也就是說,當在往系統中添加新業務時,不需要改變原有的各個組件,只需把新業務封閉在一個新的組件中就能完成整體業務的升級,我們認為這樣的系統具有較好的可擴展性。
就架構設計而言,擴展性是軟件設計的永恆話題。而要實現系統擴展性,一種思路是提供可插拔式的機制來應對所發生的變化。當系統中現有的某個組件不滿足要求時,我們可以實現一個新的組件來替換它,而整個過程對於系統的運行而言應該是無感知的,我們也可以根據需要隨時完成這種新舊組件的替換。
比如在下個課時中我們將要介紹的 ShardingSphere 中提供的分布式主鍵功能,分布式主鍵的實現可能有很多種,而擴展性在這個點上的體現就是, 我們可以使用任意一種新的分布式主鍵實現來替換原有的實現,而不需要依賴分布式主鍵的業務代碼做任何的改變 。

微內核架構模式為這種實現擴展性的思路提供了架構設計上的支持,ShardingSphere 基於微內核架構實現了高度的擴展性。在介紹如何實現微內核架構之前,我們先對微內核架構的具體組成結構和基本原理做簡要的闡述。
什么是微內核架構?
從組成結構上講, 微內核架構包含兩部分組件:內核系統和插件 。這里的內核系統通常提供系統運行所需的最小功能集,而插件是獨立的組件,包含自定義的各種業務代碼,用來向內核系統增強或擴展額外的業務能力。在 ShardingSphere 中,前面提到的分布式主鍵就是插件,而 ShardingSphere 的運行時環境構成了內核系統。

那么這里的插件具體指的是什么呢?這就需要我們明確兩個概念,一個概念就是經常在說的 API ,這是系統對外暴露的接口。而另一個概念就是 SPI(Service Provider Interface,服務提供接口),這是插件自身所具備的擴展點。就兩者的關系而言,API 面向業務開發人員,而 SPI 面向框架開發人員,兩者共同構成了 ShardingSphere 本身。

可插拔式的實現機制說起來簡單,做起來卻不容易,我們需要考慮兩方面內容。一方面,我們需要梳理系統的變化並把它們抽象成多個 SPI 擴展點。另一方面, 當我們實現了這些 SPI 擴展點之后,就需要構建一個能夠支持這種可插拔機制的具體實現,從而提供一種 SPI 運行時環境 。
那么,ShardingSphere 是如何實現微內核架構的呢?讓我們來一起看一下。
如何實現微內核架構?
事實上,JDK 已經為我們提供了一種微內核架構的實現方式,這種實現方式針對如何設計和實現 SPI 提出了一些開發和配置上的規范,ShardingSphere 使用的就是這種規范。首先,我們需要設計一個服務接口,並根據需要提供不同的實現類。接下來,我們將模擬實現分布式主鍵的應用場景。
基於 SPI 的約定,創建一個單獨的工程來存放服務接口,並給出接口定義。請注意 這個服務接口的完整類路徑為 com.tianyilan.KeyGenerator ,接口中只包含一個獲取目標主鍵的簡單示例方法。

public interface KeyGenerator{
String getKey();
}
針對該接口,提供兩個簡單的實現類,分別是基於 UUID 的 UUIDKeyGenerator 和基於雪花算法的 SnowflakeKeyGenerator。
/**
* @author WGR
* @create 2020/11/19 -- 18:54
*/
public class UUIDKeyGenerator implements KeyGenerator {
@Override
public String getKey() {
return "UUIDKey";
}
}
/**
* @author WGR
* @create 2020/11/19 -- 18:55
*/
public class SnowflakeKeyGenerator implements KeyGenerator {
@Override
public String getKey() {
return "SnowflakeKey";
}
}
接下來的這個步驟很關鍵, 在這個代碼工程的 META-INF/services/ 目錄下,需要創建一個以服務接口完整類路徑 com.dalianpai.KeyGenerator 命名的文件 ,文件的內容是指向該接口所對應的兩個實現類的完整類路徑 com.dalianpai.UUIDKeyGenerator 和 com.dalianpai. SnowflakeKeyGenerator。
我們把這個代碼工程打成一個 jar 包,然后新建另一個代碼工程,該代碼工程需要這個 jar 包,並完成如下所示的 Main 函數。
/**
* @author WGR
* @create 2020/11/19 -- 18:56
*/
public class Test {
public static void main(String[] args) {
ServiceLoader<KeyGenerator> generators = ServiceLoader.load(KeyGenerator.class);
for (KeyGenerator generator : generators) {
System.out.println(generator.getClass());
String key = generator.getKey();
System.out.println(key);
}
}
}
現在,該工程的角色是 SPI 服務的使用者,這里使用了 JDK 提供的 ServiceLoader 工具類來獲取所有 KeyGenerator 的實現類。現在在 jar 包的 META-INF/services/com.dalianpai.KeyGenerator 文件中有兩個 KeyGenerator 實現類的定義。執行這段 Main 函數,我們將得到的輸出結果如下:
class com.dalianpai.UUIDKeyGenerator
UUIDKey
class com.dalianpai.SnowflakeKeyGenerator
SnowflakeKey
如果我們調整 META-INF/services/com.dalianpai.KeyGenerator 文件中的內容,去掉 com.dalianpai.UUIDKeyGenerator 的定義,並重新打成 jar 包供 SPI 服務的使用者進行引用。再次執行 Main 函數,則只會得到基於 SnowflakeKeyGenerator 的輸出結果。
至此, 完整 的 SPI 提供者和使用者的實現過程演示完畢。我們通過一張圖,總結基於 JDK 的 SPI 機制實現微內核架構的開發流程:

這個示例非常簡單,但卻是 ShardingSphere 中實現微內核架構的基礎。接下來,就讓我們把話題轉到 ShardingSphere,看看 ShardingSphere 中應用 SPI 機制的具體方法。
ShardingSphere 如何基於微內核架構實現擴展性?
ShardingSphere 中微內核架構的實現過程並不復雜,基本就是對 JDK 中 SPI 機制的封裝。讓我們一起來看一下。
ShardingSphere 中的微內核架構基礎實現機制
我們發現,在 ShardingSphere 源碼的根目錄下,存在一個獨立的工程 shardingsphere-spi。顯然,從命名上看,這個工程中應該包含了 ShardingSphere 實現 SPI 的相關代碼。我們快速瀏覽該工程,發現里面只有一個接口定義和兩個工具類。我們先來看這個接口定義 TypeBasedSPI:
public interface TypeBasedSPI {
//獲取SPI對應的類型
String getType();
//獲取屬性
Properties getProperties();
//設置屬性
void setProperties(Properties properties);
}
從定位上看,這個接口在 ShardingSphere 中應該是一個頂層接口,我們已經在上一課時給出了這一接口的實現類類層結構。接下來再看一下 NewInstanceServiceLoader 類,從命名上看,不難想象該類的作用類似於一種 ServiceLoader,用於加載新的目標對象實例:
public final class NewInstanceServiceLoader {
private static final Map<Class, Collection<Class<?>>> SERVICE_MAP = new HashMap<>();
//通過ServiceLoader獲取新的SPI服務實例並注冊到SERVICE_MAP中
public static <T> void register(final Class<T> service) {
for (T each : ServiceLoader.load(service)) {
registerServiceClass(service, each);
}
}
@SuppressWarnings("unchecked")
private static <T> void registerServiceClass(final Class<T> service, final T instance) {
Collection<Class<?>> serviceClasses = SERVICE_MAP.get(service);
if (null == serviceClasses) {
serviceClasses = new LinkedHashSet<>();
}
serviceClasses.add(instance.getClass());
SERVICE_MAP.put(service, serviceClasses);
}
@SneakyThrows
@SuppressWarnings("unchecked")
public static <T> Collection<T> newServiceInstances(final Class<T> service) {
Collection<T> result = new LinkedList<>();
if (null == SERVICE_MAP.get(service)) {
return result;
}
for (Class<?> each : SERVICE_MAP.get(service)) {
result.add((T) each.newInstance());
}
return result;
}
}
在上面這段代碼中, 首先看到了熟悉的 ServiceLoader.load(service) 方法,這是 JDK 中 ServiceLoader 工具類的具體應用。同時,注意到 ShardingSphere 使用了一個 HashMap 來保存類的定義以及類的實例之 間 的一對多關系,可以認為,這是一種用於提高訪問效率的緩存機制。
最后,我們來看一下 TypeBasedSPIServiceLoader 的實現,該類依賴於前面介紹的 NewInstanceServiceLoader 類。 下面這段代碼演示了 基於 NewInstanceServiceLoader 獲取實例類列表,並根據所傳入的類型做過濾:
//使用NewInstanceServiceLoader獲取實例類列表,並根據類型做過濾
private Collection<T> loadTypeBasedServices(final String type) {
return Collections2.filter(NewInstanceServiceLoader.newServiceInstances(classType), new Predicate<T>() {
@Override
public boolean apply(final T input) {
return type.equalsIgnoreCase(input.getType());
}
});
}
TypeBasedSPIServiceLoader 對外暴露了服務的接口,對通過 loadTypeBasedServices 方法獲取的服務實例設置對應的屬性然后返回:
//基於類型通過SPI創建實例
public final T newService(final String type, final Properties props) {
Collection<T> typeBasedServices = loadTypeBasedServices(type);
if (typeBasedServices.isEmpty()) {
throw new RuntimeException(String.format("Invalid `%s` SPI type `%s`.", classType.getName(), type));
}
T result = typeBasedServices.iterator().next();
result.setProperties(props);
return result;
}
同時,TypeBasedSPIServiceLoader 也對外暴露了不需要傳入類型的 newService 方法,該方法使用了 loadFirstTypeBasedService 工具方法來獲取第一個服務實例:
//基於默認類型通過SPI創建實例
public final T newService() {
T result = loadFirstTypeBasedService();
result.setProperties(new Properties());
return result;
}
private T loadFirstTypeBasedService() {
Collection<T> instances = NewInstanceServiceLoader.newServiceInstances(classType);
if (instances.isEmpty()) {
throw new RuntimeException(String.format("Invalid `%s` SPI, no implementation class load from SPI.", classType.getName()));
}
return instances.iterator().next();
}
這樣,shardingsphere-spi 代碼工程中的內容就介紹完畢。 這部分內容相當於是 ShardingSphere 中所提供的插件運行時環境 。下面我們基於 ShardingSphere 中提供的幾個典型應用場景來討論這個運行時環境的具體使用方法。
微內核架構在 ShardingSphere 中的應用
- SQL 解析器 SQLParser
SQLParser 類,該類負責將具體某一條 SQL 解析成一個抽象語法樹的整個過程。而這個 SQLParser 的生成由 SQLParserFactory 負責:
public final class SQLParserFactory {
public static SQLParser newInstance(final String databaseTypeName, final String sql) {
//通過SPI機制加載所有擴展
for (SQLParserEntry each : NewInstanceServiceLoader.newServiceInstances(SQLParserEntry.class)) {
…
}
}
可以看到,這里並沒有使用前面介紹的 TypeBasedSPIServiceLoader 來加載實例,而是直接使用更為底層的 NewInstanceServiceLoader。
這里引入的 SQLParserEntry 接口就位於 shardingsphere-sql-parser-spi 工程的 org.apache.shardingsphere.sql.parser.spi 包中。顯然,從包的命名上看,該接口是一個 SPI 接口。在 SQLParserEntry 類層結構接口中包含一批實現類,分別對應各個具體的數據庫:

我們先來看針對 MySQL 的代碼工程 shardingsphere-sql-parser-mysql,在 META-INF/services 目錄下,我們找到了一個org.apache.shardingsphere.sql.parser.spi.SQLParserEntry 文件:

可以看到這里指向了 org.apache.shardingsphere.sql.parser.MySQLParserEntry 類。再來到 Oracle 的代碼工程 shardingsphere-sql-parser-oracle,在 META-INF/services 目錄下,同樣找到了一個 org.apache.shardingsphere.sql.parser.spi.SQLParserEntry 文件:

顯然,這里應該指向 org.apache.shardingsphere.sql.parser.OracleParserEntry 類,通過這種方式,系統在運行時就會根據類路徑動態加載 SPI。
可以注意到,在 SQLParserEntry 接口的類層結構中,實際並沒有使用到 TypeBasedSPI 接口 ,而是完全采用了 JDK 原生的 SPI 機制。
- 配置中心 ConfigCenter
接下來,我們來找一個使用 TypeBasedSPI 的示例,比如代表配置中心的 ConfigCenter:
public interface ConfigCenter extends TypeBasedSPI
顯然,ConfigCenter 接口繼承了 TypeBasedSPI 接口,而在 ShardingSphere 中也存在兩個 ConfigCenter 接口的實現類,一個是 ApolloConfigCenter,一個是 CuratorZookeeperConfigCenter。
在 sharding-orchestration-core 工程的 org.apache.shardingsphere.orchestration.internal.configcenter 中,我們找到了 ConfigCenterServiceLoader 類,該類擴展了前面提到的 TypeBasedSPIServiceLoader 類:
public final class ConfigCenterServiceLoader extends TypeBasedSPIServiceLoader<ConfigCenter> {
static {
NewInstanceServiceLoader.register(ConfigCenter.class);
}
public ConfigCenterServiceLoader() {
super(ConfigCenter.class);
}
//基於SPI加載ConfigCenter
public ConfigCenter load(final ConfigCenterConfiguration configCenterConfig) {
Preconditions.checkNotNull(configCenterConfig, "Config center configuration cannot be null.");
ConfigCenter result = newService(configCenterConfig.getType(), configCenterConfig.getProperties());
result.init(configCenterConfig);
return result;
}
}
那么它是如何實現的呢? 首先,ConfigCenterServiceLoader 類通過 NewInstanceServiceLoader.register(ConfigCenter.class) 語句將所有 ConfigCenter 注冊到系統中,這一步會通過 JDK 的 ServiceLoader 工具類加載類路徑中的所有 ConfigCenter 實例。
我們可以看到在上面的 load 方法中,通過父類 TypeBasedSPIServiceLoader 的 newService 方法,基於類型創建了 SPI 實例。
以 ApolloConfigCenter 為例,我們來看它的使用方法。在 sharding-orchestration-config-apollo 工程的 META-INF/services 目錄下,應該存在一個名為 org.apache.shardingsphere.orchestration.config.api.ConfigCenter 的配置文件,指向 ApolloConfigCenter 類:

其他的 ConfigCenter 實現也是一樣,你可以自行查閱 sharding-orchestration-config-zookeeper-curator 等工程中的 SPI 配置文件。
至此,我們全面了解了 ShardingSphere 中的微內核架構,也就可以基於 ShardingSphere 所提供的各種 SPI 擴展點提供滿足自身需求的具體實現。
從源碼解析到日常開發
在日常開發過程中,我們一般可以直接使用 JDK 的 ServiceLoader 類來實現 SPI 機制。當然,我們也可以采用像 ShardingSphere 的方式對 ServiceLoader 類進行一層簡單的封裝,並添加屬性設置等自定義功能。
同時,我們也應該注意到,ServiceLoader 這種實現方案也有一定缺點:
- 一方面,META/services 這個配置文件的加載地址是寫死在代碼中,缺乏靈活性。
- 另一方面,ServiceLoader 內部采用了基於迭代器的加載方法,會把配置文件中的所有 SPI 實現類都加載到內存中,效率不高。
所以如果需要提供更高的靈活性和性能,我們也可以基於 ServiceLoader 的實現方法自己開發適合自身需求的 SPI 加載 機制。
