美文网首页
apache-shenyu之SPI

apache-shenyu之SPI

作者: 二哈_8fd0 | 来源:发表于2022-05-22 21:20 被阅读0次
从这篇文章开始会从头开始以 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接口


image.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;
}
Connection接口方法
Connection接口方法

还有一堆方法,感兴趣的直接看java.sql.Connection接口,那么jdbc将连接的各种动作,和数据库交互的逻辑抽象为对应方法,那么jdbc直接使用这些方法就可以了,实现则由数据库那边不同的实现通过SPI注入。
当然如果通过指定子类名称,可以通过反射直接创建其实例注入。
下面介绍如果不知道子类名称的情况下的spi实现。

java原生SPI,使用ServiceLoader,METAINF/services实现

具体点大家可以继续搜索学习,这里只讲解使用方法

ServiceLoader + META-INF/services 的使用方法

我们可以通过ServiceLoader#load方法来加载子类。然后通过services文件夹中命名接口/抽象类内部写入要load的实现类名称,其实也可以通过classLoader将当前classpath所有类变量判断是否是其子类判断出要找的类

  1. 如果有多个实现类,我们就要指定具体某个实现类,那就要么在代码逻辑写死或者又引入新的配置逻辑
  2. 性能很差
    所有java提供了SPI机制,快速并且解耦的实现了选择性子类发现机制
举例com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy是hystrix中关于线程执行相关的策略抽象。是一个抽象类(接口也可以)
services指定其被抽象的类

我们可以使用自己的实现,通过SPI机制


我们可以使用自己的实现,通过SPI机制
实现当然也可以多个,但是SPI机制大部分用来实现多种实现,通过配置或者参数来选择一个实现使用,多个实现可以自由替换,上面的例子就是 jdbc抽象的Driver和Connection接口通过 driver-class-name来选择,

当然也可以是动态切换,例如下面要看的基于dubbo的spi方式


也可以多个

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 "";
}
使用注解的接口们
举例一个场景,apache-shenyu支持多种rule(即后端业务服务的url或者规则)注册方式,也就是业务服务有哪些接口可以通过各种方式上报给shenyu-admin服务,然后shenyu-admin会把数据同步给shenyu-bootstrap服务(真正做网关的服务)分别提供了,nacos,http,consul,etcd,zookeeper多种rule上报实现,只需要通过配置选择即可。那么在shenyu的逻辑代码中只需要对抽象出来的ShenyuClientServerRegisterRepository接口统一处理,不需要知道不同上报方式的差别,这就是java的抽象方式,通过接口或者抽象类(尽量使用接口)的方式抽象后不需要if,switch来判断了,这种只要选择一个实现的逻辑使用SPI机制精简了代码,解耦了模块间的依赖,一下图中多种上报实现只需要在实现类中关注
上报方式
apache-shenyu中很多这种SPI抽象逻辑,例如还有RateLimiterAlgorithm接口将限流逻辑抽象, 限流实现

下面看代码


shenyu的spi代码
@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);
    }
}
dubbo的spi是一个kv

下面看看核心代码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;
        }
    }
}

总结

  1. apache-shenyu基本沿用dubbo的spi机制,通过@SPI,@Join + META-INF/指定名称 的目录加载,但是文件内容使用kv形式,也提供多实现通过文件配置中的不同key区别
  2. 有一个spi工厂提供可能存在更多的spi实现
  3. 使用泛型保证类型安全,不同的抽象父类,实例化出不同的ExtensionLoader加载器来处理
  4. 使用缓存,线程安全容器,双重校验等保证单例

相关文章

网友评论

      本文标题:apache-shenyu之SPI

      本文链接:https://www.haomeiwen.com/subject/buokprtx.html