我們都知道類加載的雙親委派模型
雙親委派模型并不是一個強制限制模型,而是java設計者推薦給開發者的類加載實作方式;但是也會有例外; 今天我們主要來講一講 類似于SPI這種設計導緻的雙親委派模型被“破壞”的情況;
1JDBC
不破壞雙親委派模型的情況(不使用JNDI服務)
// 1.加載資料通路驅動
Class.forName("com.mysql.cj.jdbc.Driver");
//2.連接配接到資料"庫"上去
Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/test?characterEncoding=GBK", "root", "");
Class.forName("com.mysql.cj.jdbc.Driver"); 這句會主動去加載類com.mysql.cj.jdbc.Driver
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
//
// Register ourselves with the DriverManager
//
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
public Driver() throws SQLException {
// Required for Class.forName().newInstance()
}
}
可以看到,Class.forName()其實觸發了靜态代碼塊,然後向DriverManager中注冊了一個mysql的Driver實作。 這個時候,我們通過DriverManager去擷取connection的時候隻要周遊目前所有Driver實作,然後選擇一個建立連接配接就可以了
破壞雙親委派模型的情況
在JDBC4.0以後,開始支援使用spi的方式來注冊這個Driver,具體做法就是在mysql的jar包中的META-INF/services/java.sql.Driver 檔案中指明目前使用的Driver是哪個,然後使用的時候就直接這樣就可以了
Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/test?characterEncoding=GBK", "root", "");
可以看到這裡直接擷取連接配接,省去了上面的Class.forName()注冊過程。 現在,我們分析下看使用了這種spi服務的模式原本的過程是怎樣的:
在這裡插入圖檔描述
- 從META-INF/services/java.sql.Driver檔案中擷取具體的實作類名“com.mysql.cj.jdbc.Driver”
- 加載這個類,用class.forName("com.mysql.jdbc.Driver")來加載
Class.forName()加載用的是調用者的Classloader, 這個調用者DriverManager是在rt.jar中的,ClassLoader是啟動類加載器,而com.mysql.jdbc.Driver肯定不在<JAVA_HOME>/lib下,是以肯定是無法加載mysql中的這個類的。這就是雙親委派模型的局限性了,父級加載器無法加載子級類加載器路徑中的類。
如何解決父加載器無法加載子級類加載器路徑中的類
我們分析一下,想要正常的加載,啟動類加載器肯定不能加載,那麼隻能用應用類加載器能夠加載,那麼如果有什麼辦法能夠擷取到應用類加載器就可以解決問題了;我們看看 jdk是怎麼做的;
線程上下文類加載器
public class DriverManager {
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
//省略代碼
//這裡就是查找各個sql廠商在自己的jar包中通過spi注冊的驅動
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
//省略代碼
}
}
看這裡,加載的時候去擷取了一個加載器
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
擷取線程上下文類加載器Thread.currentThread().getContextClassLoader(); 這個值如果沒有特定設定,一般預設使用的是應用程式類加載器;
總結
為了實作SPI這種模式,實作可插拔 做出了不符合雙親委派原則行為,但是這種破壞并不具備貶義的感情色彩,隻要有足夠意義和理由,突破已有的原則就可以認為是一種創新;
對于線程上下文類加載器 的實作類似于ThreadLocal 将變量傳遞到整個線程的生命周期; 這裡無非就是将ThreadLocal裡面存放的是應用類加載器;