從這篇文章開始會從頭開始以 apache shenyu為路徑,一一學習shenyu中用到的技術及設計,當前文章學習shenyu中的spi(apache-shenyu 2.4.3版本)
apache shenyu前身soul網關,是一款java中spring5新引入的project-reactor的webflux,reactor-netty等為基礎實現(xiàn)的高性能網關,現(xiàn)已進入apache孵化器,作者yu199195 (xiaoyu) (github.com)
作者也是國內知名開源社區(qū)dromara的創(chuàng)始人,并且作有多個開源產品,apache-shenyu是其中之一apache/incubator-shenyu: ShenYu is High-Performance Java API Gateway. (github.com)
SPI是java多態(tài)和插件化非常重要的一環(huán)。
簡單的例子,java ee中jdbc的數(shù)據(jù)庫驅動,我們可以任意切換連接的數(shù)據(jù)庫,例如mysql,oracle等等,但是對于你的java應用來說并不知道具體使用哪個數(shù)據(jù)庫,而jdbc將對于數(shù)據(jù)庫的操作抽象出一套標準的接口,jdbc對于數(shù)據(jù)庫的操作基于接口來進行編碼,而不需要知道具體的實現(xiàn),但是不同數(shù)據(jù)庫的語法,實現(xiàn)邏輯都不相同,但是他們會根據(jù)jdbc提供的標準接口實現(xiàn)驅動程序,那么在java應用側只需要在使用哪個數(shù)據(jù)庫時,指定好driver-class-name即可。
jdbc抽象的主要是Driver接口和Connection接口

以上為不同數(shù)據(jù)庫廠商提供的Driver實現(xiàn),jdbc不需要知道如何實現(xiàn),只需要使用Driver接口編碼,然后由配置指定其子類,這就是一種SPI思想
public interface Driver {
// 不同的driver實現(xiàn)拿到不同的連接
Connection connect(String url, java.util.Properties info)
throws SQLException;
boolean acceptsURL(String url) throws SQLException;
DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info)
throws SQLException;
int getMajorVersion();
int getMinorVersion();
boolean jdbcCompliant();
//------------------------- JDBC 4.1 -----------------------------------
public Logger getParentLogger() throws SQLFeatureNotSupportedException;
}


還有一堆方法,感興趣的直接看java.sql.Connection接口,那么jdbc將連接的各種動作,和數(shù)據(jù)庫交互的邏輯抽象為對應方法,那么jdbc直接使用這些方法就可以了,實現(xiàn)則由數(shù)據(jù)庫那邊不同的實現(xiàn)通過SPI注入。
當然如果通過指定子類名稱,可以通過反射直接創(chuàng)建其實例注入。
下面介紹如果不知道子類名稱的情況下的spi實現(xiàn)。
java原生SPI,使用ServiceLoader,METAINF/services實現(xiàn)
具體點大家可以繼續(xù)搜索學習,這里只講解使用方法
ServiceLoader + META-INF/services 的使用方法
我們可以通過ServiceLoader#load方法來加載子類。然后通過services文件夾中命名接口/抽象類內部寫入要load的實現(xiàn)類名稱,其實也可以通過classLoader將當前classpath所有類變量判斷是否是其子類判斷出要找的類
- 如果有多個實現(xiàn)類,我們就要指定具體某個實現(xiàn)類,那就要么在代碼邏輯寫死或者又引入新的配置邏輯
- 性能很差
所有java提供了SPI機制,快速并且解耦的實現(xiàn)了選擇性子類發(fā)現(xiàn)機制
舉例com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy是hystrix中關于線程執(zhí)行相關的策略抽象。是一個抽象類(接口也可以)

我們可以使用自己的實現(xiàn),通過SPI機制

實現(xiàn)當然也可以多個,但是SPI機制大部分用來實現(xiàn)多種實現(xiàn),通過配置或者參數(shù)來選擇一個實現(xiàn)使用,多個實現(xiàn)可以自由替換,上面的例子就是 jdbc抽象的Driver和Connection接口通過 driver-class-name來選擇,
當然也可以是動態(tài)切換,例如下面要看的基于dubbo的spi方式

hystrix源碼
private static <T> T findService(
Class<T> spi,
ClassLoader classLoader) throws ServiceConfigurationError {
// 這里加載出來是一個可迭代的集合,所以是可以放入多個實現(xiàn)
ServiceLoader<T> sl = ServiceLoader.load(spi,
classLoader);
for (T s : sl) {
// hystrix的這個抽象是直接返回第一個實現(xiàn)
if (s != null)
return s;
}
return null;
}
private <T> T getPluginImplementation(Class<T> pluginClass) {
// 這里是通過配置文件的 全類名,通過反射實例化,這也是一種常見的抽象機制
T p = getPluginImplementationViaProperties(pluginClass, dynamicProperties);
if (p != null) return p;
// 利用java的SPI指定子類,然后就會使用其實現(xiàn)處理業(yè)務
return findService(pluginClass, classLoader);
}
shenyu的SPI,是參照了dubbo的spi實現(xiàn),但是本文只閱讀apache-shenyu的設計以及解決的問題
由一個 @SPI注解開始
/**
* SPI Extend the processing.
* All spi system reference the apache implementation of
* <a >Apache Dubbo Common Extension</a>.
*
* @see ExtensionFactory
* @see ExtensionLoader
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface SPI {
/**
* Value string.
*
* @return the string
*/
String value() default "";
}

舉例一個場景,apache-shenyu支持多種rule(即后端業(yè)務服務的url或者規(guī)則)注冊方式,也就是業(yè)務服務有哪些接口可以通過各種方式上報給shenyu-admin服務,然后shenyu-admin會把數(shù)據(jù)同步給shenyu-bootstrap服務(真正做網關的服務)分別提供了,nacos,http,consul,etcd,zookeeper多種rule上報實現(xiàn),只需要通過配置選擇即可。那么在shenyu的邏輯代碼中只需要對抽象出來的ShenyuClientServerRegisterRepository接口統(tǒng)一處理,不需要知道不同上報方式的差別,這就是java的抽象方式,通過接口或者抽象類(盡量使用接口)的方式抽象后不需要if,switch來判斷了,這種只要選擇一個實現(xiàn)的邏輯使用SPI機制精簡了代碼,解耦了模塊間的依賴,一下圖中多種上報實現(xiàn)只需要在實現(xiàn)類中關注

apache-shenyu中很多這種SPI抽象邏輯,例如還有RateLimiterAlgorithm接口將限流邏輯抽象,

下面看代碼

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface SPI {
// 指定在抽象方,也就放到接口上相當于jdk中的META-INF/services文件的名稱
/**
* Value string.
* 這里可以不使用 META-INF/shenyu路徑中的文件指定其實現(xiàn),直接通過注解,多一種切換方式,當然這里的spi加載也可以多實現(xiàn),下面代碼會提到
* @return the string
*/
String value() default "";
}
@SPI
public interface ShenyuClientServerRegisterRepository {
// 省略代碼
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Join {
// 放到實現(xiàn)類上,相當于jdk中的META-INF/services文件中的配置
}
ExtensionFactory spi工廠,那么spi的實現(xiàn)也可以使用多個實現(xiàn),但是apache-shenyu目前只有一個spi工廠,這里想看spi工廠多實現(xiàn)可以找dubbo源碼看一看
@SPI("spi")
public interface ExtensionFactory {
/**
* Gets Extension.
*
* @param <T> the type parameter
* @param key the key
* @param clazz the clazz
* @return the extension
*/
<T> T getExtension(String key, Class<T> clazz);
}
@Join
public class SpiExtensionFactory implements ExtensionFactory {
@Override
public <T> T getExtension(final String key, final Class<T> clazz) {
return Optional.ofNullable(clazz)
.filter(Class::isInterface)
.filter(cls -> cls.isAnnotationPresent(SPI.class))
.map(ExtensionLoader::getExtensionLoader)
.map(ExtensionLoader::getDefaultJoin)
.orElse(null);
}
}

下面看看核心代碼ExtensionLoader
@SuppressWarnings("all")
public final class ExtensionLoader<T> {
private static final Logger LOG = LoggerFactory.getLogger(ExtensionLoader.class);
// 學習jdk的spi機制指定實現(xiàn)方式
private static final String SHENYU_DIRECTORY = "META-INF/shenyu/";
// key為抽象的類的class對象,value為當前類對象的實例,使用泛型機制,在實例化時類型已經確定,保證類型安全,這里針對不同的抽象,使用各自的Loader類
private static final Map<Class<?>, ExtensionLoader<?>> LOADERS = new ConcurrentHashMap<>();
// 抽象出來的接口的class對象
private final Class<T> clazz;
private final ClassLoader classLoader;
// 緩存的 實現(xiàn)類class,實現(xiàn)類的key(dubbo的spi可以設置kv映射) -> 實現(xiàn)類class對象,一個holder對象緩存當前抽象接口的所有子類class
private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();
// 緩存的實現(xiàn)類 實例,實現(xiàn)類key -> 實現(xiàn)類的實例對象,一個holder對象緩存一個 實例
private final Map<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();
// key為實現(xiàn)類class對象,value為其實例
private final Map<Class<?>, Object> joinInstances = new ConcurrentHashMap<>();
private String cachedDefaultName;
/**
* Instantiates a new Extension loader.
*
* @param clazz the clazz.
*/
private ExtensionLoader(final Class<T> clazz, final ClassLoader cl) {
//一個在內部實例化其 傳入的抽象接口class對象的ExtensionLoader
this.clazz = clazz;
this.classLoader = cl;
if (!Objects.equals(clazz, ExtensionFactory.class)) {
ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getExtensionClasses();
}
}
// 暴露出去的唯二static方法之一,區(qū)別需要傳入classLoader,基本都是用另外一個,其他方法通過實例調用
public static <T> ExtensionLoader<T> getExtensionLoader(final Class<T> clazz, final ClassLoader cl) {
Objects.requireNonNull(clazz, "extension clazz is null");
// 可以看到這里的SPI限制必須使用接口,接口可以多繼承,相對抽象類還是好用一些的。
if (!clazz.isInterface()) {
throw new IllegalArgumentException("extension clazz (" + clazz + ") is not interface!");
}
// 必須是@SPI注解的接口
if (!clazz.isAnnotationPresent(SPI.class)) {
throw new IllegalArgumentException("extension clazz (" + clazz + ") without @" + SPI.class + " Annotation");
}
// 獲取對應 抽象接口的ExtensionLoader
ExtensionLoader<T> extensionLoader = (ExtensionLoader<T>) LOADERS.get(clazz);
if (Objects.nonNull(extensionLoader)) {
return extensionLoader;
}
// 如果沒有調用過實例化一個ExtensionLoader,看來是懶加載
LOADERS.putIfAbsent(clazz, new ExtensionLoader<>(clazz, cl));
return (ExtensionLoader<T>) LOADERS.get(clazz);
}
// 暴露出去的唯二static方法,大部分使用這個,其他方法通過實例調用
public static <T> ExtensionLoader<T> getExtensionLoader(final Class<T> clazz) {
return getExtensionLoader(clazz, ExtensionLoader.class.getClassLoader());
}
// 通過實現(xiàn)類key獲取其實例
public T getDefaultJoin() {
getExtensionClasses();
if (StringUtils.isBlank(cachedDefaultName)) {
return null;
}
return getJoin(cachedDefaultName);
}
// 通過實現(xiàn)類key獲取其實例
public T getJoin(final String name) {
if (StringUtils.isBlank(name)) {
throw new NullPointerException("get join name is null");
}
Holder<Object> objectHolder = cachedInstances.get(name);
if (Objects.isNull(objectHolder)) {
// 第一次獲取其實例,放入一個holder容器
cachedInstances.putIfAbsent(name, new Holder<>());
objectHolder = cachedInstances.get(name);
}
Object value = objectHolder.getValue();
// 通過雙重校驗 保證單例
if (Objects.isNull(value)) {
synchronized (cachedInstances) {
value = objectHolder.getValue();
if (Objects.isNull(value)) {
// 創(chuàng)建要獲取的實例
value = createExtension(name);
objectHolder.setValue(value);
}
}
}
return (T) value;
}
// 獲取所有實現(xiàn),為什么沒有參數(shù),因為對于當前類的實例只會對應一個
// 抽象的接口,和其所有實現(xiàn)類的實例緩存,如果前面獲取到了抽象接口的ExtensionLoader則直接可獲取所有實現(xiàn)的實例
public List<T> getJoins() {
// 獲取所有實現(xiàn)類的緩存,如果第一次獲取,會將所有class對象加載并緩存
Map<String, Class<?>> extensionClasses = this.getExtensionClasses();
if (extensionClasses.isEmpty()) {
return Collections.emptyList();
}
// 如果剛加載的所有class子類實現(xiàn)的對象與其實現(xiàn)類的實例數(shù)量,說明所有實現(xiàn)class對象已經都實例化了直接返回
if (Objects.equals(extensionClasses.size(), cachedInstances.size())) {
return (List<T>) this.cachedInstances.values().stream().map(e -> {
return e.getValue();
}).collect(Collectors.toList());
}
List<T> joins = new ArrayList<>();
extensionClasses.forEach((name, v) -> {
// 如果哪些class沒有實例化,進行實例化
T join = this.getJoin(name);
joins.add(join);
});
return joins;
}
@SuppressWarnings("unchecked")
private T createExtension(final String name) {
// 獲取子類實現(xiàn)的class對象
Class<?> aClass = getExtensionClasses().get(name);
if (Objects.isNull(aClass)) {
throw new IllegalArgumentException("name is error");
}
Object o = joinInstances.get(aClass);
if (Objects.isNull(o)) {
try {
// 這里只通過concurrentMap + putIfAbsent保證線程安全,實例化出來多個無所謂,只會成功放入第一個,也是單例的。
joinInstances.putIfAbsent(aClass, aClass.newInstance());
o = joinInstances.get(aClass);
} catch (InstantiationException | IllegalAccessException e) {
throw new IllegalStateException("Extension instance(name: " + name + ", class: "
+ aClass + ") could not be instantiated: " + e.getMessage(), e);
}
}
return (T) o;
}
// class對象必須保證只加載一次,也就是一個抽象接口的所有子類class只會加載一次
public Map<String, Class<?>> getExtensionClasses() {
Map<String, Class<?>> classes = cachedClasses.getValue();
// class對象必須保證只加載一次,通過雙重校驗
if (Objects.isNull(classes)) {
synchronized (cachedClasses) {
classes = cachedClasses.getValue();
if (Objects.isNull(classes)) {
classes = loadExtensionClass();
cachedClasses.setValue(classes);
}
}
}
return classes;
}
// 加載子類 class
private Map<String, Class<?>> loadExtensionClass() {
SPI annotation = clazz.getAnnotation(SPI.class);
if (Objects.nonNull(annotation)) {
String value = annotation.value();
if (StringUtils.isNotBlank(value)) {
cachedDefaultName = value;
}
}
Map<String, Class<?>> classes = new HashMap<>(16);
loadDirectory(classes);
return classes;
}
// 加載子類 class
private void loadDirectory(final Map<String, Class<?>> classes) {
String fileName = SHENYU_DIRECTORY + clazz.getName();
try {
Enumeration<URL> urls = Objects.nonNull(this.classLoader) ? classLoader.getResources(fileName)
: ClassLoader.getSystemResources(fileName);
if (Objects.nonNull(urls)) {
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
loadResources(classes, url);
}
}
} catch (IOException t) {
LOG.error("load extension class error {}", fileName, t);
}
}
private void loadResources(final Map<String, Class<?>> classes, final URL url) throws IOException {
try (InputStream inputStream = url.openStream()) {
Properties properties = new Properties();
properties.load(inputStream);
properties.forEach((k, v) -> {
// dubbo的spi形式不同于jdk,是http=org.apache.shenyu.admin.controller.ShenyuClientHttpRegistryController的形式,左邊為key,右邊為類名,可以維護一個map形式的子類實現(xiàn)集合
String name = (String) k;
String classPath = (String) v;
if (StringUtils.isNotBlank(name) && StringUtils.isNotBlank(classPath)) {
try {
loadClass(classes, name, classPath);
} catch (ClassNotFoundException e) {
throw new IllegalStateException("load extension resources error", e);
}
}
});
} catch (IOException e) {
throw new IllegalStateException("load extension resources error", e);
}
}
private void loadClass(final Map<String, Class<?>> classes,
final String name, final String classPath) throws ClassNotFoundException {
//獲取子類實現(xiàn)的class對象
Class<?> subClass = Objects.nonNull(this.classLoader) ? Class.forName(classPath, true, this.classLoader) : Class.forName(classPath);
// 校驗必須為其子類
if (!clazz.isAssignableFrom(subClass)) {
throw new IllegalStateException("load extension resources error," + subClass + " subtype is not of " + clazz);
}
// 校驗必須標注 @Join注解
if (!subClass.isAnnotationPresent(Join.class)) {
throw new IllegalStateException("load extension resources error," + subClass + " without @" + Join.class + " annotation");
}
Class<?> oldClass = classes.get(name);
if (Objects.isNull(oldClass)) {
//放入
classes.put(name, subClass);
} else if (!Objects.equals(oldClass, subClass)) {
// 如果產生了重復放入,校驗是否相同,不同報錯
throw new IllegalStateException("load extension resources error,Duplicate class " + clazz.getName() + " name " + name + " on " + oldClass.getName() + " or " + subClass.getName());
}
}
/**
* The type Holder.
*
* @param <T> the type parameter.
*/
//用于緩存的包裝類,可能緩存子類實現(xiàn)類的實例對象,或者所有子類的class對象map
public static class Holder<T> {
// 使用volatile,保證其他線程可見性
private volatile T value;
/**
* Gets value.
*
* @return the value
*/
public T getValue() {
return value;
}
/**
* Sets value.
*
* @param value the value
*/
public void setValue(final T value) {
this.value = value;
}
}
}
總結
- apache-shenyu基本沿用dubbo的spi機制,通過@SPI,@Join + META-INF/指定名稱 的目錄加載,但是文件內容使用kv形式,也提供多實現(xiàn)通過文件配置中的不同key區(qū)別
- 有一個spi工廠提供可能存在更多的spi實現(xiàn)
- 使用泛型保證類型安全,不同的抽象父類,實例化出不同的ExtensionLoader加載器來處理
- 使用緩存,線程安全容器,雙重校驗等保證單例