Java的SPI机制

什么是SPI

SPI全称Service Provider Interface,是Java提供的一种接口扩展机制。通过该机制可以将接口的定义与接口的实现分离,实现代码解耦。

使用方式

SPI的使用方法很简单,只需要按如下步骤即可:

  1. 定义接口

    package cn.bdqfork.spi;
    
    /**
     * @author bdq
     * @since 2020/3/2
     */
    public interface UserService {
        void sayHello();
    }
    
    
  2. 实现接口

    package cn.bdqfork.spi;
    
    /**
     * @author bdq
     * @since 2020/3/2
     */
    public class UserServiceImpl implements UserService {
        @Override
        public void sayHello() {
            System.out.println("hello");
        }
    }
    
    
  3. 编写扩展文件

    在META-INF/services目录下创建一个以接口全类名命名的文件,即com.test.service.UserService文件。

    - src
        -main
            -resources
                - META-INF
                    - services
                        - cn.bdqfork.spi.UserService
    

    文件的内容为实现类的全类名。

    cn.bdqfork.spi.UserServiceImpl
    
  4. 使用ServiceLoader加载服务

    package cn.bdqfork.spi;
    
    import java.util.ServiceLoader;
    
    /**
     * @author bdq
     * @since 2020/3/2
     */
    public class Main {
        public static void main(String[] args) {
            ServiceLoader<UserService> userServices = ServiceLoader.load(UserService.class);
            for (UserService userService : userServices) {
                userService.sayHello();
            }
        }
    }
    

SPI的缺点

虽然SPI机制可以很方便的将接口与其实现分离,但是却有两个缺点:

  • 在ServiceLoader加载的时候,会一次性将所有的Service都加载到JVM中,包括并不会用到的一些扩展。
  • 多线程并发访问的时候会有线程问题。

SPI的实现原理

SPI机制的原理实际上不是很难,整个加载扩展服务的过程如下:

  1. 扫描META-INF/services目录下的文件。
  2. 读取文件内容,加载服务实现的Class。
  3. 实例化服务实现。
  4. 返回服务实例。

实现一个简单SPI

了解了SPI的实现原理之后,便可以很简单的实现一个SPI,下面笔者介绍一下笔者实现的一个SPI,参考了Dubbo的SPI实现,但整体原理还是差不多的。

首先定义一个ExtensionLoader类以及相关属性:

/**
 * SPI扩展
 *
 * @author bdq
 * @since 2019-08-20
 */
public class ExtensionLoader<T> {
    /**
     * 扫描路径
     */
    private static final String PREFIX = "META-INF/extensions/";
    /**
     * 缓存
     */
    private static final Map<String, ExtensionLoader<?>> CACHES = new ConcurrentHashMap<>();
    /**
     * 扩展Class名称缓存
     */
    private final Map<Class<T>, String> classNames = new ConcurrentHashMap<>();
    /**
     * 扩展Class缓存
     */
    private final Map<String, Class<T>> extensionClasses = new ConcurrentHashMap<>();
    /**
     * 扩展实例缓存
     */
    private volatile Map<String, T> cacheExtensions;
    /**
     * 默认扩展名
     */
    private String defaultName;
    /**
     * 扩展服务类型
     */
    private Class<T> type;

    private ExtensionLoader(Class<T> type) {
        this.type = type;
    }
    // ......
}

然后实现加载扩展的方法,主要实现了扩展实现类的加载,具体代码如下:

    private void loadExtensionClasses() {
        if (classNames.size() > 0) {
            return;
        }
        try {
            ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

            // 加载扩展文件
            Enumeration<URL> urlEnumeration = classLoader.getResources(PREFIX + type.getName());

            while (urlEnumeration.hasMoreElements()) {

                URL url = urlEnumeration.nextElement();

                if (url.getPath().isEmpty()) {
                    throw new IllegalArgumentException("Extension path " + PREFIX + type.getName() + " don't exsist !");
                }

                // 读取文件内容
                if (url.getProtocol().equals("file") || url.getProtocol().equals("jar")) {

                    URLConnection urlConnection = url.openConnection();
                    Reader reader = new InputStreamReader(urlConnection.getInputStream());
                    BufferedReader bufferedReader = new BufferedReader(reader);

                    // 逐行读取
                    String line;
                    while ((line = bufferedReader.readLine()) != null) {

                        if (line.equals("")) {
                            continue;
                        }

                        // 过滤注释
                        if (line.contains("#")) {
                            line = line.substring(0, line.indexOf("#"));
                        }

                        // 解析key=value
                        String[] values = line.split("=");
                        String name = values[0].trim();
                        String impl = values[1].trim();

                        if (extensionClasses.containsKey(name)) {
                            throw new IllegalStateException("Duplicate extension named " + name);
                        }

                        // 加载Class
                        @SuppressWarnings("unchecked")
                        Class<T> clazz = (Class<T>) classLoader.loadClass(impl);

                        // 缓存Class
                        classNames.putIfAbsent(clazz, name);
                        extensionClasses.putIfAbsent(name, clazz);
                    }
                }
            }
        } catch (Exception e) {
            throw new IllegalArgumentException("Fail to get extension class from " + PREFIX + type.getName() + "!", e);
        }
    }

实例化所有的扩展,并缓存到Map中。

    /**
     * 获取所有扩展
     *
     * @return Map<String, T>
     */
    public Map<String, T> getExtensions() {
        if (cacheExtensions == null) {
            cacheExtensions = new ConcurrentHashMap<>();
            loadExtensionClasses();

            for (Map.Entry<String, Class<T>> entry : extensionClasses.entrySet()) {
                Class<T> clazz = entry.getValue();
                T instance;
                try {
                    instance = clazz.newInstance();
                } catch (InstantiationException | IllegalAccessException e) {
                    throw new IllegalStateException(e);
                }
                cacheExtensions.putIfAbsent(entry.getKey(), instance);
            }

        }
        return Collections.unmodifiableMap(cacheExtensions);
    }

提供按名获取扩展和获取默认扩展的方法。

    /**
     * 根据extensionName获取扩展实例
     *
     * @param extensionName 扩展名称
     * @return T
     */
    public T getExtension(String extensionName) {
        T extension = getExtensions().get(extensionName);
        if (extension != null) {
            return extension;
        }
        throw new IllegalStateException("No extension named " + extensionName + " for class " + type.getName() + "!");
    }

    /**
     * 根据extensionName获取扩展实例
     *
     * @return T
     */
    public T getDefaultExtension() {
        T extension = getExtensions().get(defaultName);
        if (extension != null) {
            return extension;
        }
        throw new IllegalStateException("No default extension named " + defaultName + " for class " + type.getName() + "!");
    }

最后提供一个工厂方法,创建ExtensionLoader实例。

    /**
     * 获取扩展接口对应的ExtensionLoader
     *
     * @param clazz 扩展接口
     * @param <T>   Class类型
     * @return ExtensionLoader<T>
     */
    @SuppressWarnings("unchecked")
    public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> clazz) {
        String className = clazz.getName();

        if (!clazz.isInterface()) {
            throw new IllegalArgumentException("Fail to create ExtensionLoader for class " + className
                    + ", class is not Interface !");
        }

        SPI spi = clazz.getAnnotation(SPI.class);

        if (spi == null) {
            throw new IllegalArgumentException("Fail to create ExtensionLoader for class " + className
                    + ", class is not annotated by @SPI !");
        }

        ExtensionLoader<T> extensionLoader = (ExtensionLoader<T>) CACHES.get(className);

        if (extensionLoader == null) {
            CACHES.putIfAbsent(className, new ExtensionLoader<>(clazz));
            extensionLoader = (ExtensionLoader<T>) CACHES.get(className);
            extensionLoader.defaultName = spi.value();
        }

        return extensionLoader;
    }

SPI注解的定义如下:

/**
 * 注解在扩展类上,表示可以扩展
 *
 * @author bdq
 * @since 2019/9/21
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SPI {
    /**
     * 默认扩展名
     */
    String value() default "";
}

以上是一个简单SPI的实现代码,使用方法也很简单,跟JDK的使用方法差不多,区别是需要使用@SPI注解在服务接口上表明扩展需求,且配置文件存储在META-INF/extensions文件夹下,具体使用方法如下。

  1. 首先定义接口。

    package com.github.bdqfork.core.extension;
    
    /**
     * @author bdq
     * @since 2020/2/22
     */
    @SPI
    public interface IExtensionTest {
    }
    
  2. 实现接口

    package com.github.bdqfork.core.extension;
    
    /**
     * @author bdq
     * @since 2020/2/22
     */
    public class ExtensionTestImpl1 implements IExtensionTest {
    }
    
    package com.github.bdqfork.core.extension;
    
    /**
     * @author bdq
     * @since 2020/2/22
     */
    public class ExtensionTestImpl2 implements IExtensionTest {
    }
    
  3. 编写配置文件

    在META-INF/services目录下创建一个以接口全类名命名的文件,即com.github.bdqfork.core.extension.ExtensionLoaderTest文件。

    - src
        -main
            -resources
                - META-INF
                    - extensions
                        - com.github.bdqfork.core.extension.IExtensionTest
    

    文件内容如下:

    imp1=com.github.bdqfork.core.extension.ExtensionTestImpl1
    imp2=com.github.bdqfork.core.extension.ExtensionTestImpl2
    

    这里为每一个扩展实例提供了名称。

  4. 通过ExtensionLoader获取扩展实例

    package com.github.bdqfork.core.extension;
    
    import org.junit.jupiter.api.Test;
    
    import static org.junit.jupiter.api.Assertions.*;
    
    class ExtensionLoaderTest {
    
        @Test
        public void getExtension() {
            ExtensionLoader<IExtensionTest> extensionLoader = ExtensionLoader.getExtensionLoader(IExtensionTest.class);
            IExtensionTest iExtensionTest = extensionLoader.getExtension("imp1");
            assert iExtensionTest != null;
        }
    
    }
    

以上就是一个简单SPI的实现,在笔者的ioc容器festival和rpc框架hamal中均使用到了SPI,提供扩展服务,感兴趣的同学欢迎查看笔者的项目。

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

推荐阅读更多精彩内容

  • SPI是什么 SPI的英文名称是Service Provider Interface,是Java 内置的服务发现机...
    GallenZhang阅读 334评论 0 0
  • 最近在阅读Dubbo框架源代码时,经常看到@Spi,查了一下SPI: Service Provider Inter...
    kobe0429阅读 311评论 0 0
  • SPI的概念 英文全称为Service Provider Interface 是JDK内置的一种服务提供发现机制 ...
    孙先森不可不弘毅阅读 233评论 0 0
  • 当服务的提供者,提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以...
    男人三饼阅读 267评论 0 2
  • 1.SPI简述 SPI(Service Provider Interface),是JDK内置的一种服务提供发现机制...
    zhglance阅读 460评论 0 0