jdbc的 spi 服務(wù)機制

[TOC]

SPI 在jdbc driver的運用

這幾天在看java 類加載機制,看到 spi 服務(wù)機制破壞了雙親委派模型,特地研究了下典型的 spi 服務(wù) jdbc 驅(qū)動
首先運行一下代碼,查看 mysql jdbc 驅(qū)動的類加載(maven 項目已經(jīng)引進 jdbc 驅(qū)動依賴,版本為5.1.41)

public static void main(String[] args)
    {
        Enumeration<Driver> drivers = DriverManager.getDrivers();
        Driver driver;
        while (drivers.hasMoreElements())
        {
            driver = drivers.nextElement();
            System.out.println(driver.getClass() + "------" + driver.getClass().getClassLoader());
        }
        System.out.println(DriverManager.class.getClassLoader());
    }

輸出結(jié)果如下:

class com.mysql.jdbc.Driver------sun.misc.Launcher$AppClassLoader@2a139a55
class com.mysql.fabric.jdbc.FabricMySQLDriver------sun.misc.Launcher$AppClassLoader@2a139a55
null

可以看到代碼中并沒有調(diào)用 Class.forName(“”)的代碼,但DriverManager中已經(jīng)加載了兩個 jdbc 驅(qū)動,而卻這兩個驅(qū)動都是使用的應(yīng)用類加載器(AppClassLoader)加載的,而DriverManager本身的類加載器確是 null 即BootstrapClassLoader,按照雙親委派模型的規(guī)則,委派鏈如下:

SystemApp class loader -> Extension class loader -> Bootstrap class loader
,父加載器BootstrapClassLoader是無法找到AppClassLoader加載的類的,此時使用了線程上下文加載器,Thread.currentThread().setContextClassLoader()可以將委派鏈左邊的類加載器,設(shè)置為線程上下文加載器,此時右邊的加載器就可以使用線程上下文加載器委托子加載器加載類

可以查看DriverManager的源碼

/**
     * Load the initial JDBC drivers by checking the System property
     * jdbc.properties and then use the {@code ServiceLoader} mechanism
     */
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
    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;
        }
sun.misc.Providers()
        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;
            }
        });
        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);
            }
        }
    }

可以看到DriverManager在初始化時會使用ServiceLoader來加載java.sql.Driver的實現(xiàn)類,此處就是 spi 服務(wù)的思想
查看 ServiceLoader 的load 代碼

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();
    }

創(chuàng)建了一個ServiceLoader,使用 reload 方法來加載,ServiceLoader 的主要參數(shù)與 reload 的代碼如下:

private static final String PREFIX = "META-INF/services/";
public void reload() {
        providers.clear();
        lookupIterator = new LazyIterator(service, loader);
    }

LazyIterator是一個懶加載的迭代器,看一下這個迭代器的實現(xiàn):

 private class LazyIterator
        implements Iterator<S>
    {

        Class<S> service;
        ClassLoader loader;
        Enumeration<URL> configs = null;
        Iterator<String> pending = null;
        String nextName = null;

        private LazyIterator(Class<S> service, ClassLoader loader) {
            this.service = service;
            this.loader = loader;
        }

        private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    fail(service, "Error locating configuration files", x);
                }
            }
            while ((pending == null) || !pending.hasNext()) {
                if (!configs.hasMoreElements()) {
                    return false;
                }
                pending = parse(service, configs.nextElement());
            }
            nextName = pending.next();
            return true;
        }

        private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service,
                     "Provider " + cn + " not found");
            }
            if (!service.isAssignableFrom(c)) {
                fail(service,
                     "Provider " + cn  + " not a subtype");
            }
            try {
                S p = service.cast(c.newInstance());
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                fail(service,
                     "Provider " + cn + " could not be instantiated",
                     x);
            }
            throw new Error();          // This cannot happen
        }

        public boolean hasNext() {
            if (acc == null) {
                return hasNextService();
            } else {
                PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
                    public Boolean run() { return hasNextService(); }
                };
                return AccessController.doPrivileged(action, acc);
            }
        }

        public S next() {
            if (acc == null) {
                return nextService();
            } else {
                PrivilegedAction<S> action = new PrivilegedAction<S>() {
                    public S run() { return nextService(); }
                };
                return AccessController.doPrivileged(action, acc);
            }
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }

    }

回頭查看DriverManager的初始化代碼,可以看到如下代碼:

 while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }

可以看出DriverManager會循環(huán)調(diào)用所有在META-INF/services/java.sql.Driver下定義了所有類的 Class.forName()方法
那么這些加載的驅(qū)動是如何被注冊在DriverManager中的?我們看 mysql 的驅(qū)動 Driver 的實現(xiàn)類 可以看到 Driver的實現(xiàn)在初始化時就進行了注冊,代碼如下:

static {
        try {
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }

這段代碼即可將 java.sql.Driver 的實現(xiàn)類注冊進DriverManager,注意此段代碼中 new Driver()是com.mysql.jdbc.Driver

最后查看下實現(xiàn) spi 服務(wù)必不可少的文件 META-INF/services/java.sql.Driver(這個特定用來實現(xiàn) java.sql.Driver 的接口的 spi 服務(wù))這個文件中內(nèi)容如下:

com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver

可以看到這兩個類即為文章開頭實驗的那兩個 jdbc 驅(qū)動

注意并不是所有版本的 jdbc 驅(qū)動都實現(xiàn)了 spi 服務(wù),應(yīng)該是5.1.5及之后的版本才實現(xiàn)了這種服務(wù),之前的版本還是需要手動調(diào)用 Class.forName 方法來加載驅(qū)動,還有好像 ojdbc 的驅(qū)動均沒有實現(xiàn) spi 服務(wù)

搞清楚了 spi 服務(wù)于 DriverManager 加載的過程,我們可以自己嘗試實現(xiàn)一個簡單的 jdbc 驅(qū)動(僅僅實現(xiàn)了類加載的部分)
使用 maven 工程,新建類com.lcy.mysql.Driver

public class Driver implements java.sql.Driver
{

    static
    {
        try
        {
            DriverManager.registerDriver(new com.lcy.mysql.Driver());
        }
        catch (SQLException e)
        {
            throw new RuntimeException("register driver fail");
        }
    }

    @Override
    public Connection connect(String url, Properties info)
        throws SQLException
    {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public boolean acceptsURL(String url)
        throws SQLException
    {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public DriverPropertyInfo[] getPropertyInfo(String url, Properties info)
        throws SQLException
    {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public int getMajorVersion()
    {
        // TODO Auto-generated method stub
        return 0;
    }

    @Override
    public int getMinorVersion()
    {
        // TODO Auto-generated method stub
        return 0;
    }

    @Override
    public boolean jdbcCompliant()
    {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public Logger getParentLogger()
        throws SQLFeatureNotSupportedException
    {
        // TODO Auto-generated method stub
        return null;
    }

}

僅僅寫了一個初始化方法,其他方法均使用默認(rèn)空實現(xiàn),在 src/mian/resources 目錄下新建文件 /META-INF/services/java.sql.Driver 填入內(nèi)容com.lcy.mysql.Driver 打包發(fā)布
在之前的文章開始的測試工程中引入工程依賴(如果是同一工程,直接運行即可),運行可以看到結(jié)果如下:

class com.mysql.jdbc.Driver------sun.misc.Launcher$AppClassLoader@2a139a55
class com.mysql.fabric.jdbc.FabricMySQLDriver------sun.misc.Launcher$AppClassLoader@2a139a55
class com.lcy.mysql.Driver------sun.misc.Launcher$AppClassLoader@2a139a55
null

可以看到,已經(jīng)加載了我們自定義的com.lcy.mysql.Driver(雖然這個加載器沒有實現(xiàn)任何功能,但測試 spi 機制的目的已經(jīng)實現(xiàn))

JDBC驅(qū)動加載機制

說道JDBC我們寫Java的程序員實在是太過熟悉了,如今的后端系統(tǒng)不論大小幾乎都抹不開和數(shù)據(jù)庫存在聯(lián)系镰惦。

JDBC是一個連接數(shù)據(jù)庫的Java API罚渐,包含了相關(guān)的接口和類迅耘。但是税娜,他不提供針對具體數(shù)據(jù)庫(MySQL月洛、MS、Oracle)的實際操作捌议,而只是提供了接口哼拔,以及調(diào)用框架。和具體數(shù)據(jù)庫的直接交互由對應(yīng)的驅(qū)動程序完成瓣颅,比如mysql的mysql-connector倦逐、oracle的ojdbc、MS的sqljdbc等宫补。

jdbc連接過程

1檬姥、加載JDBC驅(qū)動程序:
Class.forName("com.mysql.jdbc.Driver") ;

2、提供JDBC連接的URL

String url = jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8

3守谓、創(chuàng)建數(shù)據(jù)庫的連接

    Connection con =    
             DriverManager.getConnection(url , username , password ) ;   

4穿铆、創(chuàng)建一個Statement

PreparedStatement pstmt = con.prepareStatement(sql) ;   

5、執(zhí)行SQL語句

ResultSet rs = stmt.executeQuery("SELECT * FROM ...") ;   

6斋荞、處理結(jié)果

     while(rs.next()){   
         //do something
     }  

7荞雏、關(guān)閉JDBC對象

Class.forName作用

我們都知道,也聽了無數(shù)遍平酿,驅(qū)動的加載是由Class.forName 方法完成的凤优。

但是,讓我們深究一下蜈彼,Class.forName是JSE里面加載一個類到JVM內(nèi)存的方法筑辨,為什么又會關(guān)聯(lián)了JDBC的驅(qū)動加載邏輯呢?

確實JDBC驅(qū)動的加載是在Class.forName這一步完成的幸逆,但是完成這個工作的是加載的具體的數(shù)據(jù)庫驅(qū)動類的靜態(tài)初始化塊完成的棍辕。

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!");
        }
    }
}

由于JVM對類的加載有一個邏輯是:在類被需要的時候,或者首次調(diào)用的時候就會把類加載到JVM还绘。反過來也就是:如果類沒有被需要的時候楚昭,是不會被加載到JVM的。

當(dāng)連接數(shù)據(jù)庫的時候我們調(diào)用了Class.forName語句之后拍顷,數(shù)據(jù)庫驅(qū)動類被加載到JVM抚太,那么靜態(tài)初始化塊就會被執(zhí)行,從而完成驅(qū)動的注冊工作昔案,也就是注冊到了JDBC的DriverManager類中尿贫。

由于是靜態(tài)初始化塊中完成的加載,所以也就不必?fù)?dān)心驅(qū)動被加載多次

拋棄Class.forName

在JDBC 4.0之后實際上我們不需要再調(diào)用Class.forName來加載驅(qū)動程序了踏揣,我們只需要把驅(qū)動的jar包放到工程的類加載路徑里庆亡,那么驅(qū)動就會被自動加載。

這個自動加載采用的技術(shù)叫做SPI捞稿,數(shù)據(jù)庫驅(qū)動廠商也都做了更新身冀《凼可以看一下jar包里面的META-INF/services目錄,里面有一個java.sql.Driver的文件搂根,文件里面包含了驅(qū)動的全路徑名。

比如mysql-connector里面的內(nèi)容:

com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver

那么SPI技術(shù)又是在什么階段加載的數(shù)據(jù)庫驅(qū)動呢铃辖?看一下JDBC的DriverManager類就知道了剩愧。

public class DriverManager {
    static {
        loadInitialDrivers();//......1
        println("JDBC DriverManager initialized");
    }

    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);//.....2
                   Iterator driversIterator = loadedDrivers.iterator();

                //.....
}

上述代碼片段標(biāo)記…1的位置是在DriverManager類加載是執(zhí)行的靜態(tài)初始化塊,這里會調(diào)用loadInitialDrivers方法娇斩。

再看loadInitialDrivers方法里面標(biāo)記…2的位置仁卷,這里調(diào)用的 ServiceLoader.load(Driver.class); 就會加載所有在META-INF/services/java.sql.Driver文件里邊的類到JVM內(nèi)存,完成驅(qū)動的自動加載犬第。

這就是SPI的優(yōu)勢所在锦积,能夠自動的加載類到JVM內(nèi)存。這個技術(shù)在阿里的dubbo框架里面也占到了很大的分量歉嗓。

JDBC如何區(qū)分多個驅(qū)動丰介?

一個項目里邊很可能會即連接MySQL,又連接Oracle鉴分,這樣在一個工程里邊就存在了多個驅(qū)動類哮幢,那么這些驅(qū)動類又是怎么區(qū)分的呢?

關(guān)鍵點就在于getConnection的步驟志珍,DriverManager.getConnection中會遍歷所有已經(jīng)加載的驅(qū)動實例去創(chuàng)建連接橙垢,當(dāng)一個驅(qū)動創(chuàng)建連接成功時就會返回這個連接,同時不再調(diào)用其他的驅(qū)動實例伦糯。DriverManager關(guān)鍵代碼如下:

private static Connection getConnection(
    //.....

    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) {
                    // Success!
                    println("getConnection returning " + aDriver.driver.getClass().getName());
                        return (con);
                }
            } catch (SQLException ex) {
                if (reason == null) {
                      reason = ex;
                }
            }
        } else {
            println("    skipping: " + aDriver.getClass().getName());
        }
     }

    //......

是不是每個驅(qū)動實例都真真實實的要嘗試建立連接呢柜某?不是的!

每個驅(qū)動實例在getConnetion的第一步就是按照url判斷是不是符合自己的處理規(guī)則敛纲,是的話才會和db建立連接喂击。比如,MySQL驅(qū)動類中的關(guān)鍵代碼:

    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)) { //$NON-NLS-1$

            return null;
        }
        //......
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末载慈,一起剝皮案震驚了整個濱河市惭等,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌办铡,老刑警劉巖辞做,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異寡具,居然都是意外死亡秤茅,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進店門童叠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來框喳,“玉大人课幕,你說我怎么就攤上這事∥蹇澹” “怎么了乍惊?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長放仗。 經(jīng)常有香客問我润绎,道長,這世上最難降的妖魔是什么诞挨? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任莉撇,我火速辦了婚禮,結(jié)果婚禮上惶傻,老公的妹妹穿的比我還像新娘棍郎。我一直安慰自己,他們只是感情好银室,可當(dāng)我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布涂佃。 她就那樣靜靜地躺著,像睡著了一般粮揉。 火紅的嫁衣襯著肌膚如雪巡李。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天扶认,我揣著相機與錄音侨拦,去河邊找鬼。 笑死辐宾,一個胖子當(dāng)著我的面吹牛狱从,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播叠纹,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼季研,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了誉察?” 一聲冷哼從身側(cè)響起与涡,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎持偏,沒想到半個月后驼卖,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡鸿秆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年酌畜,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片卿叽。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡桥胞,死狀恐怖恳守,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情贩虾,我是刑警寧澤催烘,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站缎罢,受9級特大地震影響颗圣,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜屁使,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望奔则。 院中可真熱鬧蛮寂,春花似錦、人聲如沸易茬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽抽莱。三九已至范抓,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間食铐,已是汗流浹背匕垫。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留虐呻,地道東北人象泵。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像斟叼,于是被迫代替她去往敵國和親偶惠。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,925評論 2 344