一文快速理解Java SPI(Service Provider Interface)机制

606 阅读3分钟

一、10秒看一个demo

run一下打印100。这是哪里来的呢,关注到引入的TestJar.jar,类InstanceMap实现了size方法,return 100;

不熟悉SPI的同学可能不理解这2者是怎么关联上的。细心的已经发现了jar包下面还有个配置文件,没错就是它。

二、什么是SPI

全称Service Provider Interface,是java的一种服务发现机制,符合java规范JSR-223.如下图,消费者即调用方(本例中的main方法),标准接口(java.util.Map),服务实例(只有一个,InstanceMap).SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。配置内容也不藏了,在META-INF.services/Java.util.Map里加一段,当然可以多个服务实例咯

com.jay.InstanceMap

三、使用场景

  1. JDBC 4.0

JDBC 4.0 Drivers 必须包括 META-INF/services/java.sql.Driver 文件。此文件包含 java.sql.Driver 的 JDBC 驱动程序实现的名称。 例如,要加载 my.sql.Driver 类,META-INF/services/java.sql.Driver 文件需要包含下面的条目:my.sql.Driver 应用程序不再需要使用 Class.forName() 显式地加载 JDBC 驱动程序。

  1. JS等引擎

Java8 引入的Nashorn引擎,其中的getEngineByName也是使用该方式,在Nashorn.jar中,services下包含该条目jdk.nashorn.api.scripting.NashornScriptEngineFactory,感兴趣可以自行点进去看一下,下面附带一个Nashor引擎创建代码片段

ScriptEngine engine= new ScriptEngineManager().getEngineByName("Nashorn");
  1. 其他

四、原理

原理解析可能会迟到,但从不会缺席;

其实我们自己先不妨猜测一下,首先入口是ServiceLoad.load,无非是加载类嘛,我们已经有了接口路径了,那么就去services里找,找到了创建出来,再深一步考虑,执行时反复调用load怎么办呢,做个缓存呗.一句话,就是提供接口名找实例的功能.下面的过程直接copy:

    1. 应用程序调用ServiceLoader.load方法

    • ServiceLoader.load方法内先创建一个新的ServiceLoader,并实例化该类中的成员变量,包括:
    • loader(ClassLoader类型,类加载器)
    • acc(AccessControlContext类型,访问控制器)
    • providers(LinkedHashMap类型,用于缓存加载成功的类)
    • lookupIterator(实现迭代器功能)
    1. 应用程序通过迭代器接口获取对象实例
    • ServiceLoader先判断成员变量providers对象中(LinkedHashMap类型)是否有缓存实例对象,如果有缓存,直接返回。
    • 如果没有缓存,执行类的装载:
    • 读取META-INF/services/下的配置文件,获得所有能被实例化的类的名称
    • 通过反射方法Class.forName()加载类对象,并用instance()方法将类实例化
    • 把实例化后的类缓存到providers对象中(LinkedHashMap类型)
    • 然后返回实例对象。

五、优劣势

总而言之,调用者根据实际使用需要,启用、扩展、或者替换框架的实现策略,同时可以加载多个,可根据实例全路径分配给调用者不同的实现;但是正因如此,可能加载了一堆但是都没用到,这种情况倒也没太大关系,需要注意的是ServiceLoader类的实例是线程不安全的。

六、写在最后

说是理解SPI,实际更想表达的是以小见大,不要上来就是什么原理,规则。动不动就被前者鼻子走,往往一些一开始觉得高深莫测,或者隐晦难懂的东西可能也只是比较基础的东西。多多发挥自己的思考能力吧。当然本文解析可能对于某些人来说过于表面或者谬误甚多,不过鸡汤该灌还是要灌的。装完就跑,撤。