1. 开始感受SPI机制的作用
SPI(Service Provider Interface),翻译成为中文叫做"服务提供接口"。它的作用是什么呢?它的作用其实就是根据一个接口,找到项目中要使用的实现类,用户可以根据实际需要启用或者是扩展原来的默认策略。
下面先以jdk提供的ServiceLoader
这个类库去进行举例吧。
通过ServiceLoader.load
方法中传入指定的接口,它会加载这个接口下的指定实现类,那么这个指定实现类具体是指什么?需要在模块的resources/META-INF/services/
下创建一个文件,文件名为接口的全类名,文件中的内容为需要通过SPI拿到的实现类的全类名(可以指定多个,每行一个)。
我们编写如下这样一个接口
public interface SPIService {
public void serve();
}
再编写两个它的实现类吧
public class SPIServiceImpl1 implements SPIService {
@Override
public void serve() {
System.out.println("SPIServiceImpl1 is serve...");
}
}
//.....................................分隔符..........................................
public class SPIServiceImpl2 implements SPIService {
@Override
public void serve() {
System.out.println("SPIServiceImpl2 is serve...");
}
}
在resources
目录下建立META-INF/services/
目录,并创建一个文件com.wanna.spi.SPIService
指定的是服务接口的全类名,在文件中配置的内容是这个接口的类的实现类com.wanna.spi.SPIServiceImpl1
和com.wanna.spi.SPIServiceImpl2
。
预备工作完成,下面我们要做的,就是使用SPI机制,获取这两个实现类?编写如下的测试代码:
public class SpiTest {
public static void main(String[] args) {
ServiceLoader serviceLoader = ServiceLoader.load(SPIService.class);
serviceLoader.stream().forEach(e -> e.get().serve());
}
}
得到运行结果:
SPIServiceImpl1 is serve...
SPIServiceImpl2 is serve...
也就是说,通过ServiceLoader,成功地加载到了我们编写的两个实现类。
2. 开始了解ServiceLoader的源码?
直接打开ServiceLoader.load
方法的源码
public static ServiceLoader load(Class service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
}
获取当前线程的上下文加载器、调用方类和Service类,从而去创建一个ServiceLoader对象,然后在构造器中就没做什么。
接着我们主要关心它的迭代器的创建:
public Iterator iterator() {
// create lookup iterator if needed
if (lookupIterator1 == null) {
lookupIterator1 = newLookupIterator();
}
return new Iterator() {
// record reload count
final int expectedReloadCount = ServiceLoader.this.reloadCount;
// index into the cached providers list
int index;
/**
* Throws ConcurrentModificationException if the list of cached
* providers has been cleared by reload.
*/
private void checkReloadCount() {
if (ServiceLoader.this.reloadCount != expectedReloadCount)
throw new ConcurrentModificationException();
}
@Override
public boolean hasNext() {
checkReloadCount();
if (index < instantiatedProviders.size())
return true;
return lookupIterator1.hasNext();
}
@Override
public S next() {
checkReloadCount();
S next;
if (index < instantiatedProviders.size()) {
next = instantiatedProviders.get(index);
} else {
next = lookupIterator1.next().get();
instantiatedProviders.add(next);
}
index++;
return next;
}
};
}
最主要的组件在于它创建的lookupIterator1
字段:
private Iterator> newLookupIterator() {
assert layer == null || loader == null;
if (layer != null) {
return new LayerLookupIterator<>();
} else {
Iterator> first = new ModuleServicesLookupIterator<>();
Iterator> second = new LazyClassPathLookupIterator<>();
return new Iterator>() {
@Override
public boolean hasNext() {
return (first.hasNext() || second.hasNext());
}
@Override
public Provider next() {
if (first.hasNext()) {
return first.next();
} else if (second.hasNext()) {
return second.next();
} else {
throw new NoSuchElementException();
}
}
};
}
}
主要创建了两个迭代器,主要关注LazyClassPathLookupIterator
,我们就可以猜测它是负责加载META-INF/services/
下的Provider的加载。
private final class LazyClassPathLookupIterator
implements Iterator>
{
static final String PREFIX = "META-INF/services/";
Set providerNames = new HashSet<>(); // to avoid duplicates
Enumeration configs;
Iterator pending;
Provider nextProvider;
ServiceConfigurationError nextError;
//---------------------------------------------------------------------
private Class> nextProviderClass() {
if (configs == null) {
try {
String fullName = PREFIX + service.getName();
if (loader == null) {
configs = ClassLoader.getSystemResources(fullName);
} else if (loader == ClassLoaders.platformClassLoader()) {
// The platform classloader doesn't have a class path,
// but the boot loader might.
if (BootLoader.hasClassPath()) {
configs = BootLoader.findResources(fullName);
} else {
configs = Collections.emptyEnumeration();
}
} else {
configs = loader.getResources(fullName);
}
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return null;
}
pending = parse(configs.nextElement());
}
String cn = pending.next();
try {
return Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service, "Provider " + cn + " not found");
return null;
}
}
}
我们还可以看到,最终就是使用ClassLoader.getResources
去完成的相关实现类的URL的获取,最后保存到pending队列中,然后使用Class.forName
返回一个加载到的Class。
3. SPI机制在JdbcDriver中的使用?
我们打开mysql的驱动jar包,我们发现也是在META-INF/services/
下存放了一个java.sql.Driver
文件,里面放入了一个Driver的实现类com.mysql.cj.jdbc.Driver
。
在DriverManager.getDriver
方法中,会有如下一步,确保Driver被初始化完成。
public static Driver getDriver(String url) throws SQLException {
ensureDriversInitialized();
//-------------以下代码省略-------------------
}
在这个方法中有如下两行代码,就是使用到的ServiceLoader
去进行加载Driver
的实现类。
ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class);
Iterator driversIterator = loadedDrivers.iterator();
4. SPI机制在Spring中的使用?
在Spring Framework的spring-web
模块中,有如下的文件,主要是用来找到Spring的Servlet容器的初始化器。
我们可以手写一个SpringServletContainerInitializer
,Spring容器在启动的过程中,就会回调它的onStartup方法,从而完成Servlet等配置工作,这个过程也就是传统的SpringMVC的注解版的使用方法。
public class MyMvcStarter implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
//Spring会给我们传入servletContext
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(AppConfig.class);
DispatcherServlet servlet = new DispatcherServlet(context);
final ServletRegistration.Dynamic app = servletContext.addServlet("app", servlet);
app.setLoadOnStartup(1);
app.addMapping("/"); //指定servlet的映射路径
}
}
还有一个典型应用是在SpringBoot中自定义场景启动器时,需要在META-INF/
目录下配置一个spring.factories
文件,去进行相关的配置从而往容器中添加合适的组件,不过这个使用的不是ServiceLoader,而是自定义了一个路径,并且自定义了自己的加载器去完成相关组件的加载。
这个可以参考我的另外一篇帖子:SpringBoot自定义场景启动器(starter)
5. SPI机制在其它地方的使用?
其实很多地方都有使用到SPI机制,比如日志组件、Dubbo、jar包方式进行gradle插件的导入等。
通过jar包导入gradle
插件需要的步骤:
- 1.在jar包的
META-INF/gradle-plugins/
路径下存在一个xxx.properties
的文件,这个xxx.properties
文件中有如下内容:implementation-class=xxx.xxx.xxx
(真正插件的主类名)。 - 2.在buildscript中引入jar包的gav坐标。
- 3.使用
apply plugin 'xxx'
引用该gradle插件。