Java SPI介绍使用

1.SPI的定义

A service provider interface(SPI) is the set of public interfaces and abstract classes that a service defines.
SPI是定义了一组公共的接口和抽象类的服务类。其实就是定义了一组公共的接口或者抽象方法,例如Java Database Connectivity (JDBC) 规范,对于不同的数据库厂商而言是SPI,但是对于使用JDBC的开发者而言又是API。
SPI描述了服务厂商需要拓展实现的内容规范,API则描述了如何、怎么使用的内容。

2. 入门示例

javax.sound.sampledjavax.sound.midi包是提供给那些需要处理音频工作的开发者使用,软件包提供了信息获取、控制和访问音频相关接口。此外Java Sound API同样提供了javax.sound.sampled.spijavax.sound.midi.spi相关软件包,其定义了音频相关的抽象类。开发者想要提供新的音频相关的服务,只需要实现在SPI包下的具体类的子类即可。 这些类以及支持新服务的附加类,都被放到Jar文件中,并包含了对所附加了对一个或者多个服务的描述。当这个Jar包被载入到用户的CLASSPATH下,运行时系统就会自动的载入将新的服务类(扩展了Java运行时系统的功能)。一旦新的服务类被载入,它可以像其他已经预先存在的类一样被正常访问。通过调用AudioSystemMidiSystem类下的方法,调用者可以获取服务类的相关信息,也可以创建出服务类的实例。应用程序不需要也不应该直接引用位于SPI包中的类来使用已经载入的服务。

举个栗子:现在有一个Acme软件公司提供了新的服务类,允许应用程序读取新格式的音频文件。新的类就假设称为AcmeAudioFileReader,其提供实现了AudioFileReader类的所有定义的方法(AudioFileReader仅有两个方法getAudioFileFormatgetAudioInputStream)。当一个应用程序试图读取音频文件,碰巧格式就是Acme时,就会调用AudioSystem类的方法来访问文件及其相关信息。AudioSystem.getAudioInputStreamAudioSystem.getAudioFileFormat方法提供了一个读取音频流的标准API,在载入了AcmeAudioFileReader类的情况下,该接口被扩展为透明地支持读取新格式的音频文件。开发者不需要直接访问新注册的SPI类:通过AudioSystem对象的方法将参数传递给AcmeAudioFileReader

例子中使用这些工厂类的目的是什么?为什么不让应用开发者直接访问新的服务类?这虽然是一种可行的途径,但是通过统一的入口去管理和载入这些服务类,使得开发人员不必知道每个载入的服务类是做什么的。开发者只需要通过统一的入口使用在这些服务类拿到想要的结果即可,与此同时厂商也可以借助这种架构有效的去管理包下可用资源。

通常使用新的音频类对应用程序是透明的。 例如,想象一下,应用程序开发人员想要从文件中读取音频流的情况。 假设thePathName标识一个音频输入文件,程序大概是这样子的:

File theInFile = new File(thePathName);
AudioInputStream theInStream = AudioSystem.getAudioInputStream(theInFile); 

在这个场景下,AudioSystem检测到哪些被载入的服务可以读取这个文件,并要求对应的服务类将音频数据以一个AudioInputStream对象返回.开发者可能并不知道甚至不在意输入的音频文件使用了新的第三方提供的格式。程序的第一次使用到的流对象是通过AudioSystem来完成,而后续的所有的访问流和流属性的都是通过AudioInputStream的方法来完成。这两个都是javax.sound.sampled API的标准对象;所有可能的针对新文件格式的特殊处理的细节都已经被完全的屏蔽了。

3. 窥探AudioSystem的秘密

上面例子中AudioSystem究竟有什么神奇的魔法,可以知道哪些被载入的服务类可以读取这个文件,所谓源码面前无秘密,直接"上菜"。

3.1 AudioSystem是如何找到对应解析类

public static AudioInputStream getAudioInputStream(File file)
        throws UnsupportedAudioFileException, IOException {

        List providers = getAudioFileReaders();
        AudioInputStream audioStream = null;

        for(int i = 0; i < providers.size(); i++ ) {
            AudioFileReader reader = (AudioFileReader) providers.get(i);
            try {
                audioStream = reader.getAudioInputStream( file ); // throws IOException
                break;
            } catch (UnsupportedAudioFileException e) {
                continue;
            }
        }

        if( audioStream==null ) {
            throw new UnsupportedAudioFileException("could not get audio input stream from input file");
        } else {
            return audioStream;
        }
    }

首先通过getAudioFileReaders()方法获取所有AudioFileReader第三方实现类的实例,然后循环遍历AudioFileReader对象,如果当前对象不支持当前文件格式则抛出UnsupportedAudioFileException异常,捕获异常继续循环。神奇的魔法已经被揭开:AudioSystem在这里其实隐式的约定,第三方实现类如果不支持当前文件格式的读取则需要抛出特定的异常作为信号。

3.2 解析类是如何被载入的

通过追踪getAudioFileReaders(),追溯到如下关键代码,

static synchronized <T> List<T> getProviders(final Class<T> var0) {
        ArrayList var1 = new ArrayList(7);
        PrivilegedAction var2 = new PrivilegedAction<Iterator<T>>() {
            public Iterator<T> run() {
                return ServiceLoader.load(var0).iterator();
            }
        };
        final Iterator var3 = (Iterator)AccessController.doPrivileged(var2);
        PrivilegedAction var4 = new PrivilegedAction<Boolean>() {
            public Boolean run() {
                return var3.hasNext();
            }
        };

        while(((Boolean)AccessController.doPrivileged(var4)).booleanValue()) {
            try {
                Object var5 = var3.next();
                if (var0.isInstance(var5)) {
                    var1.add(0, var5);
                }
            } catch (Throwable var6) {
                ;
            }
        }

        return var1;
    }

同样只需要关心主要代码ServiceLoader.load(var0).iterator();,借助于ServiceLoader来完成的载入实例的工作。

4. 使用Java的SPI机制

4.1 使用步骤

1. SPI定义
2. 统一访问入口
3. 实现SPI接口并加以描述实现了哪个SPI。(描述文件约定如下:放到META-INF/services/这个路      
   径下;并以接口全路径作为文件名称,以实现接口类的全路径作为文件内容)

4.2 实践

4.2.1 SPI定义:

public interface ObjectResourceStore<K ,V> {

    V read(K k) throws UnsupportedOperationException;

    void store(K k, V v) throws UnsupportedOperationException;

}

定义了一个具有储取Object对象的功能的SPI,提供了read()store()方法。

4.2.2 统一访问入口

public final class ObjectResourceStores<K,V> {

    private static synchronized List<ObjectResourceStore> loaderProviders() {
        List<ObjectResourceStore> providers = new ArrayList();
        Iterator iterator = ServiceLoader.load(ObjectResourceStore.class).iterator();
        while (iterator.hasNext()) {
            providers.add((ObjectResourceStore) iterator.next());
        }
        return providers;
    }

    public static<K, V> V read(K k) throws UnsupportedOperationException {
        List<ObjectResourceStore> providers = loaderProviders();
        for (int i = 0; i < providers.size(); i++) {
            ObjectResourceStore provider = providers.get(i);
            try {
                return (V) provider.read(k);
            } catch (UnsupportedOperationException e) {
                continue;
            }
        }
        throw new UnsupportedOperationException("can not find suitable provider");
    }

    public static<K, V> void store(K k, V v) throws UnsupportedOperationException {
        List<ObjectResourceStore> providers = loaderProviders();
        for (int i = 0; i < providers.size(); i++) {
            ObjectResourceStore provider = providers.get(i);
            try {
                provider.store(k, v);
                return;
            } catch (UnsupportedOperationException e) {
                continue;
            }
        }
        throw new UnsupportedOperationException("can not find suitable provider");
    }
}

4.2.3 实现SPI

public class ObjectResourceMemoryStore implements ObjectResourceStore{

    private static ConcurrentHashMap store = new ConcurrentHashMap<Object, Object>();


    @Override
    public Object read(Object o) throws UnsupportedOperationException {
        return store.get(o);
    }

    @Override
    public void store(Object o, Object o2) throws UnsupportedOperationException {
        store.put(o, o2);
    }
}

创建描述文件


文件内容如下:org.rhine.ObjectResourceMemoryStore

4.3 测试

测试代码如下:

public class TestSPI {

    public static void main(String[] args) {

        ObjectResourceStores.store(1, 1);
        System.out.println(ObjectResourceStores.read(1).toString());
    }

}

程序能够正确的输出结果:1

至此一个完整的SPI的demo已经完成,大家如果有兴趣可以继续深入的研究下SPI在开源框架中的具体实践。比如Spring、Dubbo都有非常典型的应用场景。

5. 写在最后

希望博客的内容能给广大的Java道友提供一些的帮助和提升。由于笔者水平有限,如果内容有误,希望大家批评指出,索要源码的同学可以直接私信。

参考文献

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,494评论 18 139
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,493评论 18 399
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,137评论 11 349
  • 今年是90后参加的最后一届高考,那些最小的90后也已经开始进入大学的校门,更多的已经开始踏入社会,从事着各...
    阳仔观天下阅读 741评论 0 0
  • 版权声明:本文为博主原创文章,未经博主允许不得转载。 一、算术运算 C语言一共有34种运算符,包括常见的加减乘除运...
    LeaderBiao阅读 564评论 0 1