apache-shenyu之SPI

從這篇文章開始會從頭開始以 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接口


image.png

以上為不同數(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;
}
Connection接口方法

Connection接口方法

還有一堆方法,感興趣的直接看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所有類變量判斷是否是其子類判斷出要找的類

  1. 如果有多個實現(xiàn)類,我們就要指定具體某個實現(xiàn)類,那就要么在代碼邏輯寫死或者又引入新的配置邏輯
  2. 性能很差
    所有java提供了SPI機制,快速并且解耦的實現(xiàn)了選擇性子類發(fā)現(xiàn)機制
舉例com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy是hystrix中關于線程執(zhí)行相關的策略抽象。是一個抽象類(接口也可以)
services指定其被抽象的類

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


我們可以使用自己的實現(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接口將限流邏輯抽象,
限流實現(xiàn)

下面看代碼


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

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

總結

  1. apache-shenyu基本沿用dubbo的spi機制,通過@SPI,@Join + META-INF/指定名稱 的目錄加載,但是文件內容使用kv形式,也提供多實現(xiàn)通過文件配置中的不同key區(qū)別
  2. 有一個spi工廠提供可能存在更多的spi實現(xiàn)
  3. 使用泛型保證類型安全,不同的抽象父類,實例化出不同的ExtensionLoader加載器來處理
  4. 使用緩存,線程安全容器,雙重校驗等保證單例
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容