Dubbo的SPI之Filter實(shí)現(xiàn)

前面我們了解過了Java的SPI擴(kuò)展機(jī)制吱型,對(duì)于Java擴(kuò)展機(jī)制的原理以及優(yōu)缺點(diǎn)也有了大概的了解帐萎,這里繼續(xù)深入一下Dubbo的擴(kuò)展點(diǎn)加載機(jī)制跟狱。玩過Dubbo框架的同學(xué)都知道扎运,Dubbo框架最強(qiáng)大的地方就是他的SPI機(jī)制脖母,可以滿足使用者天馬行空的擴(kuò)展性需求士鸥。
本文主要討論2點(diǎn):Dubbo的spi機(jī)制實(shí)現(xiàn)原理;基于SPI思想的Filter實(shí)現(xiàn)谆级。

Dubbo的spi機(jī)制實(shí)現(xiàn)原理

這里以Protocol 協(xié)議接口來講解烤礁,先上一張圖來幫助理解:


ExtensionLoader加載過程.jpg
Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

ExtensionLoader類的實(shí)現(xiàn)思想?yún)⒖剂薐DK中的ServiceLoader類,也是用來加載指定路徑下的接口實(shí)現(xiàn)肥照,具體實(shí)現(xiàn)細(xì)節(jié)比JDK的復(fù)雜了很多脚仔。
首先看ExtensionLoader的靜態(tài)方法getExtensionLoader。

public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
        if (type == null)
            throw new IllegalArgumentException("Extension type == null");
        if(!type.isInterface()) {
            throw new IllegalArgumentException("Extension type(" + type + ") is not interface!");
        }
        if(!withExtensionAnnotation(type)) {
            throw new IllegalArgumentException("Extension type(" + type + 
                    ") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
        }
        //根據(jù)接口對(duì)象取ExtensionLoader類
        ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        if (loader == null) {
            //如果為空保存接口類對(duì)應(yīng)的 新建的ExtensionLoader對(duì)象
            EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
            loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        }
        return loader;
    }

有4個(gè)點(diǎn)需要注意:
1.EXTENSION_LOADERS這個(gè)Map中以接口為key,以ExtensionLoader對(duì)象為value舆绎。
2.判斷Map中根據(jù)接口get對(duì)象鲤脏,如果沒有就new個(gè)ExtensionLoader對(duì)象保存進(jìn)去。并返回該ExtensionLoader對(duì)象。
3.注意創(chuàng)建ExtensionLoader對(duì)象的構(gòu)造函數(shù)代碼猎醇,將傳入的接口type屬性賦值給了ExtensionLoader類的type屬性
4.創(chuàng)建ExtensionFactory objectFactory對(duì)象

@SPI("dubbo")
public interface Protocol {

      int getDefaultPort();

      @Adaptive
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;

     @Adaptive
    <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;

    void destroy();

}

ExtensionLoader使用loadExtensionClasses方法讀取擴(kuò)展點(diǎn)中的實(shí)現(xiàn)類

loadExtensionClasses先讀取SPI注解的value值窥突,如果value有值,就把這個(gè)值作為默認(rèn)擴(kuò)展實(shí)現(xiàn)的key硫嘶。然后再以此讀取META-INF/dubbo/internal/阻问,META-INF/dubbo/,META-INF/services/下對(duì)應(yīng)的文件沦疾。

  loadFile逐行讀取com.alibaba.dubbo.rpc.Protocol文件中的內(nèi)容称近,每行內(nèi)容以key/value形式存儲(chǔ)。先判斷實(shí)現(xiàn)類上是否打上了@Adaptive注解曹鸠,如果打上了該注解煌茬,將此類作為Protocol協(xié)議的設(shè)配類緩存起來,讀取下一行彻桃。如果實(shí)現(xiàn)類上沒有打上@Adaptive注解坛善,判斷實(shí)現(xiàn)類是否存在參數(shù)為該接口的構(gòu)造器,有的話作為包裝類存儲(chǔ)在該ExtensionLoader的Set<Class<?>> cachedWrapperClasses;集合中邻眷,這里用到了裝飾器模式眠屎。如果該類既不是設(shè)配類,也不是wrapper對(duì)象肆饶,那就是擴(kuò)展點(diǎn)的具體實(shí)現(xiàn)對(duì)象改衩,查找實(shí)現(xiàn)類上是否打了@Activate注解,有緩存到變量cachedActivates的map中將實(shí)現(xiàn)類緩存到cachedClasses中驯镊,以便于使用時(shí)獲取葫督。如ProtocolFilterWrapper的實(shí)現(xiàn)如下:
public class ProtocolFilterWrapper implements Protocol {

    private final Protocol protocol;

    public ProtocolFilterWrapper(Protocol protocol) {
        if (protocol == null) {
            throw new IllegalArgumentException("protocol == null");
        }
        this.protocol = protocol;
    }
  ..........      
}

獲取或則創(chuàng)建設(shè)配對(duì)象getAdaptiveExtension

如果cachedAdaptiveClass有值,說明有且僅有一個(gè)實(shí)現(xiàn)類打了@Adaptive, 實(shí)例化這個(gè)對(duì)象返回板惑。如果cachedAdaptiveClass為空橄镜, 創(chuàng)建設(shè)配類字節(jié)碼。

為什么要?jiǎng)?chuàng)建設(shè)配類冯乘,一個(gè)接口多種實(shí)現(xiàn)洽胶,SPI機(jī)制也是如此,這是策略模式裆馒,但是我們?cè)诖a執(zhí)行過程中選擇哪種具體的策略呢姊氓。Dubbo采用統(tǒng)一數(shù)據(jù)模式com.alibaba.dubbo.common.URL(它是dubbo定義的數(shù)據(jù)模型不是jdk的類),它會(huì)穿插于系統(tǒng)的整個(gè)執(zhí)行過程喷好,URL中定義的協(xié)議類型字段protocol翔横,會(huì)根據(jù)具體業(yè)務(wù)設(shè)置不同的協(xié)議。url.getProtocol()值可以是dubbo也是可以webservice梗搅, 可以是zookeeper也可以是redis棕孙。

設(shè)配類的作用是根據(jù)url.getProtocol()的值extName,去ExtensionLoader. getExtension( extName)選取具體的擴(kuò)展點(diǎn)實(shí)現(xiàn)。

有上述的分析可知蟀俊,能夠使用javasist生成設(shè)配類的條件:

1)接口方法中必須至少有一個(gè)方法打上了@Adaptive注解

2)打上了@Adaptive注解的方法參數(shù)必須有URL類型參數(shù)或者有參數(shù)中存在getURL()方法
仔細(xì)看看Protocol接口代理的具體實(shí)現(xiàn)钦铺,在使用接口代理中的方法時(shí),都會(huì)根據(jù)URL來確定接口的具體實(shí)現(xiàn)肢预,因?yàn)閁RL中攜帶了用戶大部分的參數(shù)配置矛洞,根據(jù)里面的屬性來獲取。里面關(guān)鍵代碼:

com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);

看到這里思路應(yīng)該比較清晰了烫映!所有的接口代理中沼本,并沒有給定具體的實(shí)現(xiàn),全部根據(jù)用戶的參數(shù)配置來動(dòng)態(tài)創(chuàng)建接口的具體實(shí)現(xiàn)锭沟。這樣做讓程序非常的靈活抽兆,讓接口的實(shí)現(xiàn)插拔更加方便。如果想增加一個(gè)接口的實(shí)現(xiàn)族淮,只需要按照SPI的配置方式增加配置文件辫红,xml標(biāo)簽配置指定新接口實(shí)現(xiàn)的標(biāo)記即可。

基于SPI思想的Filter實(shí)現(xiàn)

在微服務(wù)場(chǎng)景下祝辣,一次調(diào)用過程常常會(huì)涉及多個(gè)應(yīng)用贴妻,在定位問題時(shí),往往需要在多個(gè)應(yīng)用中查看某一次調(diào)用鏈路上的日志蝙斜,為了達(dá)到這個(gè)目的名惩,一種常見的做法是在調(diào)用入口處生成一個(gè)traceId,并基于RpcContext來實(shí)現(xiàn)traceId的透?jìng)髟熊O旅鎭砜匆幌略趺赐ㄟ^filter實(shí)現(xiàn)traceId的跟蹤記錄娩鹉。
1.創(chuàng)建Dubbo框架的api項(xiàng)目,創(chuàng)建類FilterTest

package com.enjoy.filter;

import com.alibaba.dubbo.common.Constants;
import com.alibaba.dubbo.common.extension.Activate;
import com.alibaba.dubbo.rpc.*;

@Activate(group = Constants.CONSUMER)
public class FilterSpi implements Filter{


    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
      String traceId = String.valueOf(System.currentTimeMillis());
        RpcContext.getContext().setAttachment("tracdId",traceId);
        System.out.println("traceId:"+traceId);

        Result result = invoker.invoke(invocation);
        return result;
    }
}

項(xiàng)目結(jié)構(gòu)圖為


api項(xiàng)目結(jié)構(gòu).JPG

2.創(chuàng)建server項(xiàng)目稚伍,提供服務(wù),創(chuàng)建訂單類

package com.enjoy.service.impl;

import com.enjoy.dao.OrderDao;
import com.enjoy.entity.OrderEntiry;
import com.enjoy.service.OrderService;
import com.enjoy.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;

public class OrderServiceImpl implements OrderService {

    @Autowired
    private OrderDao orderDao;
    @Autowired
    private ProductService productService;


    @Override
    public OrderEntiry getDetail(String id) {
        OrderEntiry orderEntiry =  orderDao.getDetail(id);
        orderEntiry.addProduct(productService.getDetail("P001"));
        orderEntiry.addProduct(productService.getDetail("P002"));
        System.out.println(super.getClass().getName()+"被調(diào)用一次:"+System.currentTimeMillis());
        return orderEntiry;
    }

    @Override
    public OrderEntiry submit(OrderEntiry order) {
        try {
            Thread.currentThread().sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (1==order.getStatus()){
            System.out.println("警告:訂單重復(fù)提交弯予!");
            throw new RuntimeException("訂單重復(fù)提交!");
        }
        System.out.println(super.getClass().getName()+"被調(diào)用一次:"+System.currentTimeMillis());
        return orderDao.submit(order);
    }

    @Override
    public String cancel(OrderEntiry order) {
        try {
            Thread.currentThread().sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(super.getClass().getName()+"被調(diào)用一次:"+System.currentTimeMillis());
        return orderDao.cancel(order);
    }
}

配置dubbo.xm文件

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
     http://www.springframework.org/schema/beans/spring-beans.xsd
          http://www.springframework.org/schema/context
                        http://www.springframework.org/schema/context/spring-context-3.1.xsd
     http://code.alibabatech.com/schema/dubbo
     http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

    <context:component-scan base-package="com.enjoy"/>

    <!-- 提供方應(yīng)用信息槐瑞,用于計(jì)算依賴關(guān)系 -->
    <dubbo:application name="storeServer"/>

    <!-- 使用zookeeper注冊(cè)中心暴露服務(wù)地址 -->
    <dubbo:registry address="zookeeper://10.xxx.xxx.xxx:2181"/>

    <!--用dubbo協(xié)議在20880端口暴露服務(wù) -->
    <dubbo:protocol name="dubbo" port="20880"/>

    <dubbo:consumer check="false" />

    <!-- 聲明需要暴露的服務(wù)接口 -->
    <dubbo:service interface="com.enjoy.service.OrderService" ref="orderService" protocol="dubbo" />
    <dubbo:service interface="com.enjoy.service.PayService" ref="payService" protocol="dubbo" />
    <dubbo:service interface="com.enjoy.service.OtherService" ref="otherService" protocol="dubbo" />
    <dubbo:service interface="com.enjoy.service.ProductService" ref="productService" protocol="dubbo"/>
    <dubbo:service interface="com.enjoy.service.UserService" ref="userService" />

    <!-- 聲明需要引用的服務(wù)接口 -->

    <!--和本地bean一樣實(shí)現(xiàn)服務(wù) -->
    <bean id="orderService" class="com.enjoy.service.impl.OrderServiceImpl"/>
    <bean id="payService" class="com.enjoy.service.impl.PayServiceImpl"/>
    <bean id="otherService" class="com.enjoy.service.impl.OtherServiceImpl"/>
    <bean id="productService" class="com.enjoy.service.impl.ProductServiceImpl"/>
    <bean id="userService" class="com.enjoy.service.impl.UserServiceImpl"/>

</beans>
server項(xiàng)目結(jié)構(gòu).JPG

3.創(chuàng)建消費(fèi)端

package com.enjoy.controller;

import com.alibaba.dubbo.rpc.RpcContext;
import com.enjoy.entity.OrderEntiry;
import com.enjoy.service.OrderService;
import com.enjoy.service.PayService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;


@Controller
public class OrderController {

    @Autowired
    private PayService payService;

    @Autowired
    private OrderService orderService;

    @RequestMapping(value = "/order", method = RequestMethod.GET)
    public String getDetail(HttpServletRequest request, HttpServletResponse response){
        OrderEntiry orderView = orderService.getDetail("1");

        request.setAttribute("order", orderView);
        return "order";
    }

    /**
     * 異步并發(fā)調(diào)用
     * @param request
     * @param response
     * @return
     */
    @RequestMapping(value = "/cancel", method = RequestMethod.GET)
    public String cancel(HttpServletRequest request, HttpServletResponse response)  {
        OrderEntiry orderView = orderService.getDetail("1");

        String cancel_order = null,cancel_pay = null;
        long start = System.currentTimeMillis();

        //若設(shè)置了async=true,方法立即返回null
        cancel_order = orderService.cancel(orderView);
        //只有async=true阁苞,才能得到此對(duì)象困檩,否則為null
        Future<String> cancelOrder = RpcContext.getContext().getFuture();
        cancel_pay = payService.cancelPay(orderView.getMoney());
        Future<String> cancelpay = RpcContext.getContext().getFuture();

        /**
         * Future模式
         *
         */

        try {
            cancel_order = cancelOrder.get();
            cancel_pay = cancelpay.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

        request.setAttribute("cancelOrder", cancel_order);
        request.setAttribute("cancelpay", cancel_pay);

        long time = System.currentTimeMillis() - start;
        request.setAttribute("time", time);

        return "/cancel";
    }

    /**
     * 事件通知
     * @param request
     * @param response
     * @return
     */
    @RequestMapping(value = "/order/submit", method = RequestMethod.GET)
    public String submit(HttpServletRequest request, HttpServletResponse response){
        OrderEntiry orderView = orderService.getDetail("1");
        orderView.setStatus(1);
        orderService.submit(orderView);

        request.setAttribute("order", orderView);
        return "/order";
    }

}
消費(fèi)端項(xiàng)目結(jié)構(gòu)

4.啟動(dòng)服務(wù)提供方和消費(fèi)端項(xiàng)目,其中消費(fèi)端項(xiàng)目控制臺(tái)信息:


控制臺(tái).JPG

這個(gè)信息就是我們?cè)赼pi中定義的fiter過濾器的具體實(shí)現(xiàn)那槽,即在微服務(wù)跨域調(diào)用過程中悼沿,traceId的追蹤,方便后續(xù)排查日志骚灸。

終于寫完了糟趾,接口代理的生成是不是有點(diǎn)動(dòng)態(tài)代理的感覺。然后用戶在XML中配置的dubbo標(biāo)簽屬性都保存在了URL中,URL攜帶的參數(shù)貫穿了整個(gè)dubbo架構(gòu)义郑,所有的組件調(diào)用都根據(jù)URL中配置的參數(shù)做處理蝶柿。其實(shí)SPI技術(shù)在很多地方都有用到,比如數(shù)據(jù)庫的驅(qū)動(dòng)非驮,日志的處理交汤,原理不是很復(fù)雜,仔細(xì)研究下就明白了劫笙。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末芙扎,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子填大,更是在濱河造成了極大的恐慌戒洼,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,386評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件允华,死亡現(xiàn)場(chǎng)離奇詭異圈浇,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)例获,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門汉额,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人榨汤,你說我怎么就攤上這事蠕搜。” “怎么了收壕?”我有些...
    開封第一講書人閱讀 164,704評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵妓灌,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我蜜宪,道長(zhǎng)虫埂,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,702評(píng)論 1 294
  • 正文 為了忘掉前任圃验,我火速辦了婚禮掉伏,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘澳窑。我一直安慰自己斧散,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,716評(píng)論 6 392
  • 文/花漫 我一把揭開白布摊聋。 她就那樣靜靜地躺著鸡捐,像睡著了一般。 火紅的嫁衣襯著肌膚如雪麻裁。 梳的紋絲不亂的頭發(fā)上箍镜,一...
    開封第一講書人閱讀 51,573評(píng)論 1 305
  • 那天源祈,我揣著相機(jī)與錄音,去河邊找鬼色迂。 笑死香缺,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的脚草。 我是一名探鬼主播赫悄,決...
    沈念sama閱讀 40,314評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼馏慨!你這毒婦竟也來了埂淮?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,230評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤写隶,失蹤者是張志新(化名)和其女友劉穎倔撞,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體慕趴,經(jīng)...
    沈念sama閱讀 45,680評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡痪蝇,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,873評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了冕房。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片躏啰。...
    茶點(diǎn)故事閱讀 39,991評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖耙册,靈堂內(nèi)的尸體忽然破棺而出给僵,到底是詐尸還是另有隱情,我是刑警寧澤详拙,帶...
    沈念sama閱讀 35,706評(píng)論 5 346
  • 正文 年R本政府宣布帝际,位于F島的核電站,受9級(jí)特大地震影響饶辙,放射性物質(zhì)發(fā)生泄漏蹲诀。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,329評(píng)論 3 330
  • 文/蒙蒙 一弃揽、第九天 我趴在偏房一處隱蔽的房頂上張望脯爪。 院中可真熱鬧,春花似錦矿微、人聲如沸痕慢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽守屉。三九已至惑艇,卻和暖如春蒿辙,著一層夾襖步出監(jiān)牢的瞬間拇泛,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工思灌, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留俺叭,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,158評(píng)論 3 370
  • 正文 我出身青樓泰偿,卻偏偏與公主長(zhǎng)得像熄守,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子耗跛,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,941評(píng)論 2 355

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

  • Dubbo采用微內(nèi)核+插件體系裕照,使得設(shè)計(jì)優(yōu)雅,擴(kuò)展性強(qiáng)调塌。那所謂的微內(nèi)核+插件體系是如何實(shí)現(xiàn)的呢晋南!大家是否熟悉spi...
    carl_zhao閱讀 937評(píng)論 1 3
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)羔砾,斷路器负间,智...
    卡卡羅2017閱讀 134,657評(píng)論 18 139
  • 前面我們了解過了Java的SPI擴(kuò)展機(jī)制,對(duì)于Java擴(kuò)展機(jī)制的原理以及優(yōu)缺點(diǎn)也有了大概的了解姜凄,這里繼續(xù)深入一下D...
    加大裝益達(dá)閱讀 5,062評(píng)論 2 20
  • 一.概覽 整體描述 dubbo利用spi擴(kuò)展機(jī)制實(shí)現(xiàn)大量的動(dòng)態(tài)擴(kuò)展政溃,要想充分了解dubbo的擴(kuò)展機(jī)制,首先必須弄明...
    致慮閱讀 902評(píng)論 0 2
  • 0 前言 站在一個(gè)框架作者的角度來說态秧,定義一個(gè)接口董虱,自己默認(rèn)給出幾個(gè)接口的實(shí)現(xiàn)類,同時(shí) 允許框架的使用者也能夠自定...
    七寸知架構(gòu)閱讀 16,245評(píng)論 3 67