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]] 后续进行研究


SPI
http://localhost:8080/archives/95e1b19f-b09e-4d0a-9717-1871adf768ee
作者
Administrator
发布于
2024年06月11日
许可协议