Java深入講解SPI的使用
什么是Java SPI
SPI的全名為:Service Provider Interface。在java.util.ServiceLoader的文檔里有比較詳細(xì)的介紹。簡(jiǎn)單的總結(jié)下 Java SPI 機(jī)制的思想。我們系統(tǒng)里抽象的各個(gè)模塊,往往有很多不同的實(shí)現(xiàn)方案,比如日志模塊的方案,xml解析模塊、jdbc模塊的方案等。面向的對(duì)象的設(shè)計(jì)里,我們一般推薦模塊之間基于接口編程,模塊之間不對(duì)實(shí)現(xiàn)類進(jìn)行硬編碼。一旦代碼里涉及具體的實(shí)現(xiàn)類,就違反了可拔插的原則,如果需要替換一種實(shí)現(xiàn),就需要修改代碼。為了實(shí)現(xiàn)在模塊裝配的時(shí)候能不在程序里動(dòng)態(tài)指明,這就需要一種服務(wù)發(fā)現(xiàn)機(jī)制。
Java SPI 就是提供這樣的一個(gè)機(jī)制:為某個(gè)接口尋找服務(wù)實(shí)現(xiàn)的機(jī)制。有點(diǎn)類似IOC的思想,就是將裝配的控制權(quán)移到程序之外,在模塊化設(shè)計(jì)中這個(gè)機(jī)制尤其重要Java SPI 的具體約定為:當(dāng)服務(wù)的提供者,提供了服務(wù)接口的一種實(shí)現(xiàn)之后,在jar包的META-INF/services/目錄里同時(shí)創(chuàng)建一個(gè)以服務(wù)接口命名的文件。該文件里就是實(shí)現(xiàn)該服務(wù)接口的具體實(shí)現(xiàn)類。而當(dāng)外部程序裝配這個(gè)模塊的時(shí)候,就能通過該jar包META-INF/services/里的配置文件找到具體的實(shí)現(xiàn)類名,并裝載實(shí)例化,完成模塊的注入?;谶@樣一個(gè)約定就能很好的找到服務(wù)接口的實(shí)現(xiàn)類,而不需要再代碼里制定。jdk提供服務(wù)實(shí)現(xiàn)查找的一個(gè)工具類:java.util.ServiceLoader。
Java SPI使用demo
定義一個(gè)接口:
package com.hiwei.spi.demo;
public interface Animal {
void speak();
}
創(chuàng)建兩個(gè)實(shí)現(xiàn)類:
package com.hiwei.spi.demo;
public class Cat implements Animal {
@Override
public void speak() {
System.out.println("喵喵喵!");
}
}
package com.hiwei.spi.demo;
public class Dog implements Animal {
@Override
public void speak() {
System.out.println("汪汪汪!");
}
}
在resources目錄下創(chuàng)建META-INF/services目錄:

創(chuàng)建以接口類路徑命名的文件,文件中添加實(shí)現(xiàn)類路徑:
com.hiwei.spi.demo.Cat
com.hiwei.spi.demo.Dog
使用
package com.hiwei.spi;
import com.hiwei.spi.demo.Animal;
import java.sql.SQLException;
import java.util.ServiceLoader;
public class SpiDemoApplication {
public static void main(String[] args){
//會(huì)根據(jù)文件找到對(duì)應(yīng)的實(shí)現(xiàn)類
ServiceLoader<Animal> load = ServiceLoader.load(Animal.class);
//執(zhí)行實(shí)現(xiàn)類方法
for (Animal animal : load) {
animal.speak();
}
}
}
執(zhí)行結(jié)果:

上面我們可以看到j(luò)ava spi會(huì)幫助我們找到接口實(shí)現(xiàn)類。那么實(shí)際生產(chǎn)中怎么使用呢? 將上面的代碼打成jar,然后在其它項(xiàng)目中引入,同樣的目錄下創(chuàng)建文件,并寫上自己實(shí)現(xiàn)類的路徑:

本項(xiàng)目實(shí)現(xiàn)類:
package com.example.demo;
import com.hiwei.spi.demo.Animal;
public class Pig implements Animal {
@Override
public void speak() {
System.out.println("哼哼哼!");
}
}
代碼中,我們調(diào)用jar中的main方法:
package com.example.demo;
import com.hiwei.spi.SpiDemoApplication;
public class DemoApplication {
public static void main(String[] args) {
SpiDemoApplication.main(args);
}
}
執(zhí)行結(jié)果:

可以看見自定義的實(shí)現(xiàn)類也被執(zhí)行了。在實(shí)際生產(chǎn)中,我們就可以使用java spi面向接口編程,實(shí)現(xiàn)可插拔。
SPI在JDBC中的應(yīng)用
以最新的mysql-connector-java-8.0.27.jar為例
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
</dependency>
在使用JDBC連接數(shù)據(jù)庫(kù)時(shí),只需要使用:
DriverManager.getConnection("url", "username", "password");
DriverManager有靜態(tài)方法:
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
看下loadInitialDrivers()方法,其中有:
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
//獲取Driver.class的實(shí)現(xiàn)類
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
/* Load these drivers, so that they can be instantiated.
* It may be the case that the driver class may not be there
* i.e. there may be a packaged driver with the service class
* as implementation of java.sql.Driver but the actual class
* may be missing. In that case a java.util.ServiceConfigurationError
* will be thrown at runtime by the VM trying to locate
* and load the service.
*
* Adding a try catch block to catch those runtime errors
* if driver not available in classpath but it's
* packaged as service and that service is there in classpath.
*/
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
可以看見,會(huì)根據(jù)java spi獲取Driver.class的實(shí)現(xiàn)類,可以在mysql-connector-java-8.0.27.jar下面看到,定義的文件:

程序會(huì)根據(jù)文件找到對(duì)應(yīng)的實(shí)現(xiàn)類,并連接數(shù)據(jù)庫(kù)。
SPI在sharding-jdbc中的應(yīng)用
sharding-jdbc是一款用于分庫(kù)分表的中間件,在數(shù)據(jù)庫(kù)分布式場(chǎng)景中,對(duì)于主鍵生成要保證唯一性,主鍵生成策略有很多種實(shí)現(xiàn)。sharding-jsbc在主鍵生成上就使用了SPI進(jìn)行擴(kuò)展。
下面看下sharding-jdbc源碼在主鍵生成上是怎么應(yīng)用的: 源碼中的 ShardingRule.class主要封裝分庫(kù)分表的策略規(guī)則,包括主鍵生成??聪耤reateDefaultKeyGenerator方法:
//生成默認(rèn)主鍵生成策略
private ShardingKeyGenerator createDefaultKeyGenerator(final KeyGeneratorConfiguration keyGeneratorConfiguration) {
//SPI服務(wù)發(fā)現(xiàn)
ShardingKeyGeneratorServiceLoader serviceLoader = new ShardingKeyGeneratorServiceLoader();
return containsKeyGeneratorConfiguration(keyGeneratorConfiguration)
? serviceLoader.newService(keyGeneratorConfiguration.getType(), keyGeneratorConfiguration.getProperties()) : serviceLoader.newService();
}
繼續(xù)看ShardingKeyGeneratorServiceLoader(),有靜態(tài)代碼塊注冊(cè):
static {
//SPI: 加載主鍵生成策略
NewInstanceServiceLoader.register(ShardingKeyGenerator.class);
}
看下register方法:
public static <T> void register(final Class<T> service) {
//服務(wù)發(fā)現(xiàn)
for (T each : ServiceLoader.load(service)) {
registerServiceClass(service, each);
}
}
看到這,真相大白,就是應(yīng)用java spi機(jī)制。
我們?cè)倏聪聄esources目錄下:

可以看到有對(duì)應(yīng)接口命名的文件,文件內(nèi)容:

有兩個(gè)實(shí)現(xiàn),分別是雪花算法和UUID,這也對(duì)應(yīng)了sharding-jdbc的提供的兩種生成策略。我們?cè)谑褂胹harding-jdbc時(shí),也可以自定義策略,便于擴(kuò)展。 sharding-jdbc對(duì)于SPI的使用點(diǎn)還有很多,這里就不一一列舉了。對(duì)于SPI機(jī)制,我們?cè)诠ぷ髦幸部梢詫?shí)際應(yīng)用,提升程序的可擴(kuò)展性。
擴(kuò)展
以上是Java SPI的解析。其實(shí)SPI機(jī)制在很多地方都有用到,只是以不同的形式應(yīng)用,具體的實(shí)現(xiàn)略有不同。例如dubbo中也有類似的spi機(jī)制;springboot的自動(dòng)裝配,也使用了spi機(jī)制:
springboot自動(dòng)裝配:
定義文件:

文件中聲明需要發(fā)現(xiàn)的類:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.hiwei.valve.ValveAutoConfiguration
springboot的掃描文件,裝配對(duì)應(yīng)的類:
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
Map<String, List<String>> result = cache.get(classLoader);
if (result != null) {
return result;
}
result = new HashMap<>();
try {
//加載文件中的類
Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
String[] factoryImplementationNames =
StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
for (String factoryImplementationName : factoryImplementationNames) {
result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
.add(factoryImplementationName.trim());
}
}
}
// Replace all lists with unmodifiable lists containing unique elements
result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
.collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));
cache.put(classLoader, result);
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
return result;
}
FACTORIES_RESOURCE_LOCATION的值:

SPI在Java開發(fā)中是個(gè)很重要的設(shè)計(jì),所以我們一定要熟練掌握。
到此這篇關(guān)于Java深入講解SPI的使用的文章就介紹到這了,更多相關(guān)Java SPI內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java連接Oracle數(shù)據(jù)庫(kù)實(shí)例解析
數(shù)據(jù)庫(kù)的操作是當(dāng)前系統(tǒng)開發(fā)必不可少的開發(fā)部分之一。接下來通過本文給大家介紹Java連接Oracle數(shù)據(jù)庫(kù)實(shí)例解析,非常不錯(cuò)具有參考借鑒價(jià)值,感興趣的朋友一起學(xué)習(xí)吧2016-06-06
Spring Data Redis對(duì)象緩存序列化問題解決
相信在項(xiàng)目中,你一定是經(jīng)常使用Redis,在使用時(shí),有沒有遇到同我一樣,對(duì)象緩存序列化問題的呢,本文主要介紹了Spring Data Redis對(duì)象緩存序列化問題解決,感興趣的可以了解一下2024-01-01
Java獲取HttpServletRequest的三種方法詳解
這篇文章主要介紹了Java獲取HttpServletRequest的三種方法詳解,是一個(gè)接口,全限定名稱為Jakarta.Serclet.http.HttpServletRequest2023-11-11
HttpServletRequest接口是Servlet規(guī)范的一員,需要的朋友可以參考下
Java使用注解實(shí)現(xiàn)防止重復(fù)提交實(shí)例
這篇文章主要介紹了Java使用注解實(shí)現(xiàn)防止重復(fù)提交實(shí)例,在一些項(xiàng)目中由于用戶誤操作,多次點(diǎn)擊表單提交按鈕,會(huì)產(chǎn)生很多次的數(shù)據(jù)交互,為了解決這一問題,本文使用注解來實(shí)現(xiàn)防止重復(fù)提交,需要的朋友可以參考下2023-07-07
visual studio 2019安裝配置可編寫c/c++語言的IDE環(huán)境
這篇文章主要介紹了visual studio 2019安裝配置可編寫c/c++語言的IDE環(huán)境,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03
Spring Boot啟動(dòng)及退出加載項(xiàng)的方法
這篇文章主要介紹了Spring Boot啟動(dòng)及退出加載項(xiàng)的方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04
Mybatis的TypeHandler加解密數(shù)據(jù)實(shí)現(xiàn)
在我們數(shù)據(jù)庫(kù)中有些時(shí)候會(huì)保存一些用戶的敏感信息,所以就需要對(duì)這些數(shù)據(jù)進(jìn)行加密,那么本文就介紹了Mybatis的TypeHandler加解密數(shù)據(jù)實(shí)現(xiàn),感興趣的可以了解一下2021-06-06

