一、什么是双亲委派机制?
我们要获得一个类的Class实例,可以采用如下方式:
那么在Class的forName(className)
方法中,会根据是谁调用了Class.forName(className)这个方法,那么就获得当时加载了它的那个ClassLoader,然后,再通过类加载器来负责对类进行加载操作。
获得了类加载器之后,就可以通过如下的loadClass(...)
方法对className执行类加载操作的。那么在如下的代码逻辑中,可以看到双亲委派机制的逻辑了。也就是说,如果自己没有加载过className这个类,则必须“委托”给父加载器执行加载操作。除非父类加载器无法加载这个类,才由自己执行加载操作。
那么,为什么父类加载器无法加载某个类呢?原因就是,每个类型的加载器z都有其约束的加载路径,如果这个className
没有在这个路径下,那么对应的类加载器就无法加载这个类了。如下所示:
二、什么情况下,需要破坏双亲委派机制?
我们以使用JDBC操作数据库时的代码为例:
public class JdbcTest { public static void main(String[] args) { Class.forName("com.mysql.jdbc.Driver"); Connection conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/seata?useSSL=false&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true", "root", "root"); ... ... } }
【解释】由于是JdbcTest类调用了Class.forName方法,并且
JdbcTest
类是由AppClassLoader加载的,所以先由AppClassLoader去查找自己是否加载过类"com.mysql.jdbc.Driver",发现没有加载过,那么委托ExtClassLoader去加载,而它之前也没有加载过这个类,那么委托给BootstrapClassLoader去加载,但是由于com.mysql.jdbc.Driver这个类在CLASSPATH中,而不在rt.jar
和ext/*.jar
里,所以最终还是会由AppClassLoader去加载。
当"com.mysql.jdbc.Driver
"这个类加载后,就会执行它内部的静态方法,将new Driver()实例对象注册到DriverManager中,这样就完成了驱动注册操作。
public class Driver extends NonRegisteringDriver implements java.sql.Driver { public Driver() throws SQLException {} // 静态块执行语句 static { try { DriverManager.registerDriver(new Driver()); } catch (SQLException var1) { throw new RuntimeException("Can't register driver!"); } } }
在JDBC4.0以后,开始支持使用SPI的方式来注册这个Driver,具体做法就是在mysql的jar包中的META-INF/services/java.sql.Driver
文件中指明当前使用的Driver是哪个。
那么,我们以往的数据库连接代码就可以改成这样:
public class JdbcTest { public static void main(String[] args) { // Class.forName("com.mysql.jdbc.Driver"); // 此处就不需要显示通过Class.forName(...)执行类的加载操作了 Connection conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/seata?useSSL=false&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true", "root", "root"); ... ... } }
这个不对啊?Driver在哪加载了呢?我们来看一下DriverManager类(JDK8)的静态方法就知道了。如下所示:
static { loadInitialDrivers(); // 加载初始化驱动器 println("JDBC DriverManager initialized"); } 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() { ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); // 使用SPI加载Driver Iterator<Driver> driversIterator = loadedDrivers.iterator(); try{ while(driversIterator.hasNext()) { driversIterator.next(); } } catch(Throwable t) {} return null; } }); if (drivers == null || drivers.equals("")) return; String[] driversList = drivers.split(":"); for (String aDriver : driversList) { try { Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()); } catch (Exception ex) { println("DriverManager.Initialize: load failed: " + ex); } } }
【解释】在上面的代码中,我们看到了
ServiceLoader.load(Driver.class)
这行代码,它是基于SPI的方式去加载了Driver实例了。
那为啥要这么做呢?直接在DriverManager中编写Class.forName("com.mysql.cj.jdbc.Driver")
这行代码不就可以了嘛,采用SPI难道是为了炫技嘛?
其实不是的,因为DriverManager
这个类的全路径名是java.sql.DriverManager
,它是在rt.jar
包中的,所以它是由BootstrapClassLoader
负责加载的。而com.mysql.cj.jdbc.Driver
类不在rt.jar
包中,所以BootstrapClassLoader
无法加载,而BootstrapClassLoader
又是顶层的类加载器了,它没有父加载器,所以既无法委托给父加载器负责加载,而自己又无法加载,那么就只能报错ClassNotFoundException了。
那这个问题怎么解决呢?SPI的出现解决了这个问题,下面我们来看一下SPI是如何处理的,即:
ServiceLoader.load(Driver.class);
那么问题又来了,这个Thread.currentThread().getContextClassLoader()
是从当前线程的上下文中获得了ClassLoader,那到底这个ClassLoader是什么呢?想要找到答案,我们就需要看Launcher类了,它是负责用来创建类加载器的。
【解释】从上面代码我们可以看到,当前线程的上下文中获得的ClassLoader就是AppClassLoader,那么它加载的类路径就是CLASSPATH,就可以成功的加载"com.mysql.cj.jdbc.Driver"这个类了。它采用的这种方式,就是破坏双亲委托机制来加载Driver类了。
今天的文章内容就这些了:
写作不易,笔者几个小时甚至数天完成的一篇文章,只愿换来您几秒钟的 点赞 & 分享 。
更多技术干货,欢迎大家关注公众号“爪哇缪斯” ~ \(^o^)/ ~ 「干货分享,每天更新」