从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声明
- 具体的实现类分别是 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
2driver: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 | private ServiceLoader(Class<S> svc, ClassLoader cl) { |
- reload()里面是new了一个LazyIterator 就走了。实际加载是使用的时候,及时调用迭代器next获取的时候,
1 | public S next() { |
在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
27private 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
31private 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)