java本身有一套資源管理服務(wù)JNDI,是放置在rt.jar中,由啟動(dòng)類加載器加載的。以對數(shù)據(jù)庫管理JDBC為例,
java給數(shù)據(jù)庫操作提供了一個(gè)Driver接口:
public interface Driver {
Connection connect(String url, java.util.Properties info)
throws SQLException;
boolean acceptsURL(String url) throws SQLException;
DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info)
throws SQLException;
int getMajorVersion();
int getMinorVersion();
boolean jdbcCompliant();
public Logger getParentLogger() throws SQLFeatureNotSupportedException;
}
然后提供了一個(gè)DriverManager來管理這些Driver的具體實(shí)現(xiàn):
public class DriverManager {
// List of registered JDBC drivers 這里用來保存所有Driver的具體實(shí)現(xiàn)
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
public static synchronized void registerDriver(java.sql.Driver driver)
throws SQLException {
registerDriver(driver, null);
}
public static synchronized void registerDriver(java.sql.Driver driver,
DriverAction da)
throws SQLException {
/* Register the driver if it has not already been added to our list */
if(driver != null) {
registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
} else {
// This is for compatibility with the original DriverManager
throw new NullPointerException();
}
println("registerDriver: " + driver);
}
}
這里省略了大部分代碼,可以看到我們使用數(shù)據(jù)庫驅(qū)動(dòng)前必須先要在DriverManager中使用registerDriver()注冊,然后我們才能正常使用。
不破壞雙親委派模型的情況(不使用JNDI服務(wù))
我們看下mysql的驅(qū)動(dòng)是如何被加載的:
// 1.加載數(shù)據(jù)訪問驅(qū)動(dòng)
Class.forName("com.mysql.jdbc.Driver");
//2.連接到數(shù)據(jù)"庫"上去
Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb?characterEncoding=GBK", "root", "");
核心就是這句Class.forName()觸發(fā)了mysql驅(qū)動(dòng)的加載,我們看下mysql對Driver接口的實(shí)現(xiàn):
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()其實(shí)觸發(fā)了靜態(tài)代碼塊,然后向DriverManager中注冊了一個(gè)mysql的Driver實(shí)現(xiàn)。
這個(gè)時(shí)候,我們通過DriverManager去獲取connection的時(shí)候只要遍歷當(dāng)前所有Driver實(shí)現(xiàn),然后選擇一個(gè)建立連接就可以了。
破壞雙親委派模型的情況
在JDBC4.0以后,開始支持使用spi的方式來注冊這個(gè)Driver,具體做法就是在mysql的jar包中的META-INF/services/java.sql.Driver 文件中指明當(dāng)前使用的Driver是哪個(gè),然后使用的時(shí)候就直接這樣就可以了:
Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb?characterEncoding=GBK", "root", "");
可以看到這里直接獲取連接,省去了上面的Class.forName()注冊過程。
現(xiàn)在,我們分析下看使用了這種spi服務(wù)的模式原本的過程是怎樣的:
- 第一,從META-INF/services/java.sql.Driver文件中獲取具體的實(shí)現(xiàn)類名“com.mysql.jdbc.Driver”
- 第二,加載這個(gè)類,這里肯定只能用class.forName("com.mysql.jdbc.Driver")來加載
好了,問題來了,Class.forName()加載用的是調(diào)用者的Classloader,這個(gè)調(diào)用者DriverManager是在rt.jar中的,ClassLoader是啟動(dòng)類加載器,而com.mysql.jdbc.Driver肯定不在<JAVA_HOME>/lib下,所以肯定是無法加載mysql中的這個(gè)類的。這就是雙親委派模型的局限性了,父級加載器無法加載子級類加載器路徑中的類。
那么,這個(gè)問題如何解決呢?按照目前情況來分析,這個(gè)mysql的drvier只有應(yīng)用類加載器能加載,那么我們只要在啟動(dòng)類加載器中有方法獲取應(yīng)用程序類加載器,然后通過它去加載就可以了。這就是所謂的線程上下文加載器。
線程上下文類加載器可以通過Thread.setContextClassLoaser()方法設(shè)置,如果不特殊設(shè)置會從父類繼承,一般默認(rèn)使用的是應(yīng)用程序類加載器
很明顯,線程上下文類加載器讓父級類加載器能通過調(diào)用子級類加載器來加載類,這打破了雙親委派模型的原則
現(xiàn)在我們看下DriverManager是如何使用線程上下文類加載器去加載第三方j(luò)ar包中的Driver類的。
public class DriverManager {
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
//省略代碼
//這里就是查找各個(gè)sql廠商在自己的jar包中通過spi注冊的驅(qū)動(dòng)
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
//省略代碼
}
}
使用時(shí),我們直接調(diào)用DriverManager.getConn()方法自然會觸發(fā)靜態(tài)代碼塊的執(zhí)行,開始加載驅(qū)動(dòng)
然后我們看下ServiceLoader.load()的具體實(shí)現(xiàn):
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader){
return new ServiceLoader<>(service, loader);
}
可以看到核心就是拿到線程上下文類加載器,然后構(gòu)造了一個(gè)ServiceLoader,后續(xù)的具體查找過程,我們不再深入分析,這里只要知道這個(gè)ServiceLoader已經(jīng)拿到了線程上下文類加載器即可。
接下來,DriverManager的loadInitialDrivers()方法中有一句driversIterator.next();,它的具體實(shí)現(xiàn)如下:
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
//此處的cn就是產(chǎn)商在META-INF/services/java.sql.Driver文件中注冊的Driver具體實(shí)現(xiàn)類的名稱
//此處的loader就是之前構(gòu)造ServiceLoader時(shí)傳進(jìn)去的線程上下文類加載器
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
//省略部分代碼
}
現(xiàn)在,我們成功的做到了通過線程上下文類加載器拿到了應(yīng)用程序類加載器(或者自定義的然后塞到線程上下文中的),同時(shí)我們也查找到了廠商在子級的jar包中注冊的驅(qū)動(dòng)具體實(shí)現(xiàn)類名,這樣我們就可以成功的在rt.jar包中的DriverManager中成功的加載了放在第三方應(yīng)用程序包中的類了。
總結(jié)
這個(gè)時(shí)候我們再看下整個(gè)mysql的驅(qū)動(dòng)加載過程:
- 第一,獲取線程上下文類加載器,從而也就獲得了應(yīng)用程序類加載器(也可能是自定義的類加載器)
- 第二,從META-INF/services/java.sql.Driver文件中獲取具體的實(shí)現(xiàn)類名“com.mysql.jdbc.Driver”
- 第三,通過線程上下文類加載器去加載這個(gè)Driver類,從而避開了雙親委派模型的弊端
很明顯,mysql驅(qū)動(dòng)采用的這種spi服務(wù)確確實(shí)實(shí)是破壞了雙親委派模型的,畢竟做到了父級類加載器加載了子級路徑中的類。