淺談SPI機制

淺談SPI機制

前言

這段時間在研究一個開源框架胀蛮,發(fā)現(xiàn)其中有一些以SPI命名的包,經(jīng)過搜索渐白、整理以及思考之后尊浓,將學(xué)習(xí)的筆記、心得整理出來纯衍,供日后復(fù)習(xí)使用栋齿。

SPI

SPI全稱是Service Provider Interface,翻譯過來是服務(wù)提供者接口襟诸,這個翻譯其實不那么形象瓦堵,理解起來也不是很好理解,至少不那么見名知意励堡。

其實SPI是一種機制,一種類似于服務(wù)發(fā)現(xiàn)的機制堡掏,什么叫做服務(wù)發(fā)現(xiàn)呢应结,就是能夠根據(jù)情況發(fā)現(xiàn)已有服務(wù)的機制,好像說了跟沒說一樣,對吧鹅龄,下面我們逐個來理解揩慕。

首先是服務(wù),英文叫做Service扮休,服務(wù)可以理解為就是某一種或者某幾種功能迎卤,比如日常生活中的醫(yī)生,提供看病的服務(wù)玷坠;家政公司蜗搔,提供家政服務(wù);房產(chǎn)中介公司八堡,提供樟凄,這樣子的話,關(guān)于服務(wù)兄渺,應(yīng)該是理清楚了缝龄。

接下來是服務(wù)的發(fā)現(xiàn),英文是Service Discovery挂谍,理解了服務(wù)叔壤,那么服務(wù)的發(fā)現(xiàn)就應(yīng)該很好理解了,用大白話講就是具有某種能力口叙,可以發(fā)現(xiàn)某些服務(wù)炼绘,比如生活中的房產(chǎn)中介公司(服務(wù)發(fā)現(xiàn)),他們就能夠發(fā)現(xiàn)很多的擁有空閑房子并且愿意出租的人(服務(wù))庐扫。

SPI機制的作用就是服務(wù)發(fā)現(xiàn)饭望,也就是說,我們有一些服務(wù)形庭,然后通過SPI機制铅辞,就能讓這些服務(wù)被需要的人所使用,而我們這些服務(wù)被發(fā)現(xiàn)的過程就是SPI的任務(wù)了萨醒。

說到這里斟珊,可能你還是不太理解SPI是什么,接下來我們通過具體的例子分析來理解SPI富纸。

在JDBC4.0之前囤踩,我們使用JDBC去連接數(shù)據(jù)庫的時候,通常會經(jīng)過如下的步驟

  1. 將對應(yīng)數(shù)據(jù)庫的驅(qū)動加到類路徑中
  2. 通過Class.forName()注冊所要使用的驅(qū)動晓褪,如Class.forName(com.mysql.jdbc.Driver)
  3. 使用驅(qū)動管理器DriverManager來獲取連接
  4. 后面的內(nèi)容我們不關(guān)心了堵漱。

這種方式有個缺點,加載驅(qū)動是由用戶來操作的涣仿,這樣就很容易出現(xiàn)加載錯驅(qū)動或者更換驅(qū)動的時候勤庐,忘記更改加載的類了示惊。

在JDBC4.0,現(xiàn)在我們使用的時候愉镰,上面的第二步就不需要了米罚,并且能夠正常使用,這個就是SPI的功勞了丈探。

接下來我們先來看下為什么不需要第二步录择。

熟悉反射的同學(xué)應(yīng)該知道,第二步其實就是將對應(yīng)的驅(qū)動類加載到虛擬機中碗降,也就是說隘竭,現(xiàn)在我們沒有手動加載,那么對應(yīng)的驅(qū)動類是如何加載到虛擬機中的呢遗锣,我們通過DriverManger的源碼的了解SPI是如何實現(xiàn)這個功能的货裹。

DriverManager.java

在DriverManager中,有一段靜態(tài)代碼(靜態(tài)代碼在類被加載的時候就會執(zhí)行)

static {
    // 在這里加載對應(yīng)的驅(qū)動類
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

接下來我們來具體看下其內(nèi)容

loadInitialDrivers()

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

    // SPI機制加載驅(qū)動類
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {
            
            //  通過ServiceLoader.load進行查找精偿,我們的重點也是這里弧圆,后面分析
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            // 獲取迭代器,也請注意這里
            Iterator<Driver> driversIterator = loadedDrivers.iterator();

            try{
                // 遍歷迭代器
                // 這里需要這么做笔咽,是因為ServiceLoader默認(rèn)是延遲加載
                // 只是找到對應(yīng)的class搔预,但是不加載
                // 所以這里在調(diào)用next的時候,其實就是實例化了對應(yīng)的對象了
                // 請注意這里 --------------------------------------------------------------------  1
                while(driversIterator.hasNext()) {
                    // 真正實例化的邏輯叶组,詳見后面分析
                    driversIterator.next();
                }
            } catch(Throwable t) {
            // Do nothing
            }
            return null;
        }
    });

    println("DriverManager.initialize: jdbc.drivers = " + drivers);

    if (drivers == null || drivers.equals("")) {
        return;
    }
    // 同時加載系統(tǒng)變量中找到的驅(qū)動類
    String[] driversList = drivers.split(":");
    println("number of Drivers:" + driversList.length);
    for (String aDriver : driversList) {
        try {
            println("DriverManager.Initialize: loading " + aDriver);
            // 由于是系統(tǒng)變量拯田,所以使用系統(tǒng)類加載器,而不是應(yīng)用類加載器
            Class.forName(aDriver, true,
                    ClassLoader.getSystemClassLoader());
        } catch (Exception ex) {
            println("DriverManager.Initialize: load failed: " + ex);
        }
    }
}

從上面的代碼中并沒有找到對應(yīng)的操作邏輯甩十,唯一的一個突破點就是ServiceLoader.load(Driver.class)方法船庇,該方法其實就是SPI的核心啦

接下來我們來分析這個類的代碼(代碼可能有點長哦,要有心理準(zhǔn)備)

ServiceLoader.java


public final class ServiceLoader<S>
    implements Iterable<S>
{
    /**
    *  由于是調(diào)用ServiceLoader.load(Driver.class)方法侣监,所以我們先從該方法分析
    */ 
    public static <S> ServiceLoader<S> load(Class<S> service) {
        // 獲取當(dāng)前的上下文線程
        // 默認(rèn)情況下是應(yīng)用類加載器鸭轮,具體的內(nèi)容稍后分析
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        // 調(diào)用帶加載器的加載方法
        return ServiceLoader.load(service, cl);
    }

    /**
    *  帶類加載器的加載方法
    */
    public static <S> ServiceLoader<S> load(Class<S> service,
                                            ClassLoader loader)
    {
        // 只是返回一哥ServiceLoader對象,調(diào)用自己的構(gòu)造函數(shù)嘛
        return new ServiceLoader<>(service, loader);
    }

    /**
    *  私有構(gòu)造函數(shù)
    */
    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        // 目標(biāo)加載類不能為null
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        // 獲取類加載器橄霉,如果cl是null窃爷,則使用系統(tǒng)類加載器
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        // 調(diào)用reload方法
        reload();
    }

    // 用于緩存加載的服務(wù)提供者
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // 真正查找邏輯的實現(xiàn)
    private LazyIterator lookupIterator;

    /**
    *  reload方法
    */
    public void reload() {
        // 先清空內(nèi)容
        providers.clear();
        // 初始化lookupIterator
        lookupIterator = new LazyIterator(service, loader);
    }
}

LazyIterator.class

LazyIterator是ServiceLoader的私有內(nèi)部類

private class LazyIterator
        implements Iterator<S>
{

    Class<S> service;
    ClassLoader loader;

    /**
    *  私有構(gòu)造函數(shù),用于初始化參數(shù)
    */
    private LazyIterator(Class<S> service, ClassLoader loader) {
        this.service = service;
        this.loader = loader;
    }
}

到了上面的內(nèi)容姓蜂,其實ServiceLoader.load()方法就結(jié)束了按厘,并沒有實際上去查找具體的實現(xiàn)類,那么什么時候才去查找以及加載呢钱慢,還記得上面的Iterator<Driver> driversIterator = loadedDrivers.iterator();這一行代碼嗎逮京,這一行代碼用于獲取一個迭代器,這里同樣也沒有進行加載束莫,但是懒棉,其后面還有遍歷迭代器的代碼御吞,上面標(biāo)注為1的部分。

迭代器以及遍歷迭代器的過程如下所示

ServiceLoader.java

public Iterator<S> iterator() {
    return new Iterator<S>() {

        // 注意這里的providers漓藕,這里就是上面提到的用于緩存
        // 已經(jīng)加載的服務(wù)提供者的容器。
        Iterator<Map.Entry<String,S>> knownProviders
            = providers.entrySet().iterator();

        // 底層其實委托給了providers
        public boolean hasNext() {
            if (knownProviders.hasNext())
                return true;
            // 如果沒有緩存挟裂,則查找及加載
            return lookupIterator.hasNext();
        }

        // 同上
        public S next() {
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
            return lookupIterator.next();
        }

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

上面已經(jīng)分析過了享钞,ServiceLoader.load()方法執(zhí)行到LazyIterator的初始化之后就結(jié)束了,真正地查找直到調(diào)用lookupIterator.hasNext()才開始诀蓉。

LazyIterator.java

// 希望你還記得他
private class LazyIterator
        implements Iterator<S>
{

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

    //檢查 AccessControlContext栗竖,這個我們不關(guān)系
    // 關(guān)鍵的核心是都調(diào)用了hasNextService()方法
    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);
        }
    }

    private boolean hasNextService() {
        // 第一次加載
        if (nextName != null) {
            return true;
        }
        // 第一次加載
        if (configs == null) {
            try {
                // 注意這里,獲取了的完整名稱
                // PREFIX定義在ServiceLoader中
                // private static final String PREFIX = "META-INF/services/"
                // 這里可以看到渠啤,完整的類名稱就是 META-INF/services/CLASS_FULL_NAME
                // 比如這里的 Driver.class狐肢,完整的路徑就是
                //                  META-INF/services/java.sql.Driver,注意這個只是文件名沥曹,不是具體的類哈
                String fullName = PREFIX + service.getName();
                // 如果類加載器為null份名,則使用系統(tǒng)類加載器進行加載
                // 類加載會加載指定路徑下的所有類
                if (loader == null)
                    configs = ClassLoader.getSystemResources(fullName);
                else // 使用傳入的類加載器進行加載,其實就是應(yīng)用類加載器
                    configs = loader.getResources(fullName);
            } catch (IOException x) {
                fail(service, "Error locating configuration files", x);
            }
        }
        // 如果pending為null或者沒有內(nèi)容妓美,則進行加載僵腺,一次只加載一個文件的一行
        while ((pending == null) || !pending.hasNext()) {
            if (!configs.hasMoreElements()) {
                return false;
            }
            // 解析讀取到的每個文件,高潮來了
            pending = parse(service, configs.nextElement());
        }
        nextName = pending.next();
        return true;
    }

    /**
    *  解析讀取到的每個文件
    */
    private Iterator<String> parse(Class<?> service, URL u)
        throws ServiceConfigurationError
    {
        InputStream in = null;
        BufferedReader r = null;
        ArrayList<String> names = new ArrayList<>();
        try {
            in = u.openStream();
            // utf-8編碼
            r = new BufferedReader(new InputStreamReader(in, "utf-8"));
            int lc = 1;
            // 一行一行地讀取數(shù)據(jù)
            while ((lc = parseLine(service, u, r, lc, names)) >= 0);
        } catch (IOException x) {
            fail(service, "Error reading configuration file", x);
        } finally {
            try {
                if (r != null) r.close();
                if (in != null) in.close();
            } catch (IOException y) {
                fail(service, "Error closing configuration file", y);
            }
        }
        // 返回迭代器
        return names.iterator();
    }

    // 解析一行行的數(shù)據(jù)
    private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
                          List<String> names)
        throws IOException, ServiceConfigurationError
    {
        String ln = r.readLine();
        if (ln == null) {
            return -1;
        }
        // 查找是否存在#
        // 如果存在壶栋,則剪取#前面的內(nèi)容
        // 目的是防止讀取到#及后面的內(nèi)容
        int ci = ln.indexOf('#');
        if (ci >= 0) ln = ln.substring(0, ci);
        ln = ln.trim();
        int n = ln.length();
        if (n != 0) {
            // 不能包含空格及制表符\t
            if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
                fail(service, u, lc, "Illegal configuration-file syntax");
            int cp = ln.codePointAt(0);
            // 檢查第一個字符是否是Java語法規(guī)范的單詞
            if (!Character.isJavaIdentifierStart(cp))
                fail(service, u, lc, "Illegal provider-class name: " + ln);
            // 檢查每個字符
            for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
                cp = ln.codePointAt(i);
                if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
                    fail(service, u, lc, "Illegal provider-class name: " + ln);
            }
            // 如果緩存中沒有辰如,并且當(dāng)前列表中也沒有,則加入列表贵试。
            if (!providers.containsKey(ln) && !names.contains(ln))
                names.add(ln);
        }
        return lc + 1;
    }

    /**
    *  上面解析完文件之后琉兜,就開始加載文件的內(nèi)容了
    */
    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 {
            // 實例化并且將其轉(zhuǎn)化為對應(yīng)的接口或者父類
            S p = service.cast(c.newInstance());
            // 將其放入緩存中
            providers.put(cn, p);
            // 返回當(dāng)前實例
            return p;
        } catch (Throwable x) {
            fail(service,
                    "Provider " + cn + " could not be instantiated",
                    x);
        }
        throw new Error();          // This cannot happen
    }
}

到此,解析的步驟就完成了毙玻,在一開始的DriverManager中豌蟋,我們也看到了在DriveirManager中一直在調(diào)用next方法,也就是持續(xù)地加載找到的所有的Driver的實現(xiàn)類了淆珊,比如MySQL的驅(qū)動類夺饲,Oracle的驅(qū)動類啦。

這個例子有點長施符,但我們收獲還是很多往声,我們知道了JDBC4不用手動加載驅(qū)動類的實現(xiàn)原理,其實就是通過ServiceLoader去查找當(dāng)前類加載器能訪問到的目錄下的WEB-INF/services/FULL_CLASS_NAME文件中的所有內(nèi)容戳吝,而這些內(nèi)容由一定的規(guī)范浩销,如下

  • 每行只能寫一個全類名
  • #作為注釋
  • 只能使用utf-8及其兼容的編碼
  • 每個實現(xiàn)類必須提供一個無參構(gòu)造函數(shù),因為是直接使用class.newInstance()來創(chuàng)建實例的嘛

由此我們也明白了SPI機制的工作原理听哭,那么這個東西有什么用呢慢洋,其實JDBC就是個最好的例子啦塘雳,這樣用戶就不需要知道到底是要加載哪個實現(xiàn)類腔寡,一方面是簡化了操作跃巡,另一方面避免了操作的錯誤,當(dāng)然野宜,這種一般是用于寫框架之類的用途太防,用于向框架使用者提供更加便利的操作妻顶,比如上面的引導(dǎo)我看到SPI的例子,其實是來自一個RPC框架蜒车,通過SPI機制讳嘱,讓我們可以直接編寫自定義的序列化方式,然后由框架來負(fù)責(zé)加載即可酿愧。

SPI實戰(zhàn)小案例

上面學(xué)習(xí)完了SPI的例子沥潭,也學(xué)習(xí)完了JDBC是如何實現(xiàn)的,接下來我們來通過一個小案例嬉挡,來動手實踐一下SPI是如何工作的钝鸽。

新建一個接口,內(nèi)容隨便啦

HelloServie.java

public interface HelloService {
    void sayHello();
}

然后編寫其實現(xiàn)類

HelloServiceImpl.java

public class HelloServiceImpl implements HelloService {
    @Override
    public void sayHello() {
        System.out.println("hello world");
    }
}

關(guān)鍵點來了庞钢,既然是學(xué)習(xí)SPI寞埠,那么我們肯定不是手動new一個實現(xiàn)類啦,而是通過SPI的機制來加載焊夸,如果認(rèn)真地看完上面的分析仁连,那么下面的內(nèi)容應(yīng)該很容易看懂啦,如果沒看懂阱穗,再回去看一下啦饭冬。

  1. 在實現(xiàn)類所在項目(這里是同個項目哈)的類路徑下,如果是maven項目揪阶,則是在resources目錄下

    1. 建立目錄META-INF/services
    2. 建立文件cn.xuhuanfeng.spi.HelloService(接口的全限定名哈)
  2. 內(nèi)容是實現(xiàn)類的類名:cn.xuhuanfeng.spi.impl.HelloServiceImpl(注意這里我們直接放在同個項目昌抠,不是同個項目也可以的!B沉拧炊苫!)

  3. 自定義一個加載的類,并且通過ServiceLoader.load()方法進行加載冰沙,如下所示

    public class HelloServiceFactory {
    
        public HelloService getHelloService() {
            ServiceLoader<HelloService> load = ServiceLoader.load(HelloService.class);
            return load.iterator().next();
        }
    }
    
  4. 測試一下侨艾,enjoy :)

  5. 如果你有興趣的話,可以嘗試將實現(xiàn)放在另一個項目中拓挥,然后打包成jar包唠梨,再放置在測試項目的classpath中,enjoy :)

總結(jié)

本小節(jié)我們主要學(xué)習(xí)了SPI侥啤,主要包括了SPI是什么当叭,JDBC4中不需要手動加載驅(qū)動類的原理茬故,并且詳細(xì)看了DriverManager中的代碼實現(xiàn),最后蚁鳖,通過一個簡單的小案例來實現(xiàn)我們自己的SPI服務(wù)磺芭,通過這個小節(jié),應(yīng)該說醉箕,SPI的大部分內(nèi)容我們是掌握了徘跪,當(dāng)然,里面管理類加載器部分我們還沒有學(xué)習(xí)琅攘,這里先挖個坑,后面有時間再分析一下松邪。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末坞琴,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子逗抑,更是在濱河造成了極大的恐慌剧辐,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件邮府,死亡現(xiàn)場離奇詭異荧关,居然都是意外死亡,警方通過查閱死者的電腦和手機褂傀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門忍啤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人仙辟,你說我怎么就攤上這事同波。” “怎么了叠国?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵未檩,是天一觀的道長。 經(jīng)常有香客問我粟焊,道長冤狡,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任项棠,我火速辦了婚禮悲雳,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘香追。我一直安慰自己怜奖,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布翅阵。 她就那樣靜靜地躺著歪玲,像睡著了一般迁央。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上滥崩,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天岖圈,我揣著相機與錄音,去河邊找鬼钙皮。 笑死蜂科,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的短条。 我是一名探鬼主播导匣,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼茸时!你這毒婦竟也來了贡定?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤可都,失蹤者是張志新(化名)和其女友劉穎缓待,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體渠牲,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡旋炒,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了签杈。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瘫镇。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖答姥,靈堂內(nèi)的尸體忽然破棺而出汇四,到底是詐尸還是另有隱情,我是刑警寧澤踢涌,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布通孽,位于F島的核電站,受9級特大地震影響睁壁,放射性物質(zhì)發(fā)生泄漏背苦。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一潘明、第九天 我趴在偏房一處隱蔽的房頂上張望行剂。 院中可真熱鬧,春花似錦钳降、人聲如沸厚宰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽铲觉。三九已至澈蝙,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間撵幽,已是汗流浹背灯荧。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留盐杂,地道東北人逗载。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像链烈,于是被迫代替她去往敵國和親厉斟。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,722評論 2 345

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

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理强衡,服務(wù)發(fā)現(xiàn)擦秽,斷路器,智...
    卡卡羅2017閱讀 134,599評論 18 139
  • 1.介紹 熟悉JDBC的同學(xué)都知道食侮,在jdbc4.0之前,在使用DriverManager獲取DB連接之前目胡,我們總...
    近路閱讀 6,272評論 2 3
  • SPI簡介 如何使用SPI 應(yīng)用舉例1. 組織方制定接口2. 實現(xiàn)方根據(jù)SPI規(guī)范實現(xiàn)接口3. 組織方加載實現(xiàn)類 ...
    齊晉閱讀 896評論 0 5
  • 本文以JDBC為例深入講解 java spi 機制锯七,將幫助你理解:什么是SPI,SPI實現(xiàn)原理誉己,SPI的使用和SP...
    匠丶閱讀 5,221評論 0 8
  • 此刻眉尸,一杯十年陳的即墨老酒,一盤香痱子巨双,一行標(biāo)題噪猾,根據(jù)要求,我得寫一寫2018的新目標(biāo). 在酒的作用下筑累,內(nèi)心狂野袱蜡;...
    113bd6fe137e閱讀 261評論 4 1