①SPI的简介
SPI是Service Provider Interfaces的简称。根据Java的SPI规范,我们可以定义一个服务接口,具体的实现由对应的实现者去提供,即Service Provider(服务提供者)。然后在使用的时候只要根据SPI的规范去获取对应的服务提供者的服务实现即可。
②使用场景
一般使用接口的实现类都是静态new一个实现类赋值给接口引用,如下:
HelloService service = new HelloImpl();
如果需要动态的获取一个接口的实现类呢?全局扫描全部的Class,然后判断是否实现了某个接口?代价太大,一般不会这么做。一种合适的方式就是使用配置文件,把实现类名配置在某个地方,然后读取这个配置文件,获取实现类名。
③SPI的作用
这种方式主要是针对不同的服务提供厂商,对不同场景的提供不同的解决方案制定的一套标准,举个简单的例子,如现在的JDK中有支持音乐播放,假设只支持mp3的播放,有些厂商想在这个基础之上支持mp4的播放,有的想支持mp5,而这些厂商都是第三方厂商,如果没有提供SPI这种实现标准,那就只有修改JAVA的源代码了,那这个弊端也是显而易见的,也就是不能够随着JDK的升级而升级现在的应用了,而有了SPI标准,SUN公司只需要提供一个播放接口,在实现播放的功能上通过ServiceLoad的方式加载服务,那么第三方只需要实现这个播放接口,再按SPI标准进行打包成jar,再放到classpath下面就OK了,没有一点代码的侵入性。
④SPI的使用
在实现类的jar包的META-INF下新建一个文件夹services,并在services下新建一个文件,以接口的全限定名为文件名,内容为实现类的全限定名。
实现代码示例:
四个项目:spiInterface、spiA、spiB、spiDemo
spiInterface中定义了一个com.zs.IOperation接口。
spiA、spiB均是这个接口的实现类,服务提供者。
spiDemo作为客户端,引入spiA或者spiB依赖,面向接口编程,通过spi的方式获取具体实现者并执行接口方法。
链接:https://github.com/lemon-simple/SPIDEMO(代码由最后的参考博客提供)
⑤实现SPI机制的核心类:ServiceLoader
ServiceLoader不是一实例化以后立马就去读配置文件中的服务实现者,并且进行对应的实例化工作的,而是会等到需要通过其Iterator实现获取对应的服务提供者时才会加载对应的配置文件进行解析,具体来说是在调用Iterator的hasNext方法时会去加载配置文件进行解析,在调用next方法时会将对应的服务提供者进行实例化并进行缓存。所有的配置文件只加载一次,服务提供者也只实例化一次,如需要重新加载配置文件可调用ServiceLoader的reload方法。
1. 构造函数,如果不指定ClassLoader或者指定的为null,则使用ClassLoader.getSystemClassLoader() ,即AppClassLoader。(设置类加载器:线程上下文类加载器)
2. 遍历有两个方法
hasNext会调用hasNextService,如果指定的ClassLoader为空(一般不会为空,构造函数会初始化),则调用ClassLoader.getSystemClassLoader().getResources,否则调用指定的ClassLoader的getResources方法,获取META-INF/services/接口的全限定名称,如META-INF/services/com.test.loader.HelloService,从这个文件中找出实现类全限定名称。next会调用nextService,根据hasNextService获取的实现类信息,使用指定的ClassLoader进行加载和实例化。
⑥典型实例:JDBC的使用
通常各大厂商(如Mysql、Oracle)会根据一个统一的规范(java.sql.Driver)开发各自的驱动实现逻辑。客户端使用jdbc时不需要去改变代码,直接引入不同的spi接口服务即可。
Mysql的则是com.mysql.jdbc.Drive,Oracle则是oracle.jdbc.driver.OracleDriver。
以jdbc加载driver为例:
数据库连接的伪代码:
1. java.sql.DriverManager静态块初始执行,其中使用spi机制加载jdbc具体实现
2.loadInitialDrivers()中完成了引入的数据库驱动的查找以及载入(看到ServiceLoader类就可以和上述内容联系起来了。)
SPI规范中配置文件的位置指定为:("META-INF/services/";)
接口:(JDK中的接口)
配置文件中的内容为接口实现类的全路径名:(实现类在第三方jar包中)
⑦双亲委派模型的破坏者-线程上下文类加载器
在Java应用中存在着很多服务提供者接口(Service Provider Interface,SPI),这些接口允许第三方为它们提供实现,如常见的 SPI 有 JDBC、JNDI等,这些 SPI 的接口属于 Java 核心库,一般存在rt.jar包中,由Bootstrap类加载器加载,而 SPI 的第三方实现代码则是作为Java应用所依赖的 jar 包被存放在classpath路径下,由于SPI接口中的代码经常需要加载具体的第三方实现类并调用其相关方法,但SPI的核心接口类是由引导类加载器来加载的,而Bootstrap类加载器无法直接加载SPI的实现类,同时由于双亲委派模式的存在,Bootstrap类加载器也无法反向委托AppClassLoader加载器SPI的实现类。在这种情况下,我们就需要一种特殊的类加载器来加载第三方的类库,而线程上下文类加载器就是很好的选择。
线程上下文类加载器(contextClassLoader)是从 JDK 1.2 开始引入的,我们可以通过java.lang.Thread类中的getContextClassLoader()和 setContextClassLoader(ClassLoader cl)方法来获取和设置线程的上下文类加载器。如果没有手动设置上下文类加载器,线程将继承其父线程的上下文类加载器,初始线程的上下文类加载器是系统类加载器(AppClassLoader),在线程中运行的代码可以通过此类加载器来加载类和资源,如下图所示,以jdbc.jar加载为例