从这篇文章开始会从头开始以 apache shenyu为路径,一一学习shenyu中用到的技术及设计,当前文章学习shenyu中的spi(apache-shenyu 2.4.3版本)
apache shenyu前身soul网关,是一款java中spring5新引入的project-reactor的webflux,reactor-netty等为基础实现的高性能网关,现已进入apache孵化器,作者yu199195 (xiaoyu) (github.com)
作者也是国内知名开源社区dromara的创始人,并且作有多个开源产品,apache-shenyu是其中之一apache/incubator-shenyu: ShenYu is High-Performance Java API Gateway. (github.com)
SPI是java多态和插件化非常重要的一环。
简单的例子,java ee中jdbc的数据库驱动,我们可以任意切换连接的数据库,例如mysql,oracle等等,但是对于你的java应用来说并不知道具体使用哪个数据库,而jdbc将对于数据库的操作抽象出一套标准的接口,jdbc对于数据库的操作基于接口来进行编码,而不需要知道具体的实现,但是不同数据库的语法,实现逻辑都不相同,但是他们会根据jdbc提供的标准接口实现驱动程序,那么在java应用侧只需要在使用哪个数据库时,指定好driver-class-name即可。
jdbc抽象的主要是Driver接口和Connection接口
![](https://img.haomeiwen.com/i11629480/e6a51d774a93c546.png)
以上为不同数据库厂商提供的Driver实现,jdbc不需要知道如何实现,只需要使用Driver接口编码,然后由配置指定其子类,这就是一种SPI思想
public interface Driver {
// 不同的driver实现拿到不同的连接
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;
}
![](https://img.haomeiwen.com/i11629480/be0e62f214e5f93f.png)
![](https://img.haomeiwen.com/i11629480/d991b5558e00cb80.png)
还有一堆方法,感兴趣的直接看java.sql.Connection接口,那么jdbc将连接的各种动作,和数据库交互的逻辑抽象为对应方法,那么jdbc直接使用这些方法就可以了,实现则由数据库那边不同的实现通过SPI注入。
当然如果通过指定子类名称,可以通过反射直接创建其实例注入。
下面介绍如果不知道子类名称的情况下的spi实现。
java原生SPI,使用ServiceLoader,METAINF/services实现
具体点大家可以继续搜索学习,这里只讲解使用方法
ServiceLoader + META-INF/services 的使用方法
我们可以通过ServiceLoader#load方法来加载子类。然后通过services文件夹中命名接口/抽象类内部写入要load的实现类名称,其实也可以通过classLoader将当前classpath所有类变量判断是否是其子类判断出要找的类
- 如果有多个实现类,我们就要指定具体某个实现类,那就要么在代码逻辑写死或者又引入新的配置逻辑
- 性能很差
所有java提供了SPI机制,快速并且解耦的实现了选择性子类发现机制
举例com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy是hystrix中关于线程执行相关的策略抽象。是一个抽象类(接口也可以)
![](https://img.haomeiwen.com/i11629480/4f3020c55ecfd5ea.png)
我们可以使用自己的实现,通过SPI机制
![](https://img.haomeiwen.com/i11629480/0512cf175a4c1d6e.png)
实现当然也可以多个,但是SPI机制大部分用来实现多种实现,通过配置或者参数来选择一个实现使用,多个实现可以自由替换,上面的例子就是 jdbc抽象的Driver和Connection接口通过 driver-class-name来选择,
当然也可以是动态切换,例如下面要看的基于dubbo的spi方式
![](https://img.haomeiwen.com/i11629480/2fbea05c6787a75e.png)
hystrix源码
private static <T> T findService(
Class<T> spi,
ClassLoader classLoader) throws ServiceConfigurationError {
// 这里加载出来是一个可迭代的集合,所以是可以放入多个实现
ServiceLoader<T> sl = ServiceLoader.load(spi,
classLoader);
for (T s : sl) {
// hystrix的这个抽象是直接返回第一个实现
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指定子类,然后就会使用其实现处理业务
return findService(pluginClass, classLoader);
}
shenyu的SPI,是参照了dubbo的spi实现,但是本文只阅读apache-shenyu的设计以及解决的问题
由一个 @SPI注解开始
/**
* SPI Extend the processing.
* All spi system reference the apache implementation of
* <a href="https://github.com/apache/dubbo/blob/master/dubbo-common/src/main/java/org/apache/dubbo/common/extension">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 "";
}
![](https://img.haomeiwen.com/i11629480/7d3d32b87dd7ad67.png)
举例一个场景,apache-shenyu支持多种rule(即后端业务服务的url或者规则)注册方式,也就是业务服务有哪些接口可以通过各种方式上报给shenyu-admin服务,然后shenyu-admin会把数据同步给shenyu-bootstrap服务(真正做网关的服务)分别提供了,nacos,http,consul,etcd,zookeeper多种rule上报实现,只需要通过配置选择即可。那么在shenyu的逻辑代码中只需要对抽象出来的ShenyuClientServerRegisterRepository接口统一处理,不需要知道不同上报方式的差别,这就是java的抽象方式,通过接口或者抽象类(尽量使用接口)的方式抽象后不需要if,switch来判断了,这种只要选择一个实现的逻辑使用SPI机制精简了代码,解耦了模块间的依赖,一下图中多种上报实现只需要在实现类中关注
![](https://img.haomeiwen.com/i11629480/acc6aa4b9d3fb921.png)
apache-shenyu中很多这种SPI抽象逻辑,例如还有RateLimiterAlgorithm接口将限流逻辑抽象,
![](https://img.haomeiwen.com/i11629480/fd0bab87d9687fc0.png)
下面看代码
![](https://img.haomeiwen.com/i11629480/4d726742b06cd460.png)
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface SPI {
// 指定在抽象方,也就放到接口上相当于jdk中的META-INF/services文件的名称
/**
* Value string.
* 这里可以不使用 META-INF/shenyu路径中的文件指定其实现,直接通过注解,多一种切换方式,当然这里的spi加载也可以多实现,下面代码会提到
* @return the string
*/
String value() default "";
}
@SPI
public interface ShenyuClientServerRegisterRepository {
// 省略代码
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Join {
// 放到实现类上,相当于jdk中的META-INF/services文件中的配置
}
ExtensionFactory spi工厂,那么spi的实现也可以使用多个实现,但是apache-shenyu目前只有一个spi工厂,这里想看spi工厂多实现可以找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);
}
}
![](https://img.haomeiwen.com/i11629480/5e7872080dcf08c7.png)
下面看看核心代码ExtensionLoader
@SuppressWarnings("all")
public final class ExtensionLoader<T> {
private static final Logger LOG = LoggerFactory.getLogger(ExtensionLoader.class);
// 学习jdk的spi机制指定实现方式
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;
// 缓存的 实现类class,实现类的key(dubbo的spi可以设置kv映射) -> 实现类class对象,一个holder对象缓存当前抽象接口的所有子类class
private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();
// 缓存的实现类 实例,实现类key -> 实现类的实例对象,一个holder对象缓存一个 实例
private final Map<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();
// key为实现类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方法之一,区别需要传入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());
}
// 通过实现类key获取其实例
public T getDefaultJoin() {
getExtensionClasses();
if (StringUtils.isBlank(cachedDefaultName)) {
return null;
}
return getJoin(cachedDefaultName);
}
// 通过实现类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)) {
// 创建要获取的实例
value = createExtension(name);
objectHolder.setValue(value);
}
}
}
return (T) value;
}
// 获取所有实现,为什么没有参数,因为对于当前类的实例只会对应一个
// 抽象的接口,和其所有实现类的实例缓存,如果前面获取到了抽象接口的ExtensionLoader则直接可获取所有实现的实例
public List<T> getJoins() {
// 获取所有实现类的缓存,如果第一次获取,会将所有class对象加载并缓存
Map<String, Class<?>> extensionClasses = this.getExtensionClasses();
if (extensionClasses.isEmpty()) {
return Collections.emptyList();
}
// 如果刚加载的所有class子类实现的对象与其实现类的实例数量,说明所有实现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) {
// 获取子类实现的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形式的子类实现集合
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 {
//获取子类实现的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.
*/
//用于缓存的包装类,可能缓存子类实现类的实例对象,或者所有子类的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形式,也提供多实现通过文件配置中的不同key区别
- 有一个spi工厂提供可能存在更多的spi实现
- 使用泛型保证类型安全,不同的抽象父类,实例化出不同的ExtensionLoader加载器来处理
- 使用缓存,线程安全容器,双重校验等保证单例
网友评论