SPI機(jī)制與JDBC的應(yīng)用分析

Java的類加載機(jī)制的核心是雙親委派模型论咏,雙親委派模型(不存在自定義類加載器的情況下)加載某個(gè)類時(shí)會(huì)先委托父加載器尋找目標(biāo)類勤哗,找不到再委托上層父加載器加載抡爹,如果所有父加載器在自己的加載類路徑下都找不到目標(biāo)類,則在自己的類加載路徑中查找并載入目標(biāo)類芒划。
Java里有如下幾種類加載器
引導(dǎo)類加載器:負(fù)責(zé)加載支撐JVM運(yùn)行的位于JRE的lib目錄下的核心類庫(kù)冬竟,比如
rt.jar、charsets.jar等
擴(kuò)展類加載器:負(fù)責(zé)加載支撐JVM運(yùn)行的位于JRE的lib目錄下的ext擴(kuò)展目錄中的JAR
類包
應(yīng)用程序類加載器:負(fù)責(zé)加載ClassPath路徑下的類包民逼,主要就是加載你自己寫的那
些類或引用的非核心類庫(kù)與
自定義加載器:負(fù)責(zé)加載用戶自定義路徑下的類包
SPI的全名是Service Provider Interface泵殴,SPI:
Java 提供了很多服務(wù)提供者接口(Service Provider Interface,SPI)拼苍,允許第三方為這些接口提供實(shí)現(xiàn)笑诅。常見的 SPI 有 JDBC调缨、JCE、JNDI吆你、JAXP 和 JBI 等弦叶。
這些 SPI 的接口由 Java 核心庫(kù)來(lái)提供,而這些 SPI 的實(shí)現(xiàn)代碼則是作為 Java 應(yīng)用所依賴的 jar 包被包含進(jìn)類路徑(CLASSPATH)里妇多。SPI接口中的代碼經(jīng)常需要加載具體的實(shí)現(xiàn)類伤哺。那么問(wèn)題來(lái)了,SPI的接口是Java核心庫(kù)的一部分者祖,是由啟動(dòng)類加載器(Bootstrap Classloader)來(lái)加載的立莉;SPI的實(shí)現(xiàn)類是由系統(tǒng)類加載器(System ClassLoader)來(lái)加載的。引導(dǎo)類加載器是無(wú)法找到 SPI 的實(shí)現(xiàn)類的七问,因?yàn)橐勒针p親委派模型蜓耻,BootstrapClassloader無(wú)法委派AppClassLoader來(lái)加載類。
先看看SPI的實(shí)現(xiàn)方式械巡,以JDBC為例:

image.png

在mysql驅(qū)動(dòng)包中刹淌,存在META-INF/services/java.sql.Driver文件,內(nèi)容就是mysql驅(qū)動(dòng)包里對(duì)java.sql.Driver接口的實(shí)現(xiàn)類全類名讥耗。
項(xiàng)目中引入mysql驅(qū)動(dòng)包后芦鳍,直接通過(guò)DriverManager就可以獲得mysql的Connection對(duì)象。
image.png

根據(jù)使用方式葛账,我們提出幾個(gè)問(wèn)題。
1.META-INF/services/java.sql.Driver這個(gè)文件是做什么用的皮仁?

  1. mysql的驅(qū)動(dòng)包被加載的過(guò)程是什么籍琳?

  2. 它是如何打破雙親委派機(jī)制的?
    這幾個(gè)問(wèn)題需要通過(guò)jdk的源碼實(shí)現(xiàn)來(lái)回答贷祈,首先看DriverManager的靜態(tài)代碼塊趋急,它會(huì)在DriverManager類被加載時(shí)執(zhí)行:

    static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
    }
    再看loadInitialDrivers方法的實(shí)現(xiàn):
    private static void loadInitialDrivers() {
    String drivers;
    try {
    // 先讀取系統(tǒng)屬性
    drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
    public String run() {
    return System.getProperty("jdbc.drivers");
    }
    });
    } catch (Exception ex) {
    drivers = null;
    }
    // 通過(guò)SPI加載驅(qū)動(dòng)類
    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) {
    // Do nothing
    }
    return null;
    }
    });
    // 繼續(xù)加載系統(tǒng)屬性中的驅(qū)動(dòng)類
    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);
    // 使用AppClassloader加載
    Class.forName(aDriver, true,
    ClassLoader.getSystemClassLoader());
    } catch (Exception ex) {
    println("DriverManager.Initialize: load failed: " + ex);
    }
    }
    }
    其中與SPI核心相關(guān)的內(nèi)容已經(jīng)給出注釋,其余內(nèi)容是從system參數(shù)中加載驅(qū)動(dòng),因?yàn)槲覀儧](méi)有設(shè)置系統(tǒng)參數(shù)势誊,所以相關(guān)邏輯不會(huì)執(zhí)行呜达,關(guān)鍵看以下代碼:
    ServiceLoader.load(Driver.class);
    方法實(shí)現(xiàn)如下(按照方法堆棧調(diào)用依次貼出實(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);
    }

private ServiceLoader(Class<S> svc, ClassLoader cl) {
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    reload();
}

public void reload() {
    providers.clear();
    lookupIterator = new LazyIterator(service, loader);
}

可以看到ServiceLoader使用了一個(gè)ClassLoader是Thread.currentThread().getContextClassLoader(),這個(gè)ClassLoader如果沒(méi)有專門的設(shè)置,返回的是AppClassLoader對(duì)象粟耻,這就是SPI打破雙親委派機(jī)制的關(guān)鍵所在查近,因?yàn)閖ava.sql.Driver接口是java核心包的類,所以根據(jù)雙親委派它應(yīng)該由BootstrapClassLoader來(lái)加載挤忙,但是ServiceLoader在實(shí)現(xiàn)SPI機(jī)制的過(guò)程中霜威,使用AppClassLoader來(lái)加載,所以它才能成功加載到mysql的驅(qū)動(dòng)類册烈。但是此處只是返回了ServiceLoader對(duì)象戈泼,不能說(shuō)明加載mysql驅(qū)動(dòng)真正使用的是AppClassLoader,我們繼續(xù)看,剩余的步驟是拿到ServiceLoader的遍歷器對(duì)象,然后進(jìn)行了遍歷大猛,可以看到上面的reload方法中初始化了一個(gè)LazyIterator對(duì)象扭倾,我們來(lái)看LazyIterator類中的關(guān)鍵代碼(hasNext方法會(huì)調(diào)用):


image.png

先簡(jiǎn)單回復(fù)圖片中的一個(gè)疑點(diǎn),loader對(duì)象為什么是URLClassLoader對(duì)象而不是AppClassLoader對(duì)象挽绩,這是因?yàn)橥ㄟ^(guò)IDEA運(yùn)行java程序膛壹,斷點(diǎn)功能等需要IDEA通過(guò)額外的技術(shù)實(shí)現(xiàn),所以使用的classloader是經(jīng)過(guò)處理的琼牧,我已經(jīng)測(cè)試用system.printIn.out打印Thread.currentThread().getContextClassLoader(),在IDEA的運(yùn)行結(jié)果是URLClassLoader對(duì)象恢筝,但是通過(guò)java命令運(yùn)行的結(jié)果是AppClassLoader對(duì)象,有疑問(wèn)的小伙伴可以自行測(cè)試一下巨坊。
通過(guò)斷點(diǎn)調(diào)試可以看到fullName等于META-INF/services/java.sql.Driver撬槽,這里與第一個(gè)問(wèn)題的目錄結(jié)構(gòu)相呼應(yīng),也就解釋了為什么要用那樣的目錄接口與文件命名趾撵,fullName是由常量PREFIX和service.getName()的拼裝的侄柔,進(jìn)而說(shuō)明,ServiceLoader類是SPI的通用實(shí)現(xiàn)類占调,它不僅僅可以加載java.sql.Driver,傳入響應(yīng)的接口暂题,在指定目錄下創(chuàng)建接口全限定類名文件,并寫入第三方實(shí)現(xiàn)類究珊,同樣可以加載薪者,它是java SPI機(jī)制的通用實(shí)現(xiàn)。
下面需要看最核心的加載邏輯(next()方法會(huì)調(diào)用):


image.png

熟悉的Class.forName剿涮,關(guān)鍵看參數(shù)言津,加載的類是com.mysql.jdbc.Driver,使用的ClassLoader是URLClassLoader對(duì)象(實(shí)際運(yùn)行無(wú)特殊處理是使用AppClassLoader對(duì)象)取试。
以上分析與debug調(diào)試過(guò)程已經(jīng)充分說(shuō)明了JDBC對(duì)SPI的實(shí)際應(yīng)用過(guò)程悬槽。額外補(bǔ)充對(duì)DriverManager.getConnection方法的源碼分析。

private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
//.....省略非關(guān)鍵內(nèi)容
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());
        }

    }
//....省略非關(guān)鍵內(nèi)容
}
可以看到關(guān)鍵邏輯是遍歷registeredDrivers拿到driver對(duì)象瞬浓,嘗試通過(guò)連接信息獲取連接初婆,如果連接不為空,返回連接對(duì)象猿棉。說(shuō)明DriverManager可能同時(shí)持有多種數(shù)據(jù)庫(kù)驅(qū)動(dòng)類磅叛,會(huì)使用連接信息逐一嘗試,連接成功后會(huì)返回∪蓿現(xiàn)在的問(wèn)題轉(zhuǎn)化為registeredDrivers里驅(qū)動(dòng)對(duì)象是在什么時(shí)候放入的宪躯,通過(guò)IDEA的方法反調(diào)不難找到如下代碼:
image.png

可以看到
mysql的驅(qū)動(dòng)類的靜態(tài)代碼塊中調(diào)用了DriverManager#registerDriver方法,將自己注冊(cè)到了registeredDrivers中位迂。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末访雪,一起剝皮案震驚了整個(gè)濱河市详瑞,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌臣缀,老刑警劉巖坝橡,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異精置,居然都是意外死亡计寇,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門脂倦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)番宁,“玉大人,你說(shuō)我怎么就攤上這事赖阻〉海” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵火欧,是天一觀的道長(zhǎng)棋电。 經(jīng)常有香客問(wèn)我,道長(zhǎng)苇侵,這世上最難降的妖魔是什么赶盔? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮榆浓,結(jié)果婚禮上于未,老公的妹妹穿的比我還像新娘。我一直安慰自己陡鹃,他們只是感情好沉眶,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著杉适,像睡著了一般。 火紅的嫁衣襯著肌膚如雪柳击。 梳的紋絲不亂的頭發(fā)上猿推,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音捌肴,去河邊找鬼蹬叭。 笑死,一個(gè)胖子當(dāng)著我的面吹牛状知,可吹牛的內(nèi)容都是我干的秽五。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼饥悴,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼坦喘!你這毒婦竟也來(lái)了盲再?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤瓣铣,失蹤者是張志新(化名)和其女友劉穎答朋,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體棠笑,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡梦碗,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蓖救。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片洪规。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖循捺,靈堂內(nèi)的尸體忽然破棺而出斩例,到底是詐尸還是另有隱情,我是刑警寧澤巨柒,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布樱拴,位于F島的核電站,受9級(jí)特大地震影響洋满,放射性物質(zhì)發(fā)生泄漏晶乔。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一牺勾、第九天 我趴在偏房一處隱蔽的房頂上張望正罢。 院中可真熱鬧,春花似錦驻民、人聲如沸翻具。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)裆泳。三九已至,卻和暖如春柠硕,著一層夾襖步出監(jiān)牢的瞬間工禾,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工蝗柔, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留闻葵,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓癣丧,卻偏偏與公主長(zhǎng)得像槽畔,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子胁编,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容