1、定义
SPI是Java提供的一种服务发现机制,用于在运行时动态查找和加载实现特定接口的服务提供商。按照字面的意思是服务提供接口
将接口与具体业务独立开来。实现调用方与实现方解耦。
1.1API与SPI
最简单的区别就是接口的属于哪一方,API接口属于实现方,SPI接口属于调用方,SPI是调用方定义接口,由实现方去实现接口。
API是实现方定义接口,由实现方实现接口。
2、实现
2.1 定义 服务提供者接口
如
public interface LoggerService {
void log(String message);
}
2.2 实现方实现这个接口
public class ConsoleLoggerService implements LoggerService {
@Override
public void log(String message) {
System.out.println("Console Logger: " + message);
}
}
public class FileLoggerService implements LoggerService {
@Override
public void log(String message) {
// 假设将日志写入到文件中...
System.out.println("File Logger: " + message); // 这里为了演示简化为输出到控制台
}
}
2.3 调用
public void testFun20() throws UnsupportedEncodingException {
ServiceLoader<LoggerService> loggerServices = ServiceLoader.load(LoggerService.class);
for (LoggerService loggerService : loggerServices) {
loggerService.log("Hello, SPI!");
}
}
3、注册
实现服务接口的类需要在自身的JAR包的 META-INF/services 目录下创建一个文件,该文件的名字应为接口的全限定名(即包含完整包路径的类名)。文件内容则为服务提供者的全限定名,每一行对应一个实现类。
如
com.mvc.testdemo2.dynamic.ConsoleLoggerService
com.mvc.testdemo2.dynamic.FileLoggerService
原理Java 中的 SPI 机制就是在每次类加载的时候会先去找到 class 相对目录下的 META-INF
文件夹下的 services 文件夹下的文件,将这个文件夹下面的所有文件先加载到内存中,然后根据这些文件的文件名和里面的文件内容找到相应接口的具体实现类,找到实现类后就可以通过反射去生成对应的对象,保存在一个 list 列表里面,所以可以通过迭代或者遍历的方式拿到对应的实例对象,生成不同的实现。
所以会提出一些规范要求:文件名一定要是接口的全类名,然后里面的内容一定要是实现类的全类名,实现类可以有多个,直接换行就好了,多个实现类的时候,会一个一个的迭代加载
4、原理
在客户端代码中,使用 java.util.ServiceLoader 类的 load(Class<T> service) 方法来加载指定接口的所有服务提供者。这个方法会查找当前类加载器可以访问的所有 JAR 包下的 META-INF/services 目录下的对应接口的服务提供者列表文件
4.1实现方法
//生成ServerLoader
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
//new一个ServerLoader对象
public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader){
}
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
5、反射与实例化
5.1方法迭代
首先会在 ServiceLoader
的 Provider
缓存中进行查找,如果缓存中没有命中那么则在 LazyIterator
中进行查找
public Iterator<S> iterator() {
return new Iterator<S>() {
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
public boolean hasNext() {
//先从knowProviders中查询
if (knownProviders.hasNext())
return true;
//没有查询 LazyIterator中查询
return lookupIterator.hasNext();
}
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
5.2lookupIterator迭代
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);
}
}
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
}
6、迭代使用服务
客户端可以通过遍历 ServiceLoader 返回的迭代器来依次使用每一个服务提供者的实例,这样就可以灵活地在运行时选择和切换不同的服务实现
public void testFun20() throws UnsupportedEncodingException {
ServiceLoader<LoggerService> loggerServices = ServiceLoader.load(LoggerService.class);
for (LoggerService loggerService : loggerServices) {
loggerService.log("Hello, SPI!");
}
}