从ContextClassLoader到JDBC和SPI

SystemClassLoader

可以jvm参数 java.system.class.loader 设置默认的系统加载器,默认是当前线程的ContextClassLoader。来自sun.misc.Launcher.getLauncher的getClassLoader, 而getLauncher的getClassLoader实际来自Launcher.AppClassLoader.getAppClassLoader。

ContextClassLoader

  • 主要用于打破双亲模型
    一个典型的例子就是JDBC,JDBC的规范类是在rt.jar包里面的,默认是由BootstrapClassLoadr加载的,所以JDBC所依赖的类,比如我们自己实现的MYSQL,也应该是由BootstrapClassLoadr来加载,但是我们的MYSQL包路径并不在BootstrapClassLoadr加载的路径当中,这样就会出现找不到类的情况。所以在这种情况下面,我们就需要打破双亲委派模型,ContextClassLoader就应运而生。在BootstrapClassLoadr加载不到的情形下面,由BootstrapClassLoadr转接到当前线程的ContextClassLoader来加载。在SPI场合下都会出现,SPI就是JDK提供接口,具体实现由各个厂商实现,最后由ServiceLoader来进行加载。

SPI的关键,ServiceLoader

  • SPI的具体实现,也就是jar包在service由ServiceLoader的load方法出触发

  • 声明,泛型,且实现了迭代接口

    1
    public final class ServiceLoader<S>  implements Iterable<S>
  • 我们可以看一下jdoc里面的描述

  • A service provider is identified by placing a provider-configuration file in the resource directory META-INF/services. The file’s name is the fully-qualified binary name of the service’s type. The file contains a list of** fully-qualified binary names of concrete provider classes**, one per line. Space and tab characters surrounding each name, as well as blank lines, are ignored. The comment character is ‘#’ (‘\u0023’, NUMBER SIGN); on each line all characters following the first comment character are ignored. The file must be encoded in UTF-8.

  • 这实际上就描述了,service的接口和具体实现类声明具体位置。


    跟踪JDBC加载的过程

  • 首先引入myql和oracle的JDBC包

  • 安装过程中发现maven pom导入失败 mvn 手动安装

    1
    mvn install:install-file -Dfile=/Users/kenchan/ojdbc7-12.1.0.2.jar -DgroupId=oracle -DartifactId=ojdbc7 -Dversion=12.1.0.2 -Dpackaging=jar
  • 接口就是 java.sql.Driver,是JDK实现的规范。

  • 我们可以看到mysql和oracle的jar 包中的service声明

image.png

  • 具体的实现类分别是 com.mysql.cj.jdbc.Driver ,oracle.jdbc.OracleDriver类
  • 测试代码,我们用ServiceLoader.load去加载一次java.sql.Driver.class

    1
    2
    3
    4
    5
    //ServiceLoader服务提供者,加载实现的服务
    ServiceLoader<java.sql.Driver> loader = ServiceLoader.load(java.sql.Driver.class);
    for (java.sql.Driver driver : loader) {
    System.out.println("driver:" + driver.getClass() + ",loader" + driver.getClass().getClassLoader());
    }
  • 可以看到我们就是把jar包导入环境,什么都没做,就已经显示加载成功了JDBC的2个实现对象

  • 而且可以看到他是由AppClassLoader加载的,但java.sql.Driver是由BoostrapClassLoader加载的。

    1
    2
    driver:class com.mysql.cj.jdbc.Driver,loadersun.misc.Launcher$AppClassLoader@135fbaa4
    driver:class oracle.jdbc.OracleDriver,loadersun.misc.Launcher$AppClassLoader@135fbaa4
  • 首先可以看到 ** java.util.ServiceLoader#load(java.lang.Class)**

    1
    2
    3
    4
    5
    //在这里去获取了 ContextClassLoader
    public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
    }
  • 转接到java.util.ServiceLoader#ServiceLoader(Class svc, ClassLoader cl)

1
2
3
4
5
6
private ServiceLoader(Class<S> svc, ClassLoader cl) {
//如果线程 ContextClassLoader 为null,则选用SystemClassLoader,也就是AppClassLoader去加载
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
  • reload()里面是new了一个LazyIterator 就走了。实际加载是使用的时候,及时调用迭代器next获取的时候,
1
2
3
4
5
6
7
8
9
10
11
public S next() {
if (acc == null) {
//就是这里
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
  • 在java.util.ServiceLoader.LazyIterator#hasNextService 里面做了扫描,从META-INF/services/里面去读取具体的实现类,保留了nextName

  • 那ServiceLoader怎么知道只加载JDBC呢?其它的service它不加载??? 这就是通过String fullName = _PREFIX _+ service.getName();限定来获取的,service.getName() 我们传入的泛型及时service就是java.sql.Driver,也可以从上面看到jar包中的service下的文件名就是接口。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    private boolean hasNextService() {
    if (nextName != null) {
    return true;
    }
    if (configs == null) {
    try {
    //熟悉的样子,这里就是从META-INF/services/获取具体的类限定名
    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());
    }
    //比如这里就可能获取到com.mysql.cj.jdbc.Driver
    nextName = pending.next();
    return true;
    }
  • 转接到 java.util.ServiceLoader.LazyIterator#nextService 里面,这里实际上就是通过反射做了真正的加载

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    private S nextService() {
    if (!hasNextService())
    throw new NoSuchElementException();

    //nextName就是上面获取到的
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
    //这里反射获取,可以看到用到了loader 就是ContextClassLoader。
    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");
    }
    //通过反射,利用我们自定的ContextClassLoader 做了加载之后,就是利用反射生成一个实例,放到缓里面
    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
    }
  • 到此,JDBC的实现类加载完毕

  • 我们用JDBC 没有像这样去掉loader啊???其实你在用java.sql.DriverManager的时候,java.sql.DriverManager#loadInitialDrivers里面 调用了ServiceLoader.load(Driver.class);

总结

  • 双亲委派模型在SPI机制下面缺陷,不能加载具体的实现类
  • JDK采用ContextClassLoader可以让我们自定加载器去加载具体的实现类
  • 具体的实现类,归结到反射方法,可以让加载类的时候,指定具体的ClassLoader
    1
    public static Class<?> forName(String name, boolean initialize,ClassLoader loader)