我们都知道类加载的双亲委派模型
双亲委派模型并不是一个强制约束模型,而是java设计者推荐给开发者的类加载实现方式;但是也会有例外; 今天我们主要来讲一讲 类似于SPI这种设计导致的双亲委派模型被“破坏”的情况;
JDBC
不破坏双亲委派模型的情况(不使用JNDI服务)
// 1.加载数据访问驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//2.连接到数据"库"上去
Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/test?characterEncoding=GBK", "root", "");
Class.forName("com.mysql.cj.jdbc.Driver"); 这句会主动去加载类com.mysql.cj.jdbc.Driver
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
//
// Register ourselves with the DriverManager
//
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
public Driver() throws SQLException {
// Required for Class.forName().newInstance()
}
}
可以看到,Class.forName()其实触发了静态代码块,然后向DriverManager中注册了一个mysql的Driver实现。 这个时候,我们通过DriverManager去获取connection的时候只要遍历当前所有Driver实现,然后选择一个建立连接就可以了
破坏双亲委派模型的情况
在JDBC4.0以后,开始支持使用spi的方式来注册这个Driver,具体做法就是在mysql的jar包中的META-INF/services/java.sql.Driver 文件中指明当前使用的Driver是哪个,然后使用的时候就直接这样就可以了
Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/test?characterEncoding=GBK", "root", "");
可以看到这里直接获取连接,省去了上面的Class.forName()注册过程。 现在,我们分析下看使用了这种spi服务的模式原本的过程是怎样的:
- 从META-INF/services/java.sql.Driver文件中获取具体的实现类名“com.mysql.cj.jdbc.Driver”
- 加载这个类,用class.forName(“com.mysql.jdbc.Driver”)来加载
Class.forName()
加载用的是调用者的Classloader, 这个调用者DriverManager是在rt.jar中的,ClassLoader是启动类加载器,而com.mysql.jdbc.Driver肯定不在/lib下,所以肯定是无法加载mysql中的这个类的。这就是双亲委派模型的局限性了,父级加载器无法加载子级类加载器路径中的类。
如何解决父加载器无法加载子级类加载器路径中的类
我们分析一下,想要正常的加载,启动类加载器肯定不能加载,那么只能用应用类加载器能够加载,那么如果有什么办法能够获取到应用类加载器就可以解决问题了;我们看看 jdk是怎么做的;
线程上下文类加载器
public class DriverManager {
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
//省略代码
//这里就是查找各个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
}
//省略代码
}
}
看这里,加载的时候去获取了一个加载器
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
获取线程上下文类加载器Thread.currentThread().getContextClassLoader();
这个值如果没有特定设置,一般默认使用的是应用程序类加载器;
总结
为了实现SPI这种模式,实现可插拔 做出了不符合双亲委派原则行为,但是这种破坏并不具备贬义的感情色彩,只要有足够意义和理由,突破已有的原则就可以认为是一种创新;
对于线程上下文类加载器的实现类似于ThreadLocal
将变量传递到整个线程的生命周期; 这里无非就是将ThreadLocal里面存放的是应用类加载器;
下面双亲委派的视频讲解(摘自bilibili)
[【Java面试】请介绍类加载过程,什么是双亲委派?]
[【面试突击题】类加载双亲委派机制是怎么回事]
注意:本文归作者所有,未经作者允许,不得转载