1.SPI是什么?
SPI
,即是Service Provider Interface
,是一种服务提供(接口实现)发现机制,可以通过ClassPath路径下的META-INF/Service
文件查找文件,加载里面定义的类。 一般可以用来启用框架拓展和替换组件,比如在最常见的数据库连接JDBC中,java.sql.Driver
,不同的数据库产商可以对接口做不一样的实现,但是JDK怎么知道别人有哪些实现呢?这就需要SPI
,可以查找到接口的实现,对其进行操作。 用两个字解释:解耦。
2.如何使用SPI来提供自定义服务?
我们来写一个简单的例子: 整个项目结构:
SPI-Project
:maven
项目DBInterface
:maven
项目,parent是SPI-Project
,定义了一个接口com.aphysia.sqlserver.DBConnectionService
,自己不做实现。MysqlConnection
:prarent是SPI-Project
,实现了接口DBConnectionService
,也就是MysqlConnectionServiceImpl
SqlServerConnection
:prarent 也是SPI-Project
,实现了DBConnectionService
,也就是SqlServerConnectionServiceImpl
WebProject
:测试项目,模拟web项目里面使用数据库驱动。
不管是MySqlConnection
还是SqlServerConnection
两个module中,都是去实现了DBInterface
的接口,并且在resource/META-INF/services
下都需要声明所实现的类,文件名就是实现的接口全限定名com.aphysia.sql.DBConnectionService
,文件里面就是具体的实现类的全限定名,比如:com.aphysia.mysql.MysqlConnectionServiceImpl
SPI-Project的pom文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http:///POM/4.0.0"
xmlns:xsi="http:///2001/XMLSchema-instance"
xsi:schemaLocation="http:///POM/4.0.0 http:///xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.aphysia</groupId>
<artifactId>SPI-Project</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>DbInterface</module>
<module>MySqlConection</module>
<module>SqlServerConnection</module>
<module>WebProject</module>
</modules>
</project>
2.1 DBInterface定义接口
DBInterface是SPIProject的一个module,主要是定义一个规范(接口),不做任何实现。 pom文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http:///POM/4.0.0"
xmlns:xsi="http:///2001/XMLSchema-instance"
xsi:schemaLocation="http:///POM/4.0.0 http:///xsd/maven-4.0.0.xsd">
<parent>
<artifactId>SPI-Project</artifactId>
<groupId>com.aphysia</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>DbInterface</artifactId>
</project>
定义的接口(模拟了java提供的数据库驱动的情景,定义了驱动规范):DBConnectionService.java
package com.aphysia.sql;
public interface DBConnectionService {
void connect();
}
2.2 模拟Mysql实现驱动
接口的第一种实现,相当于模拟第三方Mysql
对接口做了自己的拓展: pom文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http:///POM/4.0.0"
xmlns:xsi="http:///2001/XMLSchema-instance"
xsi:schemaLocation="http:///POM/4.0.0 http:///xsd/maven-4.0.0.xsd">
<parent>
<artifactId>SPI-Project</artifactId>
<groupId>com.aphysia</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>MySqlConection</artifactId>
<dependencies>
<dependency>
<groupId>com.aphysia</groupId>
<artifactId>DbInterface</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
实现了前面定义的接口: MysqlConnectionServiceImpl
package com.aphysia.mysql;
import com.aphysia.sqlserver.DBConnectionService;
public class MysqlConnectionServiceImpl implements DBConnectionService {
public void connect() {
System.out.println("mysql 正在连接...");
}
}
声明实现,在resource/META-INF.services/
下定义一个文件,名为com.aphysia.sql.DBConnection
,里面内容是:
com.aphysia.mysql.MysqlConnectionServiceImpl
2.3 模拟SqlServer实现驱动
SqlServerConnection
也是一个module,pom文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http:///POM/4.0.0"
xmlns:xsi="http:///2001/XMLSchema-instance"
xsi:schemaLocation="http:///POM/4.0.0 http:///xsd/maven-4.0.0.xsd">
<parent>
<artifactId>SPI-Project</artifactId>
<groupId>com.aphysia</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>SqlServerConnection</artifactId>
<dependencies>
<dependency>
<groupId>com.aphysia</groupId>
<artifactId>DbInterface</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
接口的第二种实现,相当于第三方SqlServer
对接口做了自己的拓展:SqlServerConnectionServiceImpl
package com.aphysia.sqlserver;
public class SqlServerConnectionServiceImpl implements DBConnectionService {
public void connect() {
System.out.println("sqlServer 正在连接...");
}
}
声明实现,在resource/META-INF.services/
下定义一个文件,名为com.aphysia.sql.DBConnection
,里面内容是:
com.aphysia.sqlserver.SqlServerConnectionServiceImpl
2.4 模拟用户使用不同驱动
上面两种不同的接口实现,注意需要在resource下声明,文件名是基类的全限定名,里面内容是具体实现类的全限定名
而我们自己使用项目的时候呢?肯定是需要哪一个驱动就引入哪一个驱动的jar包。
比如我们在webProject中导入两种实现:MysqlConnection
和SqlServerConnection
:
<dependencies>
<dependency>
<groupId>com.aphysia</groupId>
<artifactId>DbInterface</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.aphysia</groupId>
<artifactId>MySqlConection</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.aphysia</groupId>
<artifactId>SqlServerConnection</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
测试代码如下:
import com.aphysia.sql.DBConnectionService;
import java.util.ServiceLoader;
public class Test {
public static void main(String[] args) {
ServiceLoader<DBConnectionService> serviceLoader= ServiceLoader.load(DBConnectionService.class);
for (DBConnectionService dbConnectionService : serviceLoader) {
dbConnectionService.connect();
}
}
}
输出:
mysql 正在连接...
sqlServer 正在连接...
如果我们只在pom文件里面引入mysql的实现呢?答案很明显,只会输出下面一句:
mysql 正在连接...
也就是对于使用的人来说,不需要自己再做什么操作,只需要把包引入进来即可,简单易用。
具体完整代码: https:///Damaer/DemoCode/tree/main/SPI-Project,仅供参考
3. ServiceLoader实现原理
ServiceLoader
位于java.util
包下,其主要代码如下:
public final class ServiceLoader<S>
implements Iterable<S>
{
private static final String PREFIX = "META-INF/services/";
private final Class<S> service;
private final ClassLoader loader;
private final AccessControlContext acc;
// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// The current lazy-lookup iterator
private LazyIterator lookupIterator;
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
private class LazyIterator
implements Iterator<S>
{
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
public void remove() {
throw new UnsupportedOperationException();
}
}
public Iterator<S> iterator() {
return new Iterator<S>() {
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
public boolean hasNext() {
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext();
}
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
ClassLoader cl = ClassLoader.getSystemClassLoader();
ClassLoader prev = null;
while (cl != null) {
prev = cl;
cl = cl.getParent();
}
return ServiceLoader.load(service, prev);
}
public String toString() {
return "java.util.ServiceLoader[" + service.getName() + "]";
}
}
我们调用ServiceLoader.load()
获取接口的实现,实际上也是调用了ServiceLoader(Class<S> svc, ClassLoader cl)
,里面都是调用reload()
,reload()
里面做了些什么操作呢?
先把provider
清空,然后创建了LazyIterator
对象,LazyIterator
是一个内部类,实现了Iterator
接口,实际上就是一个懒加载的迭代器。什么时候加载呢? 在迭代器调用的时候,调用hasNextService()
,去解析resource/META-INF/services
下面的实现,并完成实现类的实例化。这里的实例化是使用反射,也是通过全限定类名。class.forName()
。
解析的时候,每一行代表一个实现类,将已经发现的接口进行缓存,放到private LinkedHashMap<String,S> providers
中,同时对外提供遍历迭代的方法。
4. SPI的应用
我们在使用mysql驱动的时候,在mysql-connector-java-version.jar
中,有一个文件是Resource/service/java.sql.Driver
文件,里面记录的是:
com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
也就是声明了java.sql.Driver
的实现类是com.mysql.jdbc.Driver
,不需要手动使用Class.forName()
手动加载。
同样的,slf4j
也是一样的机制去实现拓展功能。
这种思想,通过服务约定-->服务实现-->服务自动注册-->服务发现和使用,完成了提供者和使用方的解耦,真的很强...