# SPI 可以用来做什么
在设计一个框架或者组件,甚至是项目中的某些模块时,经常都需要考虑扩展性, 而扩展性好应该符合以下两点:
- 作为框架的维护者,在添加一个新功能时,只需要添加一些新代码,而不用大量的修改现有的代码,即符合开闭原则。
- 作为框架的使用者,在添加一个新功能时,不需要去修改框架的源码,在自己的工程中添加代码或者修改配置即可。
而 Java SPI 可以很好的满足以上两点,从而达到良好的扩展性。
Java SPI(Service Provider Interface)是 JDK 内置的一种动态加载扩展点的实现,是一种“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。对扩展性支持非常友好,想要扩展实现,新只需要增实现接口,然后把接口的实现描述给JDK就行了。
(大致原理就是:在ClassPath的META-INF/services目录下放置一个与接口同名的文本文件,文件的内容为接口的实现类,多个实现类用换行符分隔。JDK中使用 java.util.ServiceLoader 来加载具体的实现。)
使用场景
概括地说,适用于:调用者根据实际使用需要,启用、扩展、或者替换框架的实现策略。比较常见的例子:
JDBC自动加载不同类型的数据库驱动, mysql-connector-java-xxx.jar
日志门面接口实现类加载,SLF4J加载不同提供商的日志实现类
Dubbo中在Java SPI 的基础上做了加强, 实现了根据方法参数或者配置来决定该使用哪个扩展。比如LoadBalance 做到了根据调用者参数的指定来应用不同的负债均衡策略。
# 如何实现一个自定义 SPI
这里由于现实情况不同厂商的实现肯定是分开的,所以不同厂商我是建了不同的 maven-modules, 目录结果如下:
1. 定义一个接口IRepository用于实现数据储存 (类似于强制制定了一种规范,你们不同的数据厂商可以有不同的实现,但是必须按照我这个标准接口来)
1 2 3 4 5 6 7 8 9 | public interface IRepository { /** * 建立连接 * @param url */ void connect(String url); } |
2. 不同厂商分别提供了自己不同的实现,MysqlRepository、OracleRepository、MongoRepository
MysqlRepository 实现:
1 2 3 4 5 6 7 | public class MysqlRepository implements IRepository { public void connect(String url) { System.out.println("connect " + url + " to Mysql"); } } |
在 Resources 下新建一个 META-INF/services/com.crazyfzw.spi.api.IRepository 文件, 内容为:
1
| com.crazyfzw.spi.apiimpl.mysql.MysqlRepository
|
OracleRepository 实现:
1 2 3 4 5 6 7 | public class OracleRepository implements IRepository { public void connect(String url) { System.out.println("connect " + url + " to Oracle"); } } |
在 Resources 下新建一个 META-INF/services/com.crazyfzw.spi.api.IRepository 文件, 内容为:
1
| com.crazyfzw.spi.apiimpl.oracle.OracleRepository
|
MongoRepository 实现:
1 2 3 4 5 6 7 | public class MongoRepository implements IRepository { public void connect(String url) { System.out.println("connect " + url + " to Mongo"); } } |
在 Resources 下新建一个 META-INF/services/com.crazyfzw.spi.api.IRepository 文件, 内容为:
1
| com.crazyfzw.spi.apiimpl.oracle.OracleRepository
|
3. 在应用的pom 文件中根据需要选择引入不同厂商的maven 依赖 (通过切换pom引入可以实现不同厂商的切换)
这里是invoker-test 模块的pom:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | <dependencies> <dependency> <groupId>com.crazyfzw</groupId> <artifactId>interface-standard</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>com.crazyfzw</groupId> <artifactId>mysql-repository</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>com.crazyfzw</groupId> <artifactId>oracle-repository</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>com.crazyfzw</groupId> <artifactId>mongo-repository</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies> |
4. 在主调用类中通过 通过ServiceLoader加载IRepository 的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public class MainTest { public static void main(String[] args) { ServiceLoader<IRepository> serviceLoader = ServiceLoader.load(IRepository.class); Iterator<IRepository> it = serviceLoader.iterator(); while (it != null && it.hasNext()){ IRepository repositoryService = it.next(); System.out.println("class:" + repositoryService.getClass().getName()); repositoryService.connect("172.0.0.1:3306"); } } } |
运行效果图如下:
调用主类无需修改代码,只需通过修改pom引入不同的依赖,就可以选择切换不同的实现。
# SPI 的优缺点
优点:
- Java SPI的使用很简单。也做到了基本的加载扩展点的功能,可以使业务代码和组件代码脱耦,启用替换可插拔
- 拓展性好,在不修改原来代码的基础上,通过添加代码就可以拓展新的能力
- 切换扩展点的实现,只需要在配置文件中修改具体的实现,不需要改代码。使用方便
不足:
- 需要遍历所有的实现,并实例化,然后我们在循环中才能找到我们需要的实现。
- 不提供类似于Spring的IOC和AOP功能,扩展如果依赖其他的扩展,做不到自动注入和装配
针对这些问题, Dubbo在原生 Java SPI 的基础上做了一些拓展。 可见参考文献[3][4]。
*本文涉及的spi-demo源码地址: *
https://github.com/crazyfzw/spi-demo.git
# 参考文献
[3]Dubbo可扩展机制实战