双亲委派 当某个类加载器要加载某个类时,它会先将加载任务委托给父类加载器,一直递归到顶层,如果父加载器没有加载这个类,自己才会尝试加载这个类。
类加载器的类别 BootstrapClassLoader(启动类加载器) 该加载器由C++编写,负责加载rt.jar
、resources.jar
等核心库,可以通过Launcher.getBootstrapClassPath().getURLs()
来获取 BootstrapClassLoader
的加载路径。在Java中无法直接获取该加载器的引用和信息
ExtClassLoader(标准扩展类加载器) ExtClassLoader
主要负责加载系统变量java.ext.dirs
中的类,如果不指定,则默认为加载路径为 %JAVA_HOME%/jre/lib/ext
。该加载器实现路径位于 sun.misc.Launcher$ExtClassLoader
。父加载器是BootstrapClassLoader
(由C++实现)
AppClassLoader(系统类加载器) 负责加载classpath
下的类,父加载器是ExtClassLoader
,实现路径位于sun.misc.Launcher$AppClassLoader
双亲委派的作用
避免重复加载,父加载器加载过的类,子加载器无需再加载
避免核心类库被篡改
JDBC与破坏双亲委派 JDBC之所以要破坏双亲委派模式是因为,JDBC的核心在rt.jar中由启动类加载器加载,而其实现则在各厂商实现的的jar包中,根据类加载机制,若A类调用B类,则B类由A类的加载器加载,也就是说启动类加载器要加载jar包下的类,我们都知道这是不可能的,启动类加载器负责加载%JAVA_HOME%/jre/lib/
中的一些类,那么JDBC是如何加载这些Driver实现类的?
测试代码 以下是两个不同方式连接mysql数据库(需预先导入mysql驱动)
方式一:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import com.mysql.cj.jdbc.Driver;import java.sql.Connection;import java.sql.SQLException;import java.util.Properties;public class ConnectionTest1 { public static void main (String[] args) { Properties info = new Properties(); info.setProperty("user" , "root" ); info.setProperty("password" , "123456" ); try { Connection con = new Driver().connect("jdbc:mysql://localhost:3306/test?serverTimezone=Asia/Shanghai" , info); System.out.println("连接成功" ); } catch (SQLException e) { e.printStackTrace(); } } }
方式二:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import java.sql.DriverManager;import java.sql.SQLException;public class ConnectionTest2 { public static void main (String[] args) { try { DriverManager.getConnection("jdbc:mysql://localhost:3306/test?serverTimezone=Asia/Shanghai" , "root" , "123456" ); System.out.println("连接成功" ); } catch (SQLException e) { e.printStackTrace(); } } }
方式一和方式二最明显的区别是:方式一显式地指定了com.mysql.cj.jdbc.Driver
类为数据库连接驱动,而方式二并未指定连接驱动。
源码分析 当我们加载数据库驱动类的时候,驱动类一般会将自己主动注册到注册表中,这里以com.mysql.cj.Driver
为例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class Driver extends NonRegisteringDriver implements java .sql .Driver { static { try { java.sql.DriverManager.registerDriver(new Driver()); } catch (SQLException E) { throw new RuntimeException("Can't register driver!" ); } } public Driver () throws SQLException { } }
在早期的JDBC版本中,我们常看到这样一种写法:Class.forName("com.mysql.Driver")
,通过手动加载驱动类,来实现驱动注册,JDBC3.0之后的版本中已经不需要这样手动加载驱动了。
DriverManager 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 31 32 33 34 private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();private static Connection getConnection ( String url, java.util.Properties info, Class<?> caller) throws SQLException { for (DriverInfo aDriver : registeredDrivers) { if (isDriverAllowed(aDriver.driver, callerCL)) { try { println(" trying " + aDriver.driver.getClass().getName()); Connection con = aDriver.driver.connect(url, info); if (con != null ) { println("getConnection returning " + aDriver.driver.getClass().getName()); return (con); } } catch (SQLException ex) { if (reason == null ) { reason = ex; } } } else { println(" skipping: " + aDriver.getClass().getName()); } } }
在测试代码的方式二中,调用了DriverManager#getConnection
方法。该方法的核心逻辑是遍历JDBC驱动注册表中注册的驱动,尝试连接,若某个驱动连接成功,则直接返回连接。
问题:JDBC3.0之后是何时加载的驱动类
针对这个问题我们继续分析 DriverManager
1 2 3 4 static { loadInitialDrivers(); println("JDBC DriverManager initialized" ); }
DriverManager
中有这样一段静态代码块,调用了 loadInitialDrivers
方法
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 31 private static void loadInitialDrivers () { AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run () { ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class ) ; Iterator<Driver> driversIterator = loadedDrivers.iterator(); try { while (driversIterator.hasNext()) { driversIterator.next(); } } catch (Throwable t) { } return null ; } }); }
通过loadInitialDrivers
方法中,通过 ServiceLoader
加载了满足一定条件的 java.sql.Driver
的实现类,这就是Java提供的SPI机制
SPI机制 SPI ,全称为 Service Provider Interface,是一种服务发现机制。它通过在 classpath
路径下的META-INF/services
文件夹查找文件,该文件的文件名为被实现类的全限定名,自动加载文件里所定义的类。
首先看看 mysql-connector-java.jar
是如何利用SPI实现驱动加载的
该文件的内容只是 java.sql.Driver
的实现类
1 com.mysql.cj.jdbc.Driver
ServiceLoader 在 DriverManager#loadInitialDrivers
调用了 ServiceLoader
的迭代器 LazyIterator
该迭代器的 hasNext
核心实现在 hasNextService
中
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 private static final String PREFIX = "META-INF/services/" ;private boolean hasNextService () { if (nextName != null ) { return true ; } if (configs == null ) { try { 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()); } nextName = pending.next(); return true ; }
该迭代器的next
方法核心实现为 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 31 32 private S nextService () { if (!hasNextService()) throw new NoSuchElementException(); String cn = nextName; nextName = null ; Class<?> c = null ; try { 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" ); } 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(); }
结论 DriverManager
利用了 SPI 机制,加载驱动类,驱动类将自己注册到了注册表中,通过遍历注册表获取连接
由于各个厂商的驱动类无法直接通过 BootstrapClassLoader
加载,因此需要将加载的任务委托给线程上下文的加载器