SPI ,全称为 Service Provider Interface,是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。
我先举例如何使用java的spi。
1. 首先定义一个服务接口,比如LogService.java
package code.classloader;
/**
*
* @author dgm
* @describe "日志服务接口"
* @date 2020年5月22日
*/
public interface LogService {
void print(String message);
}
2. 再定义三个LogService接口的实现类
package code.classloader;
/**
* @author dgm
* @describe "日志到控制台"
* @date 2020年5月22日
*/
public class StdOutLogServiceImpl implements LogService {
@Override
public void print(String message) {
// TODO Auto-generated method stub
System.out.println(message);
System.out.println("写日志到控制台!");
}
}
package code.classloader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
/**
*
* @author dgm
* @describe "日志到文件"
* @date 2020年5月22日
*/
public class FileLogServiceImpl implements LogService {
private static final String FILE_NAME="d://LogService.txt";
@Override
public void print(String message) {
try {
File file = new File(FILE_NAME);
FileWriter fw = null;
// true:表示是追加的标志
fw = new FileWriter(file, true);
fw.write(message+"\n");
fw.close();
System.out.println(message);
System.out.println("写日志入文件!");
} catch (IOException e) {
}
}
}
package code.classloader;
/**
* @author dgm
* @describe "写日志入mysql数据库"
* @date 2020年5月22日
*/
public class MysqlLogServiceImpl implements LogService {
@Override
public void print(String message) {
// TODO Auto-generated method stub
System.out.println(message);
System.out.println("写日志入数据库");
}
}
注意:我把三个实现类(StdOutLogServiceImpl.java,FileLogServiceImpl,MysqlLogServiceImpl)一个代码框里了
3. 在项目src
目录下新建一个META-INF/services
文件夹,然后再新建一个以LogService接口的全限定名命名的文件code.classloader.LogService
,其文件内容为:
code.classloader.StdOutLogServiceImpl
code.classloader.FileLogServiceImpl
code.classloader.MysqlLogServiceImpl
4. 最后我们再新建一个测试类LogClientTest
:
package code.test;
import java.util.Iterator;
import java.util.ServiceLoader;
import code.classloader.LogService;
/**
* @author dgm
* @describe ""
* @date 2020年5月22日
*/
public class LogClientTest {
public static void main(String[] args) {
ServiceLoader<LogService> loader = ServiceLoader.load(LogService.class);
Iterator<LogService> it = loader.iterator();
while (it != null && it.hasNext()){
LogService logService = it.next();
logService.print("日志实现是:= " + logService.getClass());
}
}
}
运行测试类,结果如下图所示:
5. Java的SPI机制的源码分析
从测试类LogClientTest我们看到Java的SPI机制实现跟ServiceLoader
这个类有关,那么我们先来看下ServiceLoader
的类结构代码:
//注意ServiceLoader类实现了Iterable接口
publicfinalclass ServiceLoader<S> implements Iterable<S>{
//这下知道为啥要把案例中约束目录固定死了吧
private static final String PREFIX = "META-INF/services/";
// The class or interface representing the service being loaded
private final Class<S> service;
// The class loader used to locate, load, and instantiate providers
private final ClassLoader loader;
// The access control context taken when the ServiceLoader is created
private final AccessControlContext acc;
// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// The current lazy-lookup iterator
private LazyIterator lookupIterator;
// 构造方法
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();
}
// ...暂时省略相关代码
// ServiceLoader的内部类LazyIterator,实现了【Iterator】接口
// Private inner class implementing fully-lazy provider lookup
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;
}
// 覆写Iterator接口的hasNext方法
public boolean hasNext() {
// ...暂时省略相关代码
}
// 覆写Iterator接口的next方法
public S next() {
// ...暂时省略相关代码
}
// 覆写Iterator接口的remove方法
public void remove() {
// ...暂时省略相关代码
}
}
// 覆写Iterable接口的iterator方法,返回一个迭代器
public Iterator<S> iterator() {
// ...暂时省略相关代码
}
// ...暂时省略相关代码
}
这下知道为啥要把案例中约束目录名META-INF/services/固定死了吧。
可以看到,ServiceLoader
实现了Iterable
接口,覆写其iterator
方法能产生一个迭代器;同时ServiceLoader
有一个内部类LazyIterator
,而LazyIterator
又实现了Iterator
接口,说明LazyIterator
是一个迭代器。
5.1 ServiceLoader.load方法,为加载服务提供者实现类做前期准备
我们开始探究Java的SPI机制的源码, 先来看LogClientTest
的第一句代码
ServiceLoader<LogService> loader = ServiceLoader.load(LogService.class);
ServiceLoader.load(LogService.class)
的源码如下:
// ServiceLoader.java
public static <S> ServiceLoader<S> load(Class<S> service) {
//获取当前线程上下文类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
// 将service接口类和线程上下文类加载器作为参数传入,继续调用load方法
return ServiceLoader.load(service, cl);
}
我们继续往下看ServiceLoader.load(service, cl)
方法:
// ServiceLoader.java
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
// 将service接口类和线程上下文类加载器作为构造参数,新建了一个ServiceLoader对象
return new ServiceLoader<>(service, loader);
}
继续接着看new ServiceLoader<>(service, loader)
是如何构建的?
// ServiceLoader.java
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();
}
可以看到在构建ServiceLoader
对象时除了给其成员属性赋值外,还调用了reload
方法:
// ServiceLoader.java
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
可以看到在reload
方法中又新建了一个LazyIterator
对象,然后赋值给lookupIterator
。
// ServiceLoader$LazyIterator.java
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
可以看到在构建LazyIterator
对象时,也只是给其成员变量service
和loader
属性赋值。
5.2 ServiceLoader.iterator方法,实现服务提供者实现类的懒加载
我们现在再来看LogClientTest
的第二句代码
Iterator<LogService> it = loader.iterator();
,执行这句代码后最终会调用serviceLoader
的iterator
方法:
// serviceLoader.java
public Iterator<S> iterator() {
return new Iterator<S>() {
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
public boolean hasNext() {
if (knownProviders.hasNext())
returntrue;
// 调用lookupIterator即LazyIterator的hasNext方法
// 可以看到是委托给LazyIterator的hasNext方法来实现
return lookupIterator.hasNext();
}
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
// 调用lookupIterator即LazyIterator的next方法
// 可以看到是委托给LazyIterator的next方法来实现
return lookupIterator.next();
}
public void remove() {
thrownew UnsupportedOperationException();
}
};
}
可以看到调用serviceLoader
的iterator
方法会返回一个匿名的迭代器对象,而这个匿名迭代器对象其实相当于一个门面类,其覆写的hasNext
和next
方法又分别委托LazyIterator
的hasNext
和next
方法来实现了。
我们继续追踪代码,发现接下来会进入LazyIterator
的hasNext
方法:
// serviceLoader$LazyIterator.java
public boolean hasNext() {
if (acc == null) {
// 调用hasNextService方法
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
然后继续跟进hasNextService
方法:
// serviceLoader$LazyIterator.java
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
// 终于出现约定目录名了,即PREFIX = "META-INF/services/"
// service.getName()即接口的全限定名
// 还记得前面的代码构建LazyIterator对象时已经给其成员属性service赋值吗
String fullName = PREFIX + service.getName();
// 加载META-INF/services/目录下的接口文件中的服务提供者类
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
// 还记得前面的代码构建LazyIterator对象时已经给其成员属性loader赋值吗
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
// 返回META-INF/services/目录下的接口文件中的服务提供者类并赋值给pending属性
pending = parse(service, configs.nextElement());
}
// 然后取出一个全限定名赋值给LazyIterator的成员变量nextName
nextName = pending.next();
return true;
}
可以看到在执行LazyIterator
的hasNextService
方法时最终将去META-INF/services/
目录下加载接口文件的内容即加载服务提供者实现类的全限定名,然后取出一个服务提供者实现类的全限定名赋值给LazyIterator
的成员变量nextName
。到了这里,我们就明白了LazyIterator
的作用真的是懒加载,在用到的时候才会真正去加载服务提供者实现类。
同样,执行完LazyIterator
的hasNext
方法后,会继续执行LazyIterator
的next
方法:
// serviceLoader$LazyIterator.java
public S next() {
if (acc == null) {
// 调用nextService方法
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
我们继续跟进nextService
方法:
// serviceLoader$LazyIterator.java
private S nextService() {
if (!hasNextService())
thrownew NoSuchElementException();
// 还记得在hasNextService方法中为nextName赋值过服务提供者实现类的全限定名吗
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
// 【1】去classpath中根据传入的类加载器和服务提供者实现类的全限定名去加载服务提供者实现类
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 {
// 【2】实例化刚才加载的服务提供者实现类,并进行转换
S p = service.cast(c.newInstance());
// 【3】最终将实例化后的服务提供者实现类放进providers集合
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
thrownew Error(); // This cannot happen
}
可以看到LazyIterator
的nextService
方法最终将实例化之前加载的服务提供者实现类,并放进providers
集合中,随后再调用服务提供者实现类的方法。注意,这里是加载一个服务提供者实现类后,若main
函数中有调用该服务提供者实现类的方法的话,紧接着会调用其方法;然后继续实例化下一个服务提供者类。
因此,我们看到了ServiceLoader.iterator
方法真正承担了加载并实例化META-INF/services/
目录下的接口文件里定义的服务提供者实现类。
想了解SpringBoot的SPI机制的样板,META-INF/spring.factories,算是一种约定,也可以参考下
SpringBoot扩展点之EnvironmentPostProcessor https://blog.csdn.net/dong19891210/article/details/106436364
总结: 如果你看懂了java的spi,那么spring boot、dubbo的spi也能搞懂了,变体(当看到一样事物的内在逻辑,就要学会润色、加工、处理、改造、完善,灵活变通、举一反三)!!!
附代码目录结构:
参考:
0. java.util Class ServiceLoader https://docs.oracle.com/javase/6/docs/api/java/util/ServiceLoader.html
Java是如何实现自己的SPI机制的? JDK源码(一) https://mp.weixin.qq.com/s/6BhHBtoBlSqHlXduhzg7Pw
Spring-SpringFactoriesLoader详解 https://msd.misuland.com/pd/2884250137616453978
探讨注解驱动Spring应用的机制,详解ServiceLoader、SpringFactoriesLoader的使用(以JDBC、spring.factories为例介绍SPI) https://cloud.tencent.com/developer/article/1497777
Dubbo源码解析之SPI(一):扩展类的加载过程 https://blog.51cto.com/14159827/2475733?source=drh
Java Code Examples for org.springframework.core.io.support.SpringFactoriesLoader https://www.programcreek.com/java-api-examples/index.php?api=org.springframework.core.io.support.SpringFactoriesLoader
Java Service Loader vs Spring Factories Loader https://blog.frankel.ch/java-service-loader-vs-spring-factories/
JDK的SPI原理及源码分析 https://mp.weixin.qq.com/s?__biz=MzI1MjQ2NjEyNA==&mid=2247483671&idx=1&sn=6d6ea78a1d7fd7ef0fb2a3f948bdca99