ServiceLoader源码分析和SPI总结
👿:最近两天通过阅读了一些源码和java doc对JVM有了粗浅的认识,特此做个知识记录!
😙介绍这个问题之前一定要对SPI知识有所了解!
SPI介绍
0. 问题引入
1、拿我们的JDBC来引入问题,我们在自己的应用程序中如果要使用到数据库,我们只需要将该数据库的驱动jar包引入到应用的classpath下,然后在项目中使用Class.forName(“xxx”);去加载驱动,
2、使用DriverManager.getConnection("url","username","pwd");
去获取数据库连接,然后使用即可。
3、但是每个数据库厂商都对自己的数据库由不同的驱动,比如MySQL有MySQL的数据库驱动,Oracle有Oracle的驱动,我们只需要将我们要使用到的数据库的驱动jar包放到classpath下即可获加载该数据库的驱动,然后获取连接和使用。
4、那么这是如何实现这种可插拔的功能的呢?
5、其实这就使用到了我们的SPI机制,首先由JDK定义一个JDBC规范(数据库接口),该规范由启动类加载器进行加载,各个数据库厂商只要定义自己的实现类去实现JDBC规范即可。数据库厂商开发完成自己的数据库驱动以后,就将驱动程序打成jar包,然后提供给用户。然后用户只需要将该jar包放置到应用程序的classpath下即可使用该数据库驱动了。
简单一句话就是,我JDK给你提供一个JDBC的接口,你们想怎样开发自己的驱动,那是你的事,你只用实现我这个接口就行了,开发完成后将驱动jar包放在应用程序的classpah下即可。
1. SPI基本介绍
SPI是什么?
SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件(解耦和可插拔)。Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。
适用场景:
调用者根据实际使用需要,启用、扩展、或者替换框架的实现策略、尤其是框架方面用得特别多。
解决了什么问题: 解耦
🔐 SPI使用步骤和约定:
1、当服务提供者提供了接口的一种具体实现后,将实现打成jar包。在jar包的META-INF/services
目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名;
2、接口实现类打成的jar包放在应用程序的classpath中(比如将MySQL驱动放在classpath下);
3、主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services
目录下的配置文件找到实现类的全限定名,把类加载到JVM;
4、SPI的实现类必须携带一个不带参数的构造方法(可以利用反射进行实例化);
🔥以数据库驱动为例:
- 首先JDK里定义好了数据库驱动接口Driver,mysql厂商需要使用自己的数据库驱动,那么他需要编写一个类实现Driver接口,并且该类必须有一个无参构造方法。
- 将实现好的驱动打成jar包,在jar包里
META-INF/services
目录下新建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名。 - 最后如果有人需要在他的项目中使用MySQL的驱动来连接数据库的话,那么他只需要将MySQL驱动的jar包放到他的项目的classpath下,使用Class.forName(“xxx”);jvm就会去classpath下加载驱动(JDK1.6以后可以省略)。
那么为什么我们只需要将数据库的jar包放到classpath下就可以将数据库实现类加载到内存呢?这就需要使用到ServiceLoader类了!!!
ServiceLoader
0、ServiceLoader介绍
借助Java doc里面的说明吧!
中文翻译如下:
一个简单的服务提供商加载工具。
服务是一组著名的接口和(通常是抽象的)类。服务提供者是服务的特定实现。提供程序中的类通常实现接口,并子类化服务本身中定义的类。服务提供程序可以扩展的形式安装在Java平台的实现中,也就是说,将jar文件放置在任何常用扩展目录中。也可以通过将提供者添加到应用程序的类路径或通过其他一些特定于平台的方式来使提供者可用。
出于加载的目的,服务由单一类型表示,即单一接口或抽象类。 (可以使用一个具体的类,但是不建议这样做。)给定服务的提供者包含一个或多个具体类,这些具体类使用该提供者特定的数据和代码扩展此服务类型。提供者类通常不是整个提供者本身,而是包含足够信息以决定提供者是否能够满足特定请求的代码以及可以按需创建实际提供者的代码的代理。提供者类的细节往往是高度特定于服务的;没有单个类或接口可能会统一它们,因此此处未定义此类。此功能强制执行的唯一要求是,提供程序类必须具有零参数构造函数,以便可以在加载期间实例化它们。
ServiceLoader类的作用: 根据应用类路径下的META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM中(即将SPI的服务实现类从class path下加载器到jvm中)。
在SPI中一般都是通过==ServiceLoader类(该类中使用线程上下文类加载器进行加载class path下的服务实现类)==来实现将服务实现类加载到内存中的。
⏰ 这里以加载MySQL数据库驱动为例进行源代码说明!!!
一、测试用例代码
public class ServiceLoaderDemo {
public static void main(String[] args) throws Exception {
ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);
Iterator<Driver> iterator = loader.iterator();
while (iterator.hasNext()){
Driver driver = iterator.next();
System.out.println("driver : " + driver.getClass() + "loader :" + driver.getClass().getClassLoader());
}
}
}
二、代码详细
①、ServiceLoader loader = ServiceLoader.load(Driver.class);源码详细
1、首先是由ServiceLoader类调用了load方法,跟踪进去发现该方法取得了上下文类加载器(ClassLoader cl = Thread.currentThread().getContextClassLoader();)然后又调用了另一个load方法!
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
2、再次进入方法内,看见他通过我们传入的ServiceLoader.load(service, cl);
服务的Class对象,和上下文类加载器(这里默认是应用类加载器,该类加载器会加载后文的应用程序classpath下的配置文件),new了一个ServiceLoader返回给我们
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
return new ServiceLoader<>(service, loader);
}
3、然后我们再点进ServiceLoader<>(service, loader);
如下,发现在以下源码中对我们的服务对象,类加载器都做了判空,然后调用了reload方法
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();
}
4、点击进入reload方法,该方法会将已经已经加载到缓存中的驱动providers清空。然后再点击进入new LazyIterator(service, loader);类
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
发现在创建LazyIterator类实例的时候,会将我们的服务的Class以及类加载器设置到变量上去!到这儿第一条语句ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);
就执行完毕了!
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
②、Iterator iterator = loader.iterator();详细
1、首先进入到iterator()方法中,发现该方法有如下内容,在该方法中会执行hasNext()方法,该方法会使用knownProviders.hasNext()判断驱动是否已经加载进来了,若是则返回true,若为没有加载进来的话则调用lookupIterator.hasNext()方法去寻找和加载驱动的实现类
lookupIterator是一个LazyIterator类的对象!即会调用LazyIterator类的hasNext方法
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();
}
};
}
2、点击进入到hasNext()
方法中,如下,可以看到在该方法中又调用了hasNextService()
方法
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);
}
}
3、点击进入方法,该方法如下hasNextService()
,这里我们可以清晰的看见,该方法将我们会去到我们的META-INF/services/
目录下寻找到我们的配置文件,然后使用parse()
方法将配置文件中的每一行数据进行解析!
private static final String PREFIX = "META-INF/services/";
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;
}
③、iterator.next()源码详细
1、接着程序开始执行while (iterator.hasNext());
语句判断迭代器是否为空,跟踪进入该方法,发现在该方法中调用了nextService()
方法,继续跟踪进入nextService()
方法如下(由于篇幅原因删除了部分代码片段),可以看到在该方法中使用了c = Class.forName(cn, false, loader);
语句传入要加载的数据库驱动名字,和类加载器,利用反射机制将MySQL数据库驱动加载到内存中了。
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");
}
}