SPI
在 ClassPath 路径下配置添加文件,META-INF/services/java. sql. Driver 文件名为接口全限定类名
在配置文件中为实现类的全限定类名。
@Test
public void test1(){
ServiceLoader<Driver> load = ServiceLoader.load(Driver.class);
Iterator<Driver> it = load.iterator();
while (it.hasNext()) {
Driver next = it.next();
System.out.println(next);
}
}
1. ServiceLoader 的核心源码分析
public final class ServiceLoader<S> implements Iterable<S>{
// 需要加载的资源的配置文件路径
private static final String PREFIX = "META-INF/services/";
// 加载的服务类或接口
private Class<S> service;
// 类加载时用到的类加载器
private ClassLoader loader;
// 基于实例的已加载的顺序缓存类,其中Key为实现类的全限定类名
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// "懒查找"迭代器,ServiceLoader的核心
private LazyIterator lookupIterator;
public void reload() {
// 清空缓存
providers.clear();
// 构造LazyIterator实例
lookupIterator = new LazyIterator(service, loader);
}
// 私有构造方法
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = svc;
loader = cl;
reload();
}
}
加载方法
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取当前线程的线程上下文类加载器实例,确保通过此classLoader也能加载到项目中的资源文件
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader)
{
return new ServiceLoader<>(service, loader);
}
public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
ClassLoader cl = ClassLoader.getSystemClassLoader();
ClassLoader prev = null;
while (cl != null) {
prev = cl;
cl = cl.getParent();
}
return ServiceLoader.load(service, prev);
}
Java 类加载的过程通常是遵循双亲委派模型的。但是对于 SPI 接口实现类的加载就需要破坏双亲委派模型。
首先 java. sql. DriverManager 是由启动类加载器加载的,创建真正的 Dirver 对象时需要使用到 mysql 提供的实现:com. mysql. jdbc. Dirver,即要初始化该类。但是启动类加载器加载 DirverManager 的时候,使用到了启动类加载器无法加载的类,这时候就需要由系统类加载器来加载。com. mysql. jdbc. Dirver 通常放在类路径下的(其实不一定)。到这里和线程上下文类加载器没由任何关系。在 DriverManager 中使用系统类加载的时候,可以直接使用静态方法 ClassLoader. getSystemClassLoader (),但是这种情况的前提是 com. mysql. jdbc. Dirver 类在类路径下。如果不在类路径下,而且在系统环境中有其他的类加载器,在通过其他类加载器可能出现无法正确加载扩展点的情况。比如某个类的字节码是在数据库中存储,这时我们需要自定义一个类加载器去加载它,这个类加载器会告诉 DriverManager 去我们指定放的地方取。因此 Thread. currentThread (). setContextClassLoader (自定义加载器/默认是系统类加载器); 这个就是线程上下文类加载器起到的介质作用。线程上下文中默认放的是系统类加载器。
2.ServiceLoader 总结
JDK 提供了一种帮第三方实现者加载服务的便捷方式,如 JDBC、日志等,第三方实现者需要遵循约定把具体实现的类名放在/META-INF 里。当 JDK 启动时会去扫描所有 jar 包里符合约定的类名,再调用 forName 进行加载,如果 JDK 的 ClassLoader 无法加载,就使用当前执行线程的线程上下文类加载器。
但是在通过 SPI 查找服务的具体实现时,会遍历所有的实现进行实例化,并在循环中找到需要的具体实现。
3. 源码分析
就是创建了 ServiceLoader 对象, 并没有进行加载
Iterator<Driver> it = load.iterator();
创建一个 Iterator 对象, 实际干活为前面初始化的 lookupIterator 对象
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();
}
};
}
lookupIterator. 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);
}
}
hasNextService ();
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;
}
configs = loader.getResources(fullName);
[[classloader]] 后续进行研究