什么是SPIService provider interface (SPI) is an API intended to be implemented or extended by a third party. It can be used to enable framework extension and replaceable components.直译过来SPI是一个能被第三方继承或者实现的API,一般用于框架扩张或者可变组件。可能这个理解起来还是比较难,再找一个图来看看
看起来好像比较简单一点了,但是对于我在刚接触SPI的时候还是比较难懂,所以我画了一个我觉得比较适合新手同学的图
可以看到我用3个颜色来标注3个处理接口方,常规情况下,这3方一般是隶属于3个组织,或者1个组织的3个部门,转化成具体的代码就是这3方一般是属于3个工程的。SPI实践有了以上的基础知识,接下来再用一个例子来加深大家的了解,大体的步骤如下:定义一个Interface规范有两个工程实现这个Interface再定义一个使用Interface的工程具体的工程目录如下:
接口定义方整个模块啥也不干,单纯地定义一个接口
接口实现方接口实现方如果想实现上面定义的接口,第一步首先肯定是在pom文件中引入java-api-service 这个模块
接着也是啥也不干,单纯地实现定义的接口即可,下面是接口实现方1的代码
为了大家看得更清楚一点,我特意在一个接口实现方中定义了两个具体实现类XxlServiceLoader
XxlOtherServiceLoader
接口实现方2的代码也是类似的
CuteyServiceLoader
注意:这两个接口实现方是隶属不同module,但是共同依赖了接口定义方注意:这两个接口实现方是隶属不同module,但是共同依赖了接口定义方注意:这两个接口实现方是隶属不同module,但是共同依赖了接口定义方但是实现了接口还不行,要想实现SPI还得有一个公共的约定配置文件,在META-INF目录下新建一个services目录,有些工程创建的时候可能没有META-INF,那就得在resource目录下先新建META-INF。接着在services目录中新增一个文本文件,文件名为接口定义的全限定类名,比如上面的例子实现的接口是InterfaceService ,那文本文件名称就必须是com.cutey.none.spi.service.InterfaceService 。注意,这里的文本文件并不是指文件格式txt,而是单纯文本文件。文本文件的内容为具体实现类的全限定类名,针对上面的例子,不同实现的文本文件内容如下:接口实现方1com.cutey.none.spi.serviceloader.XxlOtherServiceLoadercom.cutey.none.spi.serviceloader.XxlServiceLoader接口实现方2com.cutey.none.spi.serviceloader.CuteyServiceLoader实际文件目录及内容如图
接口使用方有了以上接口定义和接口实现,接下来看看使用方是怎么使用的,首先使用方要使用这个接口,那肯定也是要依赖接口定义方,接着要使用具体的实现,那同样也需要依赖接口实现方。
整个工程是比较简单的,里面只有一个类似Main的方法
App类里面的内容如下,类里面的读取SPI是用java.util.ServiceLoader
看下输出是什么
可以看到在使用方中没有写任何接口实现的代码,仅仅是导入了2个依赖jar,就能使用依赖jar包中的具体实现,感兴趣的同学可以试试如果只依赖上述的单独1个jar会不会是你想的预期。工程中的SPI上面用一个简单的例子帮大家快速体验了下什么是SPI,现在再来简单看看身边有哪些使用SPI的例子。由wikipedia可以看到有这些使用了SPI,接下来我们挑耳熟能详的数据库和spring.factories来看看。
数据库稍微熟悉一点的同学都知道以前是使用Class.forName(“com.mysql.cj.jdbc.Driver”); 去加载驱动类的。对于mysql而言mysql8更改了驱动类的包路径,然后引入了mysql8后发现代码跑不通了还专门去网上搜为什么。所以这种方式肯定是有弊端的,浅举3个需要手动编写驱动类地址,不管是硬编码还是配置文件的形式,都需要我们在使用的时候编写驱动类全限定类名更改驱动类不方便,需要找到定义的地方,然后再去替换当类名发生变化后需要使用方去适配,如果存在信息差那会导致程序运行不了而在有了spi后,已经不用以上方式去加载驱动类了,而是使用DriverManager.getConnection() 去连接数据库,可以看到,连驱动类是啥都已经不关注了。当然,无论哪种形式肯定都要引用数据库的jar包,这是大前提。由于这里不是讲SPI的原理,就不深究DriverManager里面干了啥,这里就贴一张图表明确实用到了spi,感兴趣的同学可以自行研究源码。
为了让各位同学更简单直白的看到这个效果,咱新建一个工程,然后pom文件中引入mysql和oracle的依赖,编写个main方法去加载,看能不能取到两个数据库驱动类的全限定类名。工程目录如下
好不客气地说是啥都没有,然后增加Main类中的方法
接着打断点进行代码调试,看下providerNames是不是真的有我们引入的依赖jar包了。
具体怎么做到的呢,在最开始spi实践我们自己实现的时候就知道是在src/main/resources/META-INF/services 目录下有个以接口作为文件名的文本文件,那现在就去mysql和oracle中的jar包中验证下。
可以看到确实如此,看到这里就能体会到一个最最最明显的好处,那就是哪怕以后mysql的接口实现类发生了变化,我们也不用关注,因为在它本身的jar包中就提供了驱动实现类的全限定类名。spring.factories写在前面,spring.factories是SpringBoot的特性,而非是Spring,然后是用于自动装配(目前我了解到的是只用于这个)。所谓自动装配,说得再简单,那就是在不侵入式修改代码的情况下能够加载其他jar包内的bean。下面演示代码的整体概要:有两个模块,java-spi-springboot-provider 和 java-spi-springboot前者包含Student的bean,后着包含Teacher的beanjava-spi-springboot要使用java-spi-springboot-provider 里的bean两个bean所在的包路径不一样,Student的全限定类名为com.xxl.cutey.none.javaspispringbootprovider.Student ,Teacher的全限定类名为com.cutey.none.xxl.javaspispringboot.Teacher java-spi-springboot依赖java-spi-springboot-provider整体的工程目录图如下
各位小伙伴先别想着spring.factories,就正常情况下,我们是怎么使用别的jar包里面的bean的。也就是在非provider中使用Student这个bean,那首先必是要先加@Component 注解。java-spi-springboot-provider新建一个Student类,然后在主启动类中读取这个bean
主启动类
注意,在主启动类和bean不在同一个包路径下的时候,是需要用scanBasePackage扫包才能把bean注入到IOC容器中。接下来看下输出,没问题。
java-spi-springboot新建一个Teacher和主启动类都和上面一样就不再赘述,现在是想用上面声明的bean,那就只有一个方法,修改scanBasePackages,要么是扫两个Bean的公共包,还记得吗,Student和Teacher我特意把全限定类名改得不懂;要么是具体写完这两个Bean的全限定类名。
看下输出,注意:如果Student的包名没写对,那肯定是获取不到Student的bean的,因为它是在别的jar包下面。
问题来了,如果我想要再引别的组织的包呢,要知道在真实开发中,不同部门,不同组织,甚至不同公司之间相互引用实在是太正常了,难道每次引用一个都得这么加吗,这就引出了spring.factories。我们改造的是java-spi-springboot-provider
接下来我们再修改下java-spi-springboot中的包,把扫描的student的包去掉
我们可以神奇的发现,还是能读取出来bean
让我们浅浅看Spring用是不是真的用到了SPI
诶,好像没看到跟数据库一样用到了jdk中的ServiceLoader呀,是不是骗你们了捏,其实不是,其实SpringFactoriesLoader就是类是jdk中的ServiceLoader。说白了,spring使用的SPI的思想,而SPI准确来说也不是一个接口,而是面向接口编程的其中一种思想,是接口使用方和接口定义方约定一种面向编程的思想。参考文献Service provider interface – Wikipedia10分钟让你彻底明白Java SPI,附实例代码演示#安员外很有码哔哩哔哩bilibiliJava SPI (Service Provider Interface) 机制详解 – 腾讯云开发者社区-腾讯云 (tencent.com)
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net