1.双亲委派被打破的历史
双亲委派在历史上总共有三次较大规模被破坏,这里的打破没有贬义,只是特定场景下满足需要而做的处理:
- 第一次:java设计初期就有ClassLoader和类加载器概念,但是没有双亲委派模型,为了兼容后面代码无法用技术手段避免loadClass()被子类覆盖的可能,只能新增一个protected的findClass()方法。
- 第二次:设计缺陷导致,由于越基础的类越由上层加载器进行加载,但如果有基础类又要调用回用户的代码(比如JNDI服务),这个时候就不能满足,Java的设计团队为此引入了一个不太优雅的设计:线程上下文类加载器
- 第三次:是由于用户对程序动态性(代码热替换、模块热部署)的追求而导致的。如OSGi.
2.双亲委派被打破的应用
这里以通过SPI加载JDBC应用为实例说明下打破双亲委派来实现父级类加载器调用子级加载器进行加载。
2.1 mysql-connector-java驱动包结构
这里通过maven添加的jar包如下:
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>
添加后对应的jar包结构如下:
注:这里是标准的SPI使用结构
2.2 传统加载jdbc的过程(不打破双亲委派)
在传统使用JDBC的过程中,需要指定要加载JDBC驱动程序类
核心就是Class.forName()触发了mysql驱动的加载,我们看下mysql对Driver接口的实现(即com.mysql.cj.jdbc.Driver类)
Class.forName()触发了静态代码块,然后向DriverManager中注册了一个mysql的Driver实现。这时候,通过DriverManager去获取connection的时候只要遍历当前所有的Driver实现,然后选择一个建立连接就可以了。
Class.forName()加载类的时候使用的类加载器是调用者的类加载器,在自己的项目代码中调用Class.forName()这个方法,因为自己写的项目代码是在ClassPath路径下,而这个路径下的类的类加载器就是应用类加载器(AppClassLoader),所以直接就可以用这个系统类加载器去加载com.mysql.jdbc.Driver这个类。
2.3 使用SPI加载(打破双亲委派)
在JDBC 4.0以后,开始支持使用SPI的方式来注册这个Driver,具体的做法就是在mysql的驱动jar包中通过META-INF/services/java.sql.Driver 文件(见上面 2.1)指明当前使用哪个实现类(实现了java.sql.Driver的类),使用的时候直接如下就可以了:
这里可以看到直接获取链接,省去了Class.forName()的注册过程,所以新版本可以不写Class.forName("com.mysql.jdbc.Driver")这句代码了。
现在,来分析下使用了SPI服务模式的过程:
- 第一:从META-INF/services/java.sql.Driver文件中获取具体的实现类"com.mysql.jdbc.Driver";
- 第二:加载这个实现类,这里也只能又回归到传统的方式class.forName("com.mysql.jdbc.Driver")来加载
★重点来了,Class.forName()加载用的是调用者的ClassLoader,然后通过SPI这种调用方式,这个调用者DriverManager是在rt.jar中的,rt.jar的ClassLoader是启动类加载器(Bootstrap classLoader),而com.mysql.jdbc.Driver肯定不在启动类加载器的加载范围内($JAVA_HOME/lib),所以肯定无法用启动类加载器来加载mysql中的这个类,这就是双亲委派的局限性了,父级加载器无法加载子级加载器范围中的类。
●重点:解决方案。按照目前情况看,这个mysql的driver只有应用加载器(AppClassLoader)能加载,那么只要在启动类加载器中想办法获取应用加载器,然后就可以通过应用加载器去加载了。这里就用到了上面谈到的线程上下文加载器
线程上下文类加载器
从上面的类加载器初始化过程可以看出,线程上下文类加载器就是使用的应用加载器(AppClassLoader)。很明显,父类加载器让子级加载器加载类,这打破了双亲委派的原则。
那么DriverManager是如何使用线程上下文类加载器去加载第三方jar包中的Driver类的。
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
//这里就是查找各个sql厂商在自己的jar包中通过SPI注册的驱动
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
这里着重看一下ServiceLoader.load(Driver.class)的具体实现
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
可以看到,核心就是拿到线程上下文类加载器,然后构造了一个ServiceLoader.load(service, cl)继续后续的查找过程,这里只要知道使用的是线程上下文类加载器即可。
如上面代码所示,在DriverManager的loadInitialDrivers()的方法中有一句driversIterator.next();它的具体实现如下:
此处的cn就是在META/INF/services/java.sql.Driver文件中注册的Driver具体实现类。loader就是之前获取的上下文类加载器(持有的是AppClassLoader)。
现在,通过上下文类加载器拿到了应用类加载器,同时也查到了厂商在子级jar包中注册的驱动具体实现类名,这样就可以成功在rt.jar包中的DriverManager里加载放在第三方应用程序包中的类了。
2.3 总结
再回过头来看下mysql驱动加载的过程:
- 第一:获取上下文类加载器,从而也就获得了应用程序类加载器
- 第二:从META/INF/services/java.sql.Driver文件中获取具体的实现类名:“com.mysql.jdbc.Driver”
- 第三:通过上下文类加载器去加载这个实现类,从而克服了双亲委派的局限性。
总之,mysql驱动采用的这种SPI做到父级类加载器加载子级路径中的类,一定程度是不符合双亲委派原则的。