通过JDBC驱动加载理解线程上下文加载器
JDBC JDBC
提供了独立于数据库的统一API
,MySQL
,Oracle
等数据库公司都可以基于这个标准接口来进行开发。包括java .sql
包下的Driver
,Connection
,Statement
,ResultSet
是JDBC
提供的接口。而DriverManager
是用于管理JDBC
驱动的服务类,主要用于获取Connection
对象(此类中全是静态方法)
下面是一个简单的jdbc连接数据库过程。重点不在能不能连接,而是其加载过程。
1 2 3 4 5 6 7 8 9 10 11 12 13 package Test;import java.sql.Connection;import java.sql.DriverManager;public class Test { public static void main (String[] args) throws Exception { Class.forName("com.mysql.cj.jdbc.Driver" ); String url = "jdbc:mysql://localhost:3306/mytest" ; Connection connection = DriverManager.getConnection( url, "username" , "password" ); } }
Class.forName 1 Class.forName("com.mysql.cj.jdbc.Driver" );
查看forName
1 2 3 4 5 6 7 8 9 10 11 12 public static Class<?> forName(String className) throws ClassNotFoundException { Class<?> caller = Reflection.getCallerClass(); return forName0(className, true , ClassLoader.getClassLoader(caller), caller); }
查看forName0
,其是一个native
方法
1 2 3 4 private static native Class<?> forName0(String name, boolean initialize, ClassLoader loader, Class<?> caller) throws ClassNotFoundException;
1 2 3 4 5 6 7 8 9 10 11 12 13 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!" ); } } }
从上边可以看到,它是用静态代码块实现的。根据类加载机制,当执行Class.forName(driverClass)
获取其Class
对象时,com.mysql.jdbc.Driver
就会被JVM
加载连接,并进行初始化,初始化就会执行静态代码块,也就会执行下边这句代码:
DriverManager.registerDriver(new Driver());
这里会注册驱动,调用的是DriverManager
的registerDriver
方法,那么在调用之前,也会初始化DriverManager
类。
DriverManager 1 2 3 4 5 6 7 8 9 public class DriverManager { ... static { loadInitialDrivers(); println("JDBC DriverManager initialized" ); } ... }
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 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); Iterator<Driver> driversIterator = loadedDrivers.iterator(); try { while (driversIterator.hasNext()) { driversIterator.next(); } } catch (Throwable t) { } 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); } } }
balabala大半天意思就是说如果jdbc.drivers
这个系统属性为空,则会用ServerLoader机制去加载jdbc驱动。
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
会加载所有在META-INF/services/java.sql.Driver
文件里边的类到JVM内存,完成驱动的自动加载,也就我们讲的ServiceLoader
加载JDBC驱动机制
这个自动加载采用的技术叫做SPI
,在JDBC 4.0之后实际上我们不需要再调用Class.forName
来加载驱动程序了,我们只需要把驱动的jar包放到工程的类加载路径里,那么驱动就会被自动加载
这就是SPI
的优势所在,能够自动的加载类到JVM内存。这个技术在阿里的dubbo
框架里面也占到了很大的分量。
registerDriver 1 2 3 4 5 6 7 8 9 10 11 public static synchronized void registerDriver (java.sql.Driver driver) throws SQLException { registerDriver(driver, null ); }
registerDriver(driver, null);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public static synchronized void registerDriver (java.sql.Driver driver, DriverAction da) throws SQLException { if (driver != null ) { registeredDrivers.addIfAbsent(new DriverInfo(driver, da)); } else { throw new NullPointerException(); } println("registerDriver: " + driver); }
这个驱动列表定义:
1 2 private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
DriverManager.getConnection 我们加载完驱动后就可以连接数据库,但是我们并没有看到mysql相关信息,接着我们看下getConnection
方法,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public static Connection getConnection (String url, String user, String password) throws SQLException { java.util.Properties info = new java.util.Properties(); if (user != null ) { info.put("user" , user); } if (password != null ) { info.put("password" , password); } return (getConnection(url, info, Reflection.getCallerClass())); }
接着会发现会去调用getConnection三个参数的方法
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 35 36 37 38 39 40 41 42 43 44 45 46 private static Connection getConnection ( String url, java.util.Properties info, Class<?> caller) throws SQLException { ClassLoader callerCL = caller != null ? caller.getClassLoader() : null ; synchronized (DriverManager.class) { if (callerCL == null ) { callerCL = Thread.currentThread().getContextClassLoader(); } } ... 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()); } } ... } }
一个项目里边很可能会即连接MySQL,又连接Oracle,这样在一个工程里边就存在了多个驱动类,那么这些驱动类又是怎么区分的呢?
关键点就在于getConnection
的步骤,DriverManager.getConnection
中会遍历所有已经加载的驱动实例去创建连接,当一个驱动创建连接成功时就会返回这个连接,同时不再调用其他的驱动实例。如上面
1 2 3 for (DriverInfo aDriver : registeredDrivers){ ... }
是不是每个驱动实例都真真实实的要尝试建立连接呢?不是的!
每个驱动实例在getConnetion的第一步就是按照url判断是不是符合自己的处理规则,是的话才会和db建立连接。比如,MySQL驱动类中的关键代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public boolean acceptsURL (String url) throws SQLException { return (parseURL(url, null ) != null ); } public Properties parseURL (String url, Properties defaults) throws java.sql.SQLException { Properties urlProps = (defaults != null ) ? new Properties(defaults) : new Properties(); if (url == null ) { return null ; } if (!StringUtils.startsWithIgnoreCase(url, URL_PREFIX) && !StringUtils.startsWithIgnoreCase(url, MXJ_URL_PREFIX) && !StringUtils.startsWithIgnoreCase(url, LOADBALANCE_URL_PREFIX) && !StringUtils.startsWithIgnoreCase(url, REPLICATION_URL_PREFIX)) { return null ; }
参考:
Java动态加载数据库驱动 | Mingyu’s Blog | 我荒废的今日,正是昨天殒身之人祈求的明日
JDBC驱动加载机制 - 程序园