目标

通过SPI学习setContextClassLoader,getContextClassLoader的用法

SPI概念

SPI(Service Provider Interface)是JDK内置的一种提供服务发现的机制。 也可以看成是一种服务注册或者ioc

自定义SPI方式

  • 定义一个接口 fluffy.mo.CarPlugin
  • 定义两个实现类 fluffy.mo.RedCarfluffy.mo.BlueCar
  • 配置文件 【META-INF/services/文件名=接口全名】

    META-INF/services/fluffy.mo.CarPlugin

    1
    2
    
    fluffy.mo.RedCar
    fluffy.mo.BlueCar
    
  • 代码加载 ServiceLoader.load(fluffy.mo.CarPlugin.class);

相关案例: 各类jdbc驱动,比如mysql的驱动jar。 lombok也是通过注册插件到javac中,实现编译时修改字节码。

具体案例代码

现有一段测试代码, classpath中含有h2的驱动jar 也可以是mysql的驱动jar

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import java.sql.Driver;
import java.sql.DriverManager;
import java.util.Enumeration;
class TestClassLoader {
    public static void main(String[] args) {
        Enumeration<Driver> drivers = DriverManager.getDrivers();
        while (drivers.hasMoreElements()) {
            Driver driver = (Driver) drivers.nextElement();
            System.out.println(driver.getClass() + " == " + driver.getClass().getClassLoader());
        }
        System.out.println("当前main类加载器 == " + TestClassLoader.class.getClassLoader());
        System.out.println("当前thread类加载器 == " + Thread.currentThread().getContextClassLoader());
        System.out.println("当前driver类加载器 == " + DriverManager.class.getClassLoader());
    }
}

运行结果为

1
2
3
4
class org.h2.Driver == sun.misc.Launcher$AppClassLoader@18b4aac2
当前main类加载器 == sun.misc.Launcher$AppClassLoader@18b4aac2
当前thread类加载器 == sun.misc.Launcher$AppClassLoader@18b4aac2
当前driver类加载器 == null

案例代码分析

DriverManager 位于rt.jar中,类加载器为null , 其类加载器为BootstrapClassloader。

方法-DriverManager.loadInitialDrivers

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class DriverManager {
...
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
...
    private static void loadInitialDrivers() {
    ...
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                // 加载驱动服务类
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                // 视classpath下的jar情况可能会加载多个 
                // 如com.mysql.jdbc.Driver, org.h2.Driver
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                ...
            }
        });
    ...
    }
...
}

代码ServiceLoader.load(Driver.class)这里去加载了h2的驱动
如无意外 org.h2.Driver的类加载器应该是BootstrapClassloader
但是在双亲委派的机制下, BootstrapClassloader作为顶级类加载器只能自己去加载,不会去让子孙AppClassLoader去加载。

此时BootstrapClassloader应当加载失败
那为啥没挂呢? 代码再深入一下ServiceLoader.load 方法

补充:
代理链: AppClassloader -> ExtClassloader -> BootstrapClassloader
AppClassLoader 加载类时,会先找父类加载器,父类加载器加载成功则结束
不成功则AppClassLoader自己去加载

通常我们写的代码以及相关框架jar包都是在classpath目录下,由AppClassLoader去加载。
BootstrapClassloader只会去加载java的核心jar,不会去看classpath目录。

方法-ServiceLoader.load

1
2
3
4
5
6
7
8
public class ServiceLoader {
...
    public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }
...
}

事实上呢,ServiceLoader在加载驱动类时,指定了当前线程的类加载器。
在没有setContextClassLoader的情况下默认为SystemClassLoader
也就是AppClassLoader

小结

java.lang.Thread有两个方法.getContextClassLoader(), setContextClassLoader()
通过这两个方法,使用当前线程中转了类加载器,解决了父classloader无法将加载class的任务交给子classloader的问题。 (ps: 通过编码的方式使用类加载器去加载类,自然也能绕开BootstrapClassloader类加载器。)