目录
典型回答
从面向接口编程说起
接口位于调用方所在的包中
接口位于实现方所在的包中
注意
如何定义一个SPI
SPI的实现原理
SPI的应用场景
-
典型回答
- Java中区分 API 和 SPI,通俗的讲:API 和 SPI 都是相对的概念,他们的差别只在语义上,API 直接被应用开发人员使用,SPI 被框架扩展人员使用
- API Application Programming Interface
- 大多数情况下,都是实现方来制定接口并完成对接口的不同实现,调用方仅仅依赖却无权选择不同实现
- SPI Service Provider Interface
- 而如果是调用方来制定接口,实现方来针对接口来实现不同的实现;调用方来选择自己需要的实现方
-
从面向接口编程说起
- 接口调用
- 当选择在调用方和实现方中间引入 接口
- 上图没有给出“接口”应该位于哪个“包”中,从纯粹的可能性上考虑,有三种选择:
- 1-接口位于实现方所在的包中
- 2-接口位于调用方所在的包中
- 3-接口位于独立的包中
-
接口位于调用方所在的包中
- 对于类似这种情况下接口,将其称为SPI
- SPI的规则如下:
- 概念上更依赖调用方
- 组织上位于调用方所在的包中
- 实现位于独立的包中
- 常见的例子是:插件模式的插件;如:
- 数据库驱动 Driver
- 日志 Log
- dubbo扩展点开发
-
接口位于实现方所在的包中
- 对于类似这种情况下的接口,将其称作为API
- API的规则如下:
- 概念上更接近实现方
- 组织上位于实现方所在的包中
-
注意
- SPI 和 API 也不一定是接口,这里都是指狭义的具体的接口
- API是你可以引用来达成某个目标的对象,它清楚地告诉你它可以完成什么目标,用户可以即插即用
- 而SPI则指定了你要达成某个目标你必须要继承或实现它,它一般只供某些特殊用途的接口开发商使用(当然有些公司或个人也可以自己继承或实现SPI来完成特定功能)
-
如何定义一个SPI
- 步骤1
- 定义一组接口 (假设是org.foo.demo.IShout),并写出接口的一个或多个实现,(假设是org.foo.demo.animal.Dog、org.foo.demo.animal.Cat)
- 步骤2
- 在src/main/resources/ 下建立 /META-INF/services 目录,新增一个以接口命名的文件 (org.foo.demo.IShout文件),内容是要应用的实现类(这里是org.foo.demo.animal.Dog和org.foo.demo.animal.Cat,每行一个类)
- org.foo.demo.animal.Dog
- org.foo.demo.animal.Cat
- 步骤3
- 使用 ServiceLoader 来加载配置文件中指定的实现
- 代码输出:
-
SPI的实现原理
- 看ServiceLoader类的签名类的成员变量:
- 参考具体源码,梳理一下,实现的流程如下:
- 1-应用程序调用ServiceLoader.load方法,ServiceLoader.load方法内先创建一个新的ServiceLoader,并实例化该类中的成员变量,包括:
- a. loader(ClassLoader类型,类加载器)
- b. acc(AccessControlContext类型,访问控制器)
- c. providers(LinkedHashMap类型,用于缓存加载成功的类)
- d. lookupIterator(实现迭代器功能)
- 2-应用程序通过迭代器接口获取对象实例
- a. ServiceLoader先判断成员变量providers对象中(LinkedHashMap类型)是否有缓存实例对象,如果有缓存,直接返回
- b. 如果没有缓存,执行类的装载:
- ⅰ. 读取META-INF/services/下的配置文件,获得所有能被实例化的类的名称
- ⅱ. 通过反射方法Class.forName()加载类对象,并用instance()方法将类实例化
- ⅲ. 把实例化后的类缓存到providers对象中(LinkedHashMap类型)
- ⅳ. 然后返回实例对象
-
SPI的应用场景
- 概括地说,适用于:调用者根据实际使用需要,启用、扩展、或者替换框架的实现策略
- 比较常见的例子:
- 1-数据库驱动加载接口实现类的加载
- 2-JDBC加载不同类型数据库的驱动
- 3-日志门面接口实现类加载
- 4-SLF4J加载不同提供商的日志实现类
- Spring
- Spring中大量使用了SPI
- 比如:对servlet3.0规范对ServletContainerInitializer的实现、自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)等
- Dubbo
- Dubbo中也大量使用SPI的方式实现框架的扩展,不过它对Java提供的原生SPI做了封装,允许用户扩展实现Filter接口