一、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
三、使用场景
- 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 驱动程序。
- JS等引擎
Java8 引入的Nashorn引擎,其中的getEngineByName也是使用该方式,在Nashorn.jar中,services下包含该条目jdk.nashorn.api.scripting.NashornScriptEngineFactory,感兴趣可以自行点进去看一下,下面附带一个Nashor引擎创建代码片段
ScriptEngine engine= new ScriptEngineManager().getEngineByName("Nashorn");
- 其他
四、原理
原理解析可能会迟到,但从不会缺席;
其实我们自己先不妨猜测一下,首先入口是ServiceLoad.load,无非是加载类嘛,我们已经有了接口路径了,那么就去services里找,找到了创建出来,再深一步考虑,执行时反复调用load怎么办呢,做个缓存呗.一句话,就是提供接口名找实例的功能.下面的过程直接copy:
-
-
应用程序调用ServiceLoader.load方法
- ServiceLoader.load方法内先创建一个新的ServiceLoader,并实例化该类中的成员变量,包括:
- loader(ClassLoader类型,类加载器)
- acc(AccessControlContext类型,访问控制器)
- providers(LinkedHashMap类型,用于缓存加载成功的类)
- lookupIterator(实现迭代器功能)
-
-
- 应用程序通过迭代器接口获取对象实例
- ServiceLoader先判断成员变量providers对象中(LinkedHashMap类型)是否有缓存实例对象,如果有缓存,直接返回。
- 如果没有缓存,执行类的装载:
- 读取META-INF/services/下的配置文件,获得所有能被实例化的类的名称
- 通过反射方法Class.forName()加载类对象,并用instance()方法将类实例化
- 把实例化后的类缓存到providers对象中(LinkedHashMap类型)
- 然后返回实例对象。
五、优劣势
总而言之,调用者根据实际使用需要,启用、扩展、或者替换框架的实现策略,同时可以加载多个,可根据实例全路径分配给调用者不同的实现;但是正因如此,可能加载了一堆但是都没用到,这种情况倒也没太大关系,需要注意的是ServiceLoader类的实例是线程不安全的。
六、写在最后
说是理解SPI,实际更想表达的是以小见大,不要上来就是什么原理,规则。动不动就被前者鼻子走,往往一些一开始觉得高深莫测,或者隐晦难懂的东西可能也只是比较基础的东西。多多发挥自己的思考能力吧。当然本文解析可能对于某些人来说过于表面或者谬误甚多,不过鸡汤该灌还是要灌的。装完就跑,撤。