SPI
面试题
(1)基本概念
- SPI基本概念
- SPI与API的区别
- SPI还用在了哪些方面
1. 基本概念
SPI基本概念
- 服务提供者的接口,专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口
- SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方
SPI与API的区别
- SPI: 当接口存在于调用方这边时,就是 SPI ,由接口调用方确定接口规则,然后由不同的厂商去根据这个规则对这个接口进行实现,从而提供服务
- API: 当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API
SPI还用在了哪些方面
- Spring框架
- 数据库加载驱动,Dubbo的扩展实现
2. JDK SPI 机制
(1)Java SPI,需要遵循如下约定:
-
当
服务提供者提供了接口的一种具体实现
后,在jar包的META-INF/services
目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名列表(约定大于配置的体现) -
接口实现类所在的
jar包放在主程序的classpath
中 -
主程序通过java.util.ServiceLoader动态装载实现模块,它通过
扫描META-INF/services目录下的以接口全限定名为命名的配置文件
找到实现类的全限定名,把类加载到JVM;
public static void main(String[] args) {
// 选择load的接口,会根据这个接口去META-INF/Services 找接口全限定名相同的文件,
// 在文件中 根据类的全限定名,去加载到JVM中
ServiceLoader<Serializer> serviceLoader = ServiceLoader.load(Serializer.class);
Iterator<Serializer> iterator = serviceLoader.iterator();
while (iterator.hasNext()) {
Serializer serializer= iterator.next();
System.out.println(serializer.getClass().getName());
}
}
3. JDK SPI原理分析
(1)JDK SPI机制是一种服务发现机制,动态地为接口寻找服务实现。它的核心来自于ServiceLoader
这个类
- 测试代码
public static void main(String[] args) {
// 选择load的接口,会根据这个接口去META-INF/Services 找接口全限定名相同的文件,
// 在文件中 根据类的全限定名,去加载到JVM中
ServiceLoader<Serializer> serviceLoader = ServiceLoader.load(Serializer.class);
Iterator<Serializer> iterator = serviceLoader.iterator();
while (iterator.hasNext()) {
Serializer serializer= iterator.next();
System.out.println(serializer.getClass().getName());
}
}
(2)ServiceLoader#load(java.lang.Class<S>)
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取当前线程的ClassLoader 类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
// 重载类
return ServiceLoader.load(service, cl);
}
(3)ServiceLoader.load(service, cl);
最终会调用下面的构造器
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()
方法需要关注一下
-
这里创建了
LazyIterator
,后面会使用它来遍历Fruit.class
public void reload() { providers.clear(); // 创建了一个迭代器 lookupIterator = new LazyIterator(service, loader); }
(5)小总结
ServiceLoader#load(Class<S>)
实际上是创建了一个LazyIterator
迭代器对象。
ServiceLoader#load(java.lang.Class<S>)
ServiceLoader.load(service, cl)
ServiceLoader#reload
new LazyIterator
(6)serviceLoader.iterator()
- 这里创建了一个
Iterator
,当我们调用它的hasNext
方法时,debug走下来看,其实就是调用LazyIterator#hasNext
方法,然后会调用hasNextService
方法
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();
}
}
LazyIterator#hasNext
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);
}
}
(6)小总结
- 下,通过
serviceLoader.iterator()
方法创建的Iterator
对象,它的hasNext
方法和next
方法实际上是调用了LazyIterator
中的对应方法,所以真正的主角就是LazyIterator
对象。
(7)LazyIterator#hasNextService
- 获取
fullName
(其实就是我们在META-INF/services
目录下定义的文件名) - 返回
CompoundEnumeration
: 对应的资源文件 - 判断
CompoundEnumeration
中是否有元素- 如果不存在元素,直接返回false
- 反之,则去解析该资源文件
- pending = parse(service, configs.nextElement()); 获得资源文件内所有类的限定名列表
pending
中的第一个元素赋值给nextName
引用变量 (就是对应的加载的类名)
private boolean hasNextService() {
if (nextName != null) {
return true;
}
// 如果不是第一次执行,configs !=null ,不仅会进入第一个判断
if (configs == null) {
try {
// 文件名:META-INF/services/cn.ajin.practical.java.spi.Fruit
// service.getName() : 类名,这里就是 cn.ajin.practical.java.spi.Fruit
// 获得文件类名
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
// 根据fullName获取Enumeration对象
configs = loader.getResources(fullName);
} catch (IOException x) {
...
}
}
while ((pending == null) || !pending.hasNext()) {
// 判断元素是否存在
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
// cn.ajin.practical.java.spi.Orange
nextName = pending.next();
return true;
}
(8)下面是LazyIterator
中比较重要的实例变量:
// 根据fullName查找到的 Enumeration集合 指的是资源的URL
Enumeration<URL> configs = null;
// 通过config解析出的迭代器
Iterator<String> pending = null; // 存放是资源中所有的类名
// 下一个元素
String nextName = null;
(9)LazyIterator的两个方法 next 和 nextService
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);
}
}
// 加载对应的类
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
}
(10)LazyIterator迭代原理
-
首先ServiceLoader#load(java.lang.Class
) 方法传入要SPI服务的实现接口 -
进入后,则调用相关的另一个重写方法 ServiceLoader.load(service, cl),cl为类加载器
-
进入后,主要进入函数ServiceLoader#reload,创建了一个new LazyIterator
-
用户通过 serviceLoader.iterator()可以获取一个遍历所有实现类的对象的迭代器,当我们调用它的
hasNext
方法时,debug走下来看,其实就是调用LazyIterator#hasNext
方法,然后会调用hasNextService
方法 -
在调用hasNext时,使用调用
hasNextService
方法,第一次调用时,config为空,通过ClassLoader.getSources()方法获取一个URL对象的 Enumeration集合,给config,该对象为资源加载的文件路径 -
然后 判断pending == null(表示该资源文件的加载类名称列表 还没加载) 或 !pending.hasNext()(表示资源文件的类的列表都被加载完),然后通过pend.parse()方法将 资源文件下的所有类名称都放入pending,nextName 赋值为下一个加载的类名称
-
然后调用next方法,最终会使用LazyIteratro#NextService方法, 根据nextName 加载类对象再返回,类名和对象会作为key-value放到LinkedHashMap中
-
延迟加载原理:
- hasNext,第一次会加载config对象和pend对象,第二次调用,会直接NextName = pending.next()赋值为下一个要记载类的名称
- next: 根据NextName去加载类并返回类对象。
(11)简要步骤
-
首先,ServiceLoader实现了
Iterable
接口,所以它有迭代器的属性,这里主要都是实现了迭代器的hasNext
和next
方法。这里主要都是调用的lookupIterator
的相应hasNext
和next
方法,lookupIterator
是懒加载迭代器。 -
其次,
LazyIterator
中的hasNext
方法,静态变量PREFIX就是”META-INF/services/”
目录,这也就是为什么需要在classpath
下的META-INF/services/
目录里创建一个以服务接口命名的文件 -
最后,通过反射方法
Class.forName()
加载类对象,并用newInstance
方法将类实例化,并把实例化后的类缓存到providers
对象中,(LinkedHashMap<String,S>
类型)然后返回实例对象 -
调用LayIterator的hasNext方法时,才会加载配置文件解析(只会加载异常)
-
调用LayIterator的next方法时,才会进行实例化缓存
4. JDK SPI机制的优缺点
(1)优点:JDK SPI使得我们可以面向接口编程,无需硬编码的方式即可引入实现类
(2)缺点
:
不能按需加载
,虽然ServiceLoader
做了延迟载入,但是基本只能通过遍历全部获取
,也就是接口的实现类得全部载入并实例化一遍。就是接口的实现类得全部载入并实例化一遍。如果你并不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。- 获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。
- 多个并发多线程使用 ServiceLoader 类的实例是不安全的。
(3)注意
- 通过serviceLoader 获取的iterator 是一个新的Iterator
- 他的hasNext()会先去 缓存的map中判断,取不到才会去构造函数创建的lookupIterator中判断
- next也一样
5. SPI打破双亲委派机制
上下文加载器
(1)线程上下问类加载器出现的原因
- 越基础的类由越上层的加载器进行加载,如果
基础类又要调用回用户的代码
,那该怎么办?- 解决方案:使用“线程上下文类加载器”
(2)线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoaser()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
(3)有了线程上下文类加载器,也就是父类加载器请求子类加载器去完成类加载的动作
- 打通了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则。
SPI打破双亲委派机制举例
(1)主要思想
- 对于 SPI 来说,有些接口是 Java 核心库所提供的,而 Java 核心库是由启动类加载器来加载的
- 而这些接口的实现却来自于不同的 jar 包(厂商提供),Java 的启动类加载器是不会加载其他来源的 jar 包,这样传统的双亲委托模型就无法满足 SPI 的要求。
- 通过给当前线程设置上下文类加载器,就可以由设置的上下文类加载器来实现对于接口实现类的加载。
(2)JDBC举例
-
DriverManager是通过Bootstrap ClassLoader加载进来的
-
而com.mysql.jdbc.Driver是通过classpath的JAR包加载进来的,应该通过AppClassLoader
-
当DriverManager方法内去加载Driver时,会默认使用调用者的类加载器,所以就会通过启动类加载器去加载Driver,这是不能实现的
-
通过Thread.currentThread().setContextClassLoader(),将Application ClassLoader设置为线程上下文加载器。在DriverManager类里通过Thread.currentThread().getContextClassLoader()拿到Application ClassLoader进行加载