1. SPI是什么?
最近在看 Spring 和 Dubbo 的源码,发现这两个框架都有自己的 SPI 机制,决定了解下 SPI 。
SPI (Service Provider Interface)其实是一种思想,但凡涉及到思想的东西,都是看一眼感觉自己会,看代码也看得懂,然后自己就是不会用,或者用的不是那么顺手(参考设计模式)。
2. 写个HelloWorld
首先定义一个接口,两个实现类。
代码如下:
1 | package com.xiaofa.common.spi; |
1 | package com.xiaofa.common.spi; |
1 | package com.xiaofa.common.spi; |
在 resources 目录下新建 META-INF/services ,里面新建一个文件,com.xiaofa.common.spi.SpiService (接口的全路径)。截图如下:
文件的内容如下:
com.xiaofa.common.spi.SpiServiceOne
内容是实现类的全路径。
写个单元测试:
1 | package com.xiaofa.common; |
执行结果如下,调用了 SpiServiceOne 的方法。
I am service one
如果在 com.xiaofa.common.spi.SpiService 文件中写入的是两个实现类,那么打印结果就是两行了。目前我们已经知道 SPI 的简单用法了,那原理是什么呢?
原理:ServiceLoader.load(Search.class) 方法在加载某接口时,会去 META-INF/services 下找接口的全限定名文件,再根据里面的内容加载相应的实现类。
为什么是在 META-INF/services 下呢?我们看下 ServiceLoader 类的源码。
1 | private static final String PREFIX = "META-INF/services/"; |
可以发现,查找实现类的路径前缀就是 META-INF/services/ 。完整路径是前缀 + 接口名。
1 | String fullName = PREFIX + service.getName(); |
3. 源码中的使用
还记得我们最开始学习 JDBC 时写的代码吗?大致如下吧。
1 | public class JdbcTest { |
第一步就是加载驱动,但是在某个 JDK 版本之后(具体哪个版本不清楚),就不再需要手动加载驱动了。有兴趣可以自行测试下。为什么呢?
结合之前讲的 SPI 机制很容易猜想到,JDBC 是不是也采用了这种机制。我们来看看 DriverManager 的源码
1 | /** |
先看方法的注释,翻译一下:
通过校验系统配置文件 jdbc.properties 加载初始化 JDBC 驱动,然后使用 ServiceLoader 机制。而 ServiceLoader 就是 SPI 机制的实现类。
我们再看看 loadInitialDrivers 的核心代码:
1 | // If the driver is packaged as a Service Provider, load it. 如果驱动以服务提供的方式打包好了,就加载它; |
可以看出,这个方法主要是通过 ServiceLoader 加载驱动,之后遍历所有驱动,挨个注入。看一看 mysql-connector-java:8.0.15 包中 META-INF/services 下确实有文件名 java.sql.Driver 的文件,内容是 com.mysql.cj.jdbc.Driver。
下面看看真正注入的方法,调用 ServiceLoader 的 next 方法。
1 | public Iterator<S> iterator() { |
1 | // 2 |
1 | // 3 |
详细的执行顺序如上述代码中注释的 1、2、3 所示。最终我们发现,还是执行了 Class.forName 方法。通过 SPI 的方式把手动加载变成框架自动处理。
4. SPI和API
待续。。。
彩蛋:后续应该还会写 Spring SPI、Dubbo SPI 相关的文章。
参考资料: