在前面5篇文章介绍了类加载器和对应的双亲委托模式后,我们还需要了解“破坏”双亲模式的特例,而这种特例在很多SPI(服务提供接口)架构中有着广泛的应用,甚至说 ,如果不破坏双亲模式,Java的很多功能都是无法实现的。
以jdbc的模式进行举例:java对jdbc的规范中,只是定义了Driver(驱动)、Connection(连接)等接口,具体的实现是由各自的数据库厂商决定的,比如mysql厂商的驱动是com.jdbc.mysql.driver,oracel的驱动为com.jdbc.oracle.driver,显然不一样。在jvm启动是,因为java.util.sql包的加载是由根类加载器进行加载的,但实际使用的具体实现对应的加载不可能是根类加载器(具体实现的类是位于classpath中,肯定不在rt.jar中,因此根类加载器加载不了),是由系统类加载器加载的,那么问题就出现了:
按照双亲委托模式的原则,父类加载器是看不到子类加载器所加载的类,换言之,在实际应用中,真正的数据库却动或者连接就找不到真正的实现类了。这就是SPI的通病,因此引入了线程上下文类加载器(ThreadContent ClassLoader)
线程上下文加载器的作用:在当前线程中,可以使用设定的加载器进行加载;默认的线程上下文加载器是系统类加载器,在虚拟机启动时,机器码指定会进行加载根类加载器,继而在根类加载器里面创建对应的应用类加载器和扩展类加载器;这个类就是Launcher类
这块指定了根类加载器的加载路径,我们查看Launcher的构造方法
public Launcher() {
Launcher.ExtClassLoader var1;
try {
//见名知意,获取扩展类加载器
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
//获取应用类加载器
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
//这就是关键处:将获取到的应用类加载器加载放到线程上下文中,包括了默认和自定义的
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
if (var2 != null) {
SecurityManager var3 = null;
if (!"".equals(var2) && !"default".equals(var2)) {
try {
var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
} catch (IllegalAccessException var5) {
} catch (InstantiationException var6) {
} catch (ClassNotFoundException var7) {
} catch (ClassCastException var8) {
}
} else {
var3 = new SecurityManager();
}
if (var3 == null) {
throw new InternalError("Could not create SecurityManager: " + var2);
}
System.setSecurityManager(var3);
}
}
细节处:根类加载器和应用加载器的加载路径,我们都从Launcher中得知:
在产生应用类加载器的代码中,Luncher调用了ClassLoader中privilegedAction中的run方法,我们查询run方法,里面的秘密就出来了:
run方法里面揭秘
因此从上述总结出,默认的或者自定义的系统类加载器都会放到线程上下文中。
下面重点详解:线程上下文加载器:
(双亲模式是使用加载调用该类的加载器进行加载:A是使用了B,那么加载A的加载器也会尝试去加载B,以B为第一层子类)
一般的使用流程是:取出->使用->放回
取出:
ClassLader cl= Thread.currentThread().getContentClassLoader();
try{
Thread.currentThread().setContentClassLoader(自己指定的加载器);
//method()方法
}finally{
//设定的用完之后,将原线程的加载器放回
Thread.currentThread().setContentClassLoader(cl);
}
要了解上下文加载器,首先需要引入一个核心类:ServiceLoader我们以mysql实现JDBC的标准为例
先看示例代码:
public static void main(String[] args) throws Exception {
//java.sql.Driver jdbc标准对应的二进制名称
Class<Driver> driverClazz = (Class<Driver>) Class.forName("java.sql.Driver");
//查看具体被加载出来的实现类
Iterator<Driver> iterator = ServiceLoader.load(driverClazz).iterator();
while (iterator.hasNext()) {
Driver next = iterator.next();
System.out.println("加载到的具体实现驱动" + next);
}
System.out.println("加载mysql驱动的加载器:" + driverClazz.getClassLoader());
System.out.println("当前线程的上下文类加载器:" + Thread.currentThread().getContextClassLoader());
//查看Launcher类的加载器
System.out.println("加载jdbc标准规范的加载器:" + Launcher.class.getClassLoader());
}
得出的结果为:
从结果我们可知:1、加载标准规范的驱动是由根类加载器;2、从classpath中的路径中加载出来了具体的实现驱动(包含了msql,alibaba的)3、当前线程的上下文加载器是应用类加载器。这三点就完全验证了我们前面所讲述的内容。
最后还有一点,这些具体实现厂商的驱动是如何被发现和加载的呢?查看ServiceLoader的文档:
是不是豁然开朗,一切的一切的都已经在doc文档中说明(看资料还是看源码,看官方注解文档最地道),原来是需要一个配置文件,名称和对应的目录位置都已明确,按照标准服务的二进制名称进行命名,具体的实现按照每行给出,我们找到jar包中的路径:
这样就解释了我们可以找到具体实现的根本原因了,下面为什么这些是由应用类加载器加载的,继续跟代码
在刚刚调用的load方法的实现里,第一行就是我们今天的主角:线程上下文加载器,然后传递下去进行加载动作
最后就顺其自然了,在加载的过程中,我拿到可以加载classpath路径中的加载器后,那么相对应的实现类也就加载成功了,从标准到具体实现,全部加载成功,而且根据配置文件里面内容的二进制名称,顺利找到对应的类,一切和谐,完美~