這段時間跟類加載機制是干上了。
這一篇來分析一下jdbc工作過程中涉及到的類加載流程,重點是想看看在雙親委派模型不適用的時候,如何解決。
第一步,加載數據庫的驅動
Class.forName("oracle.jdbc.driver.OracleDriver")
Class.forName("com.mysql.jdbc.Driver")
Class.forName 方法會根據類的全路徑名稱去加載對應的class文件,生成類型,並初始化類型。也就是說static語句塊會執行。
下面來看看 com.mysql.jdbc.Driver 類
1 public class Driver extends NonRegisteringDriver implements java.sql.Driver { 2 // 3 // Register ourselves with the DriverManager 4 // 5 static { 6 try { 7 java.sql.DriverManager.registerDriver(new Driver()); 8 } catch (SQLException E) { 9 throw new RuntimeException("Can't register driver!"); 10 } 11 } 12 13 /** 14 * Construct a new driver and register it with DriverManager 15 * 16 * @throws SQLException 17 * if a database error occurs. 18 */ 19 public Driver() throws SQLException { 20 // Required for Class.forName().newInstance() 21 } 22 }
里面的主要邏輯都在父類 NonRegisteringDriver 里實現,而static語句塊就做了一件事:生成驅動實例,並向DriverManager注冊。所謂注冊,就是將driver的信息保存起來,以便后來取用。
第二步,取得數據庫連接connection
Connection conn= DriverManager.getConnection(url, user, password);
這里為什么通過DriverManager來取,而不是直接通過生成driver來取???后面馬上揭曉!!!
1 public static Connection getConnection(String url, 2 String user, String password) throws SQLException { 3 java.util.Properties info = new java.util.Properties(); 4 5 if (user != null) { 6 info.put("user", user); 7 } 8 if (password != null) { 9 info.put("password", password); 10 } 11 12 return (getConnection(url, info, Reflection.getCallerClass())); 13 }
Reflection.getCallerClass() 是取得調用類,這個方法是native的。我這里是jdk1.8,以前的版本不是調用的這個方法,如果感興趣也可以看看。
1 private static Connection getConnection( 2 String url, java.util.Properties info, Class<?> caller) throws SQLException { 3 /* 4 * When callerCl is null, we should check the application's 5 * (which is invoking this class indirectly) 6 * classloader, so that the JDBC driver class outside rt.jar 7 * can be loaded from here. 8 */ 9 ClassLoader callerCL = caller != null ? caller.getClassLoader() : null; 10 synchronized(DriverManager.class) { 11 // synchronize loading of the correct classloader. 12 if (callerCL == null) { 13 callerCL = Thread.currentThread().getContextClassLoader(); 14 } 15 } 16 17 if(url == null) { 18 throw new SQLException("The url cannot be null", "08001"); 19 } 20 21 println("DriverManager.getConnection(\"" + url + "\")"); 22 23 // Walk through the loaded registeredDrivers attempting to make a connection. 24 // Remember the first exception that gets raised so we can reraise it. 25 SQLException reason = null; 26 27 for(DriverInfo aDriver : registeredDrivers) { 28 // If the caller does not have permission to load the driver then 29 // skip it. 30 if(isDriverAllowed(aDriver.driver, callerCL)) { 31 try { 32 println(" trying " + aDriver.driver.getClass().getName()); 33 Connection con = aDriver.driver.connect(url, info); 34 if (con != null) { 35 // Success! 36 println("getConnection returning " + aDriver.driver.getClass().getName()); 37 return (con); 38 } 39 } catch (SQLException ex) { 40 if (reason == null) { 41 reason = ex; 42 } 43 } 44 45 } else { 46 println(" skipping: " + aDriver.getClass().getName()); 47 } 48 49 } 50 51 // if we got here nobody could connect. 52 if (reason != null) { 53 println("getConnection failed: " + reason); 54 throw reason; 55 } 56 57 println("getConnection: no suitable driver found for "+ url); 58 throw new SQLException("No suitable driver found for "+ url, "08001"); 59 }
這個方法一開始就要得到調用類caller的類加載器callerCL,為的是后面再去加載數據庫的driver,做一下驗證,
具體在 isDriverAllowed(aDriver.driver, callerCL) 里面的代碼里。
那問題來了,為什么就不能用加載DriverManager的類加載器呢???
因為DriverManager在rt.jar里面,它的類加載器上啟動類加載器。而數據庫的driver(com.mysql.jdbc.Driver)是放在classpath里面的,啟動類加載器是不能加載的。所以,如果嚴格按照雙親委派模型,是沒辦法解決的。而這里的解決辦法是:通過調用類的類加載器去加載。而如果調用類的加載器是null,就設置為線程的上下文類加載器:
Thread.currentThread().getContextClassLoader()
好的,下面通過Thread類的源碼,分析線程的上下文類加載器。
/* The context ClassLoader for this thread */ private ClassLoader contextClassLoader; // 這段if---else代碼出自init方法 if (security == null || isCCLOverridden(parent.getClass())) this.contextClassLoader = parent.getContextClassLoader(); else this.contextClassLoader = parent.contextClassLoader; public ClassLoader getContextClassLoader() { if (contextClassLoader == null) return null; SecurityManager sm = System.getSecurityManager(); if (sm != null) { ClassLoader.checkClassLoaderPermission(contextClassLoader, Reflection.getCallerClass()); } return contextClassLoader; } public void setContextClassLoader(ClassLoader cl) { SecurityManager sm = System.getSecurityManager(); if (sm != null) { sm.checkPermission(new RuntimePermission("setContextClassLoader")); } contextClassLoader = cl; }
init方法里面代碼的邏輯是:把父線程的上下文類加載器給繼承過來。這里的父子關系是指誰啟動誰的關系,比如在線程A里面啟動了線程B,那B線程的父線程就是A。
既然都是一路繼承,那第一個啟動的線程(包含main方法的那個線程)里面的contextClassLoader是誰設置的呢???
這就要看 sun.misc.Launcher 這個類的源碼。Launcher是JRE中用於啟動程序入口main()的類。
loader = AppClassLoader.getAppClassLoader(extcl);
Thread.currentThread().setContextClassLoader(loader);
這里截取的兩行代碼出自 Launcher 的構造方法。第一行用一個擴展類加載器extcl構造了一個系統類加載器loader,第二行把loader設置為當前線程(包含main方法)的類加載器。所以,我們啟動一個線程的時候,如果之前都沒有調用 setContextClassLoader 方法明確指定的話,默認的就是系統類加載器。
到這里,整個加載流程基本上一目了然了。
現在,再回到之前 DriverManager的getConnection 方法,好像還有一個疑問沒有解決。
for(DriverInfo aDriver : registeredDrivers) { // If the caller does not have permission to load the driver then // skip it. if(isDriverAllowed(aDriver.driver, callerCL)) { try { println(" trying " + aDriver.driver.getClass().getName()); Connection con = aDriver.driver.connect(url, info); if (con != null) { // Success! println("getConnection returning " + aDriver.driver.getClass().getName()); return (con); } } catch (SQLException ex) { if (reason == null) { reason = ex; } } } else { println(" skipping: " + aDriver.getClass().getName()); } }
最后返回一個connection,但是在一個循環里面。也就是說,當我們注冊了多個數據庫驅動,mysql,oracle等;DriverManager都幫我們管理了,它會取出一個符合條件的driver,就不用我們在程序里自己去控制了。