自己动手实现2 - SPI 可扩展框架

SPI 可扩展框架:是各种 rpc 框架用于实现高可扩展性的手段。

本节实现最基本的 SPI 机制,包含四个基本部分:

  • @Extension:该注解添加在可扩展接口的 实现上,并且通常会添加一个 alias 用于标识一个扩展实现类的 key(在 core-spi 中仅仅适用于标识)
  • ExtensionClass:一个 实现类 会最终被其 ExtensionLoader 加载为一个 ExtensionClass,存储在其
    ExtensionLoader 中,并且包含了实例化 ExtensionClass 存储的 实现类 的方法
  • ExtensionLoader:每一个 可扩展接口 都有且仅有一个 ExtensionLoader,用于从相应接口的 SPI 配置文件中读取配置内容并且将每一行解析成一个 ExtensionClass(每一个 ExtensionClass 对应一个实现,SPI 配置文件中的每一行配置一个实现类),之后存储 <alias, ExtensionClass> 配置对到 Map<String, ExtensionClass<T>> 容器中
  • ExtensionLoaderFactory:用来获取或者创建 ExtensionLoader,将创建好的 ExtensionLoader 放置在 Map<Class, ExtensionLoader> 容器中

代码地址:https://github.com/zhaojigang/core-spi

一、Extension

/**
 * 扩展接口实现类的标识
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface Extension {
    /**
     * 在 core-spi 中不起作用,仅用作 alias 标识
     *
     * @return alias
     */
    String value();
}

二、ExtensionClass

/**
 * 扩展实现类类 Class 包装类
 *
 * @param <T>
 */
public class ExtensionClass<T> {
    /**
     * 真实的扩展实现类类 Class
     */
    private Class<? extends T> clazz;

    public ExtensionClass(Class<? extends T> clazz) {
        this.clazz = clazz;
    }

    /**
     * 调用无参构造器创建扩展实现类实例
     *
     * @return 扩展实现类实例
     */
    public T getExtInstance() {
        if (clazz == null) {
            throw new RuntimeException("Class of ExtensionClass is null");
        }
        try {
            return clazz.newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
            throw new RuntimeException("create " + clazz.getCanonicalName() + " instance error", e);
        }
    }

    /**
     * 调用有参构造器创建扩展实现类实例
     *
     * @return 扩展实现类实例
     */
    public T getExtInstance(Class[] argTypes, Object[] args) {
        if (clazz == null) {
            throw new RuntimeException("Class of ExtensionClass is null");
        }
        try {
            Constructor<? extends T> constructor = clazz.getDeclaredConstructor(argTypes);
            return constructor.newInstance(args);
        } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
            throw new RuntimeException("create " + clazz.getCanonicalName() + " instance error", e);
        }
    }
}

三、ExtensionLoader

/**
 * 扩展加载器
 *
 * @param <T>
 */
public class ExtensionLoader<T> {
    private static final Logger LOGGER = LoggerFactory.getLogger(ExtensionLoader.class);
    /**
     * 当前扩展加载器处理的扩展接口名
     */
    private String interfaceName;
    /**
     * interfaceName 扩展接口下的所有实现
     */
    private Map<String, ExtensionClass<T>> alias2ExtensionClass;

    public ExtensionLoader(Class<T> interfaceClass) {
        this.interfaceName = interfaceClass.getName();
        this.alias2ExtensionClass = new ConcurrentHashMap<>();
        // 此处只指定了一个 spi 文件存储的路径
        loadFromFile("META-INF/services/corespi/");
    }

    private void loadFromFile(String spiConfigPath) {
        String spiFile = spiConfigPath + interfaceName;
        try {
            ClassLoader classLoader = this.getClass().getClassLoader();
            loadFromClassLoader(classLoader, spiFile);
        } catch (Exception e) {
            LOGGER.error("load file {} error, ", spiFile, e);
        }
    }

    private void loadFromClassLoader(ClassLoader classLoader, String spiFile) throws IOException {
        // 读取多个spi文件
        Enumeration<URL> urls = classLoader != null ? classLoader.getResources(spiFile) : ClassLoader.getSystemResources(spiFile);
        if (urls == null) {
            return;
        }
        while (urls.hasMoreElements()) {
            // 每一个 url 是一个文件
            URL url = urls.nextElement();
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream(), "UTF-8"))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    // 读取文件中的每一行
                    readLine(line);
                }
            } catch (Exception e) { // 文件需要整体失败,不能单行失败
                LOGGER.error("load {} fail,", spiFile, e);
            }
        }
    }

    private void readLine(String line) throws ClassNotFoundException {
        // spi 文件需要严格按照 alias=className 格式编写
        String[] aliasAndClassName = line.split("=");
        // 任何不是 alias=className 格式的行都直接过滤掉
        if (aliasAndClassName == null || aliasAndClassName.length != 2) {
            return;
        }
        String alias = aliasAndClassName[0].trim();
        String className = aliasAndClassName[1].trim();
        Class<?> clazz = Class.forName(className, false, this.getClass().getClassLoader());

        // 必须具有扩展注解
        Extension extension = clazz.getAnnotation(Extension.class);
        if (extension == null) {
            LOGGER.error("{} need @Extension", className);
            return;
        }

        // 创建 ExtensionClass
        ExtensionClass<T> extensionClass = new ExtensionClass<>((Class<? extends T>) clazz);
        alias2ExtensionClass.putIfAbsent(alias, extensionClass);
    }

    public T getExtension(String alias) {
        ExtensionClass<T> extensionClass = alias2ExtensionClass.get(alias);
        if (extensionClass == null) {
            throw new RuntimeException("Not found extension of " + interfaceName + " named: \"" + alias + "\"!");
        }
        return extensionClass.getExtInstance();
    }

    public T getExtension(String alias, Class[] argTypes, Object[] args) {
        ExtensionClass<T> extensionClass = alias2ExtensionClass.get(alias);
        if (extensionClass == null) {
            throw new RuntimeException("Not found extension of " + interfaceName + " named: \"" + alias + "\"!");
        }
        return extensionClass.getExtInstance(argTypes, args);
    }
}
  • core-spi 规定了 spi 文件存储的唯一路径,且指定了 alias=className 这样的唯一格式 - 由于这样的强规定,@Extension 注解中 value 属性(即 alias)不再有用,只是作为一个标识,可直接去掉
  • SOFARPC 除了实现了基本的 spi 机制之外,还实现了如下功能,具体见:SOFARPC 源码分析2 - SPI 扩展机制
  • spi 文件路径的可配置化
  • spi 配置文件的名字的可配置化(默认是可扩展接口全类名)
  • spi 配置的格式的多样性
  • 高 order 的实现类覆盖低 order 实现的功能
  • 排斥掉指定的低 order 的扩展点的功能
  • 实现类是否需要编码
  • 实现类是否需要单例

四、ExtensionLoaderFactory

/**
 * 创建 ExtensionLoader 的工厂
 */
public class ExtensionLoaderFactory {
    /**
     * key: 扩展接口 Class
     * value: 扩展接口对应的 ExtensionLoader(单例,每一个扩展接口有一个 ExtensionLoader)
     */
    private static final Map<Class, ExtensionLoader> LOADER_MAP = new ConcurrentHashMap<>();

    /**
     * 获取或创建 clazz 扩展接口的 ExtensionLoader
     *
     * @param clazz 扩展接口
     * @param <T>
     * @return clazz 扩展接口的 ExtensionLoader
     */
    public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> clazz) {
        ExtensionLoader<T> loader = LOADER_MAP.get(clazz);
        if (loader == null) {
            synchronized (ExtensionLoaderFactory.class) {
                if (loader == null) {
                    loader = new ExtensionLoader<>(clazz);
                    LOADER_MAP.put(clazz, loader);
                }
            }
        }
        return loader;
    }
}

五、测试

image.png

1、可扩展接口

package com.core;

public interface LoadBalancer {
    String selectProvider();
}

2、扩展接口实现类

package com.core;

@Extension("random")
public class RandomLoadBalancer implements LoadBalancer {
    @Override
    public String selectProvider() {
        return "random: 10.211.55.10:8080";
    }
}
@Extension("hasArgs")
public class HasArgsLoadBalancer implements LoadBalancer {
    private String tag;

    public HasArgsLoadBalancer(String tag){
        this.tag = tag;
    }

    @Override
    public String selectProvider() {
        return "hasArgs: 10.211.55.11:8080 - " + tag;
    }
}

3、spi 配置文件

文件位置:META-INF/services/corespi/com.core.LoadBalancer

random = com.core.RandomLoadBalancer
hasArgs = com.core.HasArgsLoadBalancer

4、测试 SPI

public class TestSPI {
    @Test
    public void testMainFunc() {
        // 1. 获取 LoadBalancer 的 ExtensionLoader
        ExtensionLoader<LoadBalancer> loader = ExtensionLoaderFactory.getExtensionLoader(LoadBalancer.class);
        // 2. 根据 alias 获取具体的 Extension
        LoadBalancer loadBalancer = loader.getExtension("random");
        // 3. 使用具体的 loadBalancer
        System.out.println(loadBalancer.selectProvider());

        // 4. 根据 alias 获取具体的 Extension
        LoadBalancer hasArgsLoadBalancer = loader.getExtension("hasArgs", new Class[]{String.class}, new Object[]{"haha"});
        // 5. 使用具体的 loadBalancer
        System.out.println(hasArgsLoadBalancer.selectProvider());
    }
}

六、通用的策略工厂

在实际业务开发中,我们通常会基于策略模式做一些事情,例如 上述的LoadBalancer接口,我们会做一个LoadBalancerFactory,类似代码如下

class LoadBalancerFactory {
  private Map<LoadBalancerType, LoadBalancer> registry;
  static {
       // 读取 LoadBalancer 实现类,存储到registry中,后续通过registry.get(LoadBalancerType) 进行调用
  }
}

如果是另外一个其他需要策略化的接口,我们又会提供一个其他的 XxxFactory,里边的代码模式几乎与 LoadBalancerFactory 相同。其实 ExtensionLoaderFactory 本身就可以作为一个通用工厂,而策略实现类的数据的读取可以通过注解扫描直接获取,不需要通过 spi 文件进行,简化业务开发。核心变化如下。完整代码见:https://github.com/zhaojigang/x-toolsets

public class ExtensionLoader<T> {
    public ExtensionLoader(Class<T> interfaceClass) {
        this.interfaceName = interfaceClass.getName();
        this.alias2ExtensionClass = new ConcurrentHashMap<>();
        // 此处只指定了一个 spi 文件存储的路径
        loadFromClassLoader(interfaceClass, Lists.newArrayList("strategy.factory"));
    }

    private void loadFromClassLoader(Class<T> interfaceClass, List<String> scanPaths) {
        Reflections reflections = new Reflections(scanPaths, this.getClass().getClassLoader());
        Set<Class<? extends T>> classes = reflections.getSubTypesOf(interfaceClass);
        for (Class<? extends T> clazz : classes) {
            // 必须具有扩展注解
            Extension extension = clazz.getAnnotation(Extension.class);
            if (extension == null || StringUtils.isBlank(extension.value())) {
                throw new RuntimeException(clazz.getName() + " need @Extension");
            }
            // 创建 ExtensionClass
            ExtensionClass<T> extensionClass = new ExtensionClass<>(clazz);
            alias2ExtensionClass.putIfAbsent(extension.value(), extensionClass);
        }
    }
}

<!-- 简化指定路径进行反射数据扫描的包 -->
<dependency>
    <groupId>org.reflections</groupId>
    <artifactId>reflections</artifactId>
    <version>0.9.11</version>
</dependency>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,490评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,581评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,830评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,957评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,974评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,754评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,464评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,847评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,995评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,137评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,819评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,482评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,023评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,149评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,409评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,086评论 2 355

推荐阅读更多精彩内容

  • 我刷过你的朋友圈 存过你的照片 搜过你的微博 听过你喜欢的歌 看过你喜欢的书 买过你穿过的品牌衣服 记下了你去过的...
    粗腿妹阅读 152评论 0 1
  • 一、美乐家是复制的生意. 1.不可复制的事情不要做.(例如:填写入会资料必需自己亲力亲为 去学会 自己以后需要复制...
    apple宝阅读 3,018评论 0 0
  • 切记 !!!typeof返回的值是一个字符串!!!出错的代码: 此段代码为获取子元素的兼容代码,本来判断一个属...
    祝我好运zz阅读 512评论 2 0
  • 我曾经害怕发生的事情 在它还没发生之前 我是如何地顽强 顽强地抵抗 不让自己掉入无尽头的深渊 在它降临之后 我是如...
    恋文阅读 202评论 0 0