☆聊聊Dubbo(五):核心源碼-SPI擴展

0 前言

站在一個框架作者的角度來說儒喊,定義一個接口,自己默認給出幾個接口的實現(xiàn)類册养,同時 允許框架的使用者也能夠自定義接口的實現(xiàn)」谷現(xiàn)在一個簡單的問題就是:如何優(yōu)雅的根據(jù)一個接口來獲取該接口的所有實現(xiàn)類呢?

JDK SPI 正是為了優(yōu)雅解決這個問題而生霞赫,SPI 全稱為 (Service Provider Interface)腮介,即服務提供商接口,是JDK內(nèi)置的一種服務提供發(fā)現(xiàn)機制端衰。目前有不少框架用它來做服務的擴展發(fā)現(xiàn)叠洗,簡單來說,它就是一種動態(tài)替換發(fā)現(xiàn)服務實現(xiàn)者的機制旅东。

所以惕味,Dubbo如此被廣泛接納的其中的 一個重要原因就是基于SPI實現(xiàn)的強大靈活的擴展機制,開發(fā)者可自定義插件嵌入Dubbo玉锌,實現(xiàn)靈活的業(yè)務需求名挥。

有人會覺得這就是建立在面向接口編程下的一種為了使組件可擴展或動態(tài)變更實現(xiàn)的規(guī)范,常見的類SPI的設計有 JDBC主守、JNDI禀倔、JAXP 等,很多開源框架的內(nèi)部實現(xiàn)也采用了SPI参淫。例如:JDBC的架構是由一套API組成救湖,用于給Java應用提供訪問不同數(shù)據(jù)庫的能力,而數(shù)據(jù)庫提供商的驅動軟件各不相同涎才,JDBC通過提供一套通用行為的API接口鞋既,底層可以由提供商自由實現(xiàn)力九,雖然JDBC的設計沒有指明是SPI,但也和SPI的設計類似邑闺。

1 JDK SPI擴展

JDK為SPI的實現(xiàn)提供了工具類跌前,即java.util.ServiceLoader,ServiceLoader中定義的SPI規(guī)范沒有什么特別之處陡舅,只需要有一個提供者配置文件(provider-configuration file)抵乓,該文件需要在resource目錄META-INF/services下,文件名就是服務接口的全限定名靶衍。

  1. 文件內(nèi)容是提供者Class的全限定名列表灾炭,顯然提供者Class都應該實現(xiàn)服務接口;
  2. 文件必須使用UTF-8編碼颅眶;

1.1 JDK SPI 示例

代碼示例
/**
 * SPI服務接口
 */
public interface Cmand {
    public void execute();
}
public class ShutdownCommand implements Cmand {
    public void execute() {
        System.out.println("shutdown....");  
    }
}
public class StartCommand implements Cmand {
    public void execute() {
        System.out.println("start....");
    }
}
public class SPIMain {
    public static void main(String[] args) {
        ServiceLoader<Cmand> loader = ServiceLoader.load(Cmand.class);
        System.out.println(loader);
 
        for (Cmand Cmand : loader) {
            Cmand.execute();
        }
    }
}

META-INF/services/com.unei.serviceloader.Cmand文件中配置:

com.unei.serviceloader.impl.ShutdownCommand  
com.unei.serviceloader.impl.StartCommand 

運行結果:

java.util.ServiceLoader[com.unei.serviceloader.Cmand]
shutdown....
start....

1.2 JDK SPI 原理

  1. 配置文件為什么要放在META-INF/services下面蜈出?

    ServiceLoader類定義如下:

    private static final String PREFIX = "META-INF/services/"; (JDK已經(jīng)寫死了)
    

    但是 如果ServiceLoader在load時提供Classloader,則可以從其他的目錄讀取涛酗。

  2. ServiceLoader讀取實現(xiàn)類是什么時候實例化的掏缎?來看下ServiceLoader的幾個重要屬性:

    
    private static final String PREFIX = "META-INF/services/";
    
    // 要加載的接口
    private Class<S> service;
    
    // The class loader used to locate, load, and instantiate providers
    private ClassLoader loader;
    
    // 用于緩存已經(jīng)加載的接口實現(xiàn)類,其中key為實現(xiàn)類的完整類名
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    
    // 用于延遲加載接口的實現(xiàn)類
    private LazyIterator lookupIterator;
    
    public void reload() {
        providers.clear();
        lookupIterator = new LazyIterator(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();
    }
    
    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;
        }
    
        public boolean hasNext() {
            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;
        }
    
        public S next() {
            if (!hasNext()) {
                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();
        }
    
        public void remove() {
            throw new UnsupportedOperationException();
        }
    }
    

1.3 JDK SPI ServiceLoader缺點

  1. 雖然ServiceLoader也算是使用的延遲加載,但是基本只能通過遍歷全部獲取沈自,也就是接口的實現(xiàn)類全部加載并實例化一遍酌儒。如果你并不想用某些實現(xiàn)類,它也被加載并實例化了枯途,這就造成了浪費忌怎。
  2. 獲取某個實現(xiàn)類的方式不夠靈活,只能通過Iterator形式獲取酪夷,不能根據(jù)某個參數(shù)來獲取對應的實現(xiàn)類榴啸。

2 Dubbo SPI擴展

Dubbo對JDK SPI進行了擴展,對服務提供者配置文件中的內(nèi)容進行了改造晚岭,由原來的提供者類的全限定名列表改成了KV形式的列表鸥印,這也導致了Dubbo中無法直接使用JDK ServiceLoader,所以坦报,與之對應的库说,在Dubbo中有ExtensionLoader,ExtensionLoader是擴展點載入器片择,用于載入Dubbo中的各種可配置組件潜的,比如:動態(tài)代理方式(ProxyFactory)、負載均衡策略(LoadBalance)字管、RCP協(xié)議(Protocol)啰挪、攔截器(Filter)信不、容器類型(Container)、集群方式(Cluster)和注冊中心類型(RegistryFactory)等亡呵。

總之抽活,Dubbo為了應對各種場景,它的所有內(nèi)部組件都是通過這種SPI的方式來管理的政己,這也是為什么Dubbo需要將服務提供者配置文件設計成KV鍵值對形式,這個K就是我們在Dubbo配置文件或注解中用到的K掏愁,Dubbo直接通過服務接口(上面提到的ProxyFactory歇由、LoadBalance、Protocol果港、Filter等)和配置的K從ExtensionLoader拿到服務提供的實現(xiàn)類沦泌。

同時,由于Dubbo使用了URL總線的設計辛掠,即很多參數(shù)通過URL對象來傳遞谢谦,在實際中,具體要用到哪個值萝衩,可以通過URL中的參數(shù)值來指定回挽。

2.1 擴展功能介紹

Dubbo對SPI的擴展是 通過ExtensionLoader來實現(xiàn)的,查看ExtensionLoader的源碼猩谊,可以看到Dubbo對JDK SPI 做了三個方面的擴展:

  1. 方便獲取擴展實現(xiàn):JDK SPI僅僅通過接口類名獲取所有實現(xiàn)千劈,而ExtensionLoader則通過接口類名和key值獲取一個實現(xiàn)

  2. IOC依賴注入功能:Adaptive實現(xiàn)牌捷,就是生成一個代理類墙牌,這樣就可以根據(jù)實際調(diào)用時的一些參數(shù)動態(tài)決定要調(diào)用的類了

    舉例來說:接口A暗甥,實現(xiàn)者A1喜滨、A2。接口B撤防,實現(xiàn)者B1虽风、B2。

    現(xiàn)在實現(xiàn)者A1含有setB()方法寄月,會自動注入一個接口B的實現(xiàn)者焰情,此時注入B1還是B2呢?都不是剥懒,而是注入一個動態(tài)生成的接口B的實現(xiàn)者B$Adpative内舟,該實現(xiàn)者能夠根據(jù)參數(shù)的不同,自動引用B1或者B2來完成相應的功能初橘;

  3. 采用裝飾器模式進行功能增強验游,自動包裝實現(xiàn)充岛,這種實現(xiàn)的類一般是自動激活的,常用于包裝類耕蝉,比如:Protocol的兩個實現(xiàn)類:ProtocolFilterWrapper崔梗、ProtocolListenerWrapper。

    還是第2個的例子垒在,接口A的另一個實現(xiàn)者AWrapper1蒜魄。大體內(nèi)容如下:

    private A a;
    AWrapper1(A a){
       this.a=a;
    }
    

    因此,當在獲取某一個接口A的實現(xiàn)者A1的時候场躯,已經(jīng)自動被AWrapper1包裝了谈为。

2.2 擴展源碼分析

2.2.1 ExtensionLoader初始化

以獲取DubboProtocol為例

@SPI("dubbo")
public interface Protocol {
    
    /**
     * 獲取缺省端口,當用戶沒有配置端口時使用踢关。
     * 
     * @return 缺省端口
     */
    int getDefaultPort();

    /**
     * 暴露遠程服務:<br>
     * 1. 協(xié)議在接收請求時伞鲫,應記錄請求來源方地址信息:RpcContext.getContext().setRemoteAddress();<br>
     * 2. export()必須是冪等的,也就是暴露同一個URL的Invoker兩次签舞,和暴露一次沒有區(qū)別秕脓。<br>
     * 3. export()傳入的Invoker由框架實現(xiàn)并傳入,協(xié)議不需要關心儒搭。<br>
     * 
     * @param <T> 服務的類型
     * @param invoker 服務的執(zhí)行體
     * @return exporter 暴露服務的引用吠架,用于取消暴露
     * @throws RpcException 當暴露服務出錯時拋出,比如端口已占用
     */
    @Adaptive
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;

    /**
     * 引用遠程服務:<br>
     * 1. 當用戶調(diào)用refer()所返回的Invoker對象的invoke()方法時搂鲫,協(xié)議需相應執(zhí)行同URL遠端export()傳入的Invoker對象的invoke()方法诵肛。<br>
     * 2. refer()返回的Invoker由協(xié)議實現(xiàn),協(xié)議通常需要在此Invoker中發(fā)送遠程請求默穴。<br>
     * 3. 當url中有設置check=false時怔檩,連接失敗不能拋出異常,并內(nèi)部自動恢復蓄诽。<br>
     * 
     * @param <T> 服務的類型
     * @param type 服務的類型
     * @param url 遠程服務的URL地址
     * @return invoker 服務的本地代理
     * @throws RpcException 當連接服務提供方失敗時拋出
     */
    @Adaptive
    <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;

    /**
     * 釋放協(xié)議:<br>
     * 1. 取消該協(xié)議所有已經(jīng)暴露和引用的服務薛训。<br>
     * 2. 釋放協(xié)議所占用的所有資源,比如連接和端口仑氛。<br>
     * 3. 協(xié)議在釋放后乙埃,依然能暴露和引用新的服務。<br>
     */
    void destroy();

}

public class DubboProtocol extends AbstractProtocol {

    public static final String NAME = "dubbo";
    ...
    ...
}

// 示例:
ExtensionLoader<Protocol> protocolLoader = ExtensionLoader.getExtensionLoader(Protocol.class);
Protocol dubboProtocol = protocolLoader.getExtension(DubboProtocol.NAME);
  1. ExtensionLoader.getExtensionLoader(Protocol.class):獲取ExtensionLoader實例

    private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<Class<?>, Object>();
    
    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!");
         }
         // 0. 判斷是否為通過SPI注解定義的可擴展接口
         if(!withExtensionAnnotation(type)) {
             throw new IllegalArgumentException("Extension type(" + type + 
                     ") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
         }
         // 1. 先從EXTENSION_LOADERS中锯岖,根據(jù)傳入可擴展類型type查找
         ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
         if (loader == null) {
             // 2. 不存在介袜,則新建ExtensionLoader實例
             EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
             loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
         }
         return loader;
    }
    
     private ExtensionLoader(Class<?> type) {
         this.type = type;
         objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
     }
    
     private static <T> boolean withExtensionAnnotation(Class<T> type) {
         return type.isAnnotationPresent(SPI.class);
     }
    

    ExtensionLoader充當插件工廠角色,提供了一個私有的構造器出吹。其入?yún)ype為擴展接口類型遇伞。Dubbo通過SPI注解定義了可擴展的接口,如Filter捶牢、Transporter等鸠珠。每個類型的擴展對應一個ExtensionLoader巍耗。SPI的value參數(shù)決定了默認的擴展實現(xiàn)

    當擴展類型是ExtensionFactory時渐排,不指定objectFactory炬太,否則初始化ExtensionFactory的ExtensionLoader并獲取一個擴展適配器

  2. protocolLoader.getExtension(DubboProtocol.NAME):根據(jù)Key獲取相應的擴展實現(xiàn)類實例

     /**
      * 返回指定名字的擴展驯耻。如果指定名字的擴展不存在亲族,則拋異常 {@link IllegalStateException}.
      *
      * @param name
      * @return
      */
     @SuppressWarnings("unchecked")
     public T getExtension(String name) {
         if (name == null || name.length() == 0)
             throw new IllegalArgumentException("Extension name == null");
         if ("true".equals(name)) {
             return getDefaultExtension();
         }
         // 1. 先從緩存中取相應的擴展實現(xiàn)類實例
         Holder<Object> holder = cachedInstances.get(name);
         if (holder == null) {
             cachedInstances.putIfAbsent(name, new Holder<Object>());
             holder = cachedInstances.get(name);
         }
         Object instance = holder.get();
         if (instance == null) {
             synchronized (holder) {
                 instance = holder.get();
                 if (instance == null) {
                     // 2. 創(chuàng)建相應的擴展實現(xiàn)類實例
                     instance = createExtension(name);
                     holder.set(instance);
                 }
             }
         }
         return (T) instance;
    }
    
     @SuppressWarnings("unchecked")
     private T createExtension(String name) {
         // 3. 根據(jù)name獲取相應擴展類的類實例
         Class<?> clazz = getExtensionClasses().get(name);
         if (clazz == null) {
             throw findException(name);
         }
         try {
             T instance = (T) EXTENSION_INSTANCES.get(clazz);
             if (instance == null) {
                 EXTENSION_INSTANCES.putIfAbsent(clazz, (T) clazz.newInstance());
                 instance = (T) EXTENSION_INSTANCES.get(clazz);
             }
             injectExtension(instance);
             Set<Class<?>> wrapperClasses = cachedWrapperClasses;
             if (wrapperClasses != null && wrapperClasses.size() > 0) {
                 for (Class<?> wrapperClass : wrapperClasses) {
                     instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
                 }
             }
             return instance;
         } catch (Throwable t) {
             throw new IllegalStateException("Extension instance(name: " + name + ", class: " +
                     type + ")  could not be instantiated: " + t.getMessage(), t);
         }
     }   
    

    createExtension方法就是創(chuàng)建相應的擴展類實例,具體里面的創(chuàng)建步驟下文會具體說到可缚,接下來先繼續(xù)深入看getExtensionClasses方法是如何從配置文件中找到相應的擴展類的類配置霎迫。

2.2.2 配置文件掃描

Dubbo默認依次掃描META-INF/dubbo/internal/、META-INF/dubbo/城看、META-INF/services/三個classpath目錄下的配置文件女气。配置文件以具體擴展接口全名命名杏慰,如:com.alibaba.dubbo.rpc.Filter测柠,內(nèi)容如下:

# 等號前為擴展名,其后為擴展實現(xiàn)類全路徑名
echo=com.alibaba.dubbo.rpc.filter.EchoFilter
generic=com.alibaba.dubbo.rpc.filter.GenericFilter
genericimpl=com.alibaba.dubbo.rpc.filter.GenericImplFilter
token=com.alibaba.dubbo.rpc.filter.TokenFilter
accesslog=com.alibaba.dubbo.rpc.filter.AccessLogFilter
activelimit=com.alibaba.dubbo.rpc.filter.ActiveLimitFilter
classloader=com.alibaba.dubbo.rpc.filter.ClassLoaderFilter
context=com.alibaba.dubbo.rpc.filter.ContextFilter
consumercontext=com.alibaba.dubbo.rpc.filter.ConsumerContextFilter
exception=com.alibaba.dubbo.rpc.filter.ExceptionFilter
executelimit=com.alibaba.dubbo.rpc.filter.ExecuteLimitFilter
deprecated=com.alibaba.dubbo.rpc.filter.DeprecatedFilter
compatible=com.alibaba.dubbo.rpc.filter.CompatibleFilter
timeout=com.alibaba.dubbo.rpc.filter.TimeoutFilter
monitor=com.alibaba.dubbo.monitor.support.MonitorFilter
validation=com.alibaba.dubbo.validation.filter.ValidationFilter
cache=com.alibaba.dubbo.cache.filter.CacheFilter
trace=com.alibaba.dubbo.rpc.protocol.dubbo.filter.TraceFilter
future=com.alibaba.dubbo.rpc.protocol.dubbo.filter.FutureFilter

從上一小節(jié)的缘滥,getExtensionClasses()方法源碼看起:

    private Map<String, Class<?>> getExtensionClasses() {
        Map<String, Class<?>> classes = cachedClasses.get();
        if (classes == null) {
            synchronized (cachedClasses) {
                classes = cachedClasses.get();
                if (classes == null) {
                    classes = loadExtensionClasses();
                    cachedClasses.set(classes);
                }
            }
        }
        return classes;
    }

    // 此方法已經(jīng)getExtensionClasses方法同步過轰胁。
    private Map<String, Class<?>> loadExtensionClasses() {
        final SPI defaultAnnotation = type.getAnnotation(SPI.class);
        if(defaultAnnotation != null) {
            String value = defaultAnnotation.value();
            if(value != null && (value = value.trim()).length() > 0) {
                String[] names = NAME_SEPARATOR.split(value);
                if(names.length > 1) {
                    throw new IllegalStateException("more than 1 default extension name on extension " + type.getName()
                            + ": " + Arrays.toString(names));
                }
                if(names.length == 1) cachedDefaultName = names[0];
            }
        }
        
        Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
        loadFile(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
        loadFile(extensionClasses, DUBBO_DIRECTORY);
        loadFile(extensionClasses, SERVICES_DIRECTORY);
        return extensionClasses;
    }

    private void loadFile(Map<String, Class<?>> extensionClasses, String dir) {
        String fileName = dir + type.getName();
        try {
            Enumeration<java.net.URL> urls;
            ClassLoader classLoader = findClassLoader();
            if (classLoader != null) {
                urls = classLoader.getResources(fileName);
            } else {
                urls = ClassLoader.getSystemResources(fileName);
            }
            if (urls != null) {
                // 1. 逐行讀取配置文件,提取出擴展名或擴展類路徑
                while (urls.hasMoreElements()) {
                    java.net.URL url = urls.nextElement();
                    try {
                        BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream(), "utf-8"));
                        try {
                            String line = null;
                            while ((line = reader.readLine()) != null) {
                                final int ci = line.indexOf('#');
                                if (ci >= 0) line = line.substring(0, ci);
                                line = line.trim();
                                if (line.length() > 0) {
                                    try {
                                        String name = null;
                                        int i = line.indexOf('=');
                                        if (i > 0) {
                                            name = line.substring(0, i).trim();
                                            line = line.substring(i + 1).trim();
                                        }
                                        if (line.length() > 0) {
                                            // 2. 利用Class.forName方法進行類加載
                                            Class<?> clazz = Class.forName(line, true, classLoader);
                                            if (! type.isAssignableFrom(clazz)) {
                                                throw new IllegalStateException("Error when load extension class(interface: " +
                                                        type + ", class line: " + clazz.getName() + "), class " 
                                                        + clazz.getName() + "is not subtype of interface.");
                                            }
                                            // 3. 處理Adaptive注解朝扼,若存在則將該實現(xiàn)類保存至cachedAdaptiveClass屬性
                                            if (clazz.isAnnotationPresent(Adaptive.class)) {
                                                if(cachedAdaptiveClass == null) {
                                                    cachedAdaptiveClass = clazz;
                                                } else if (! cachedAdaptiveClass.equals(clazz)) {
                                                    throw new IllegalStateException("More than 1 adaptive class found: "
                                                            + cachedAdaptiveClass.getClass().getName()
                                                            + ", " + clazz.getClass().getName());
                                                }
                                            } else {
                                                try {
                                                    // 4. 嘗試獲取參數(shù)類型為當前擴展類型的構造器方法赃阀,若成功則表明存在該擴展的封裝類型,將封裝類型存入wrappers集合擎颖;否則轉入第五步
                                                    clazz.getConstructor(type);
                                                    Set<Class<?>> wrappers = cachedWrapperClasses;
                                                    if (wrappers == null) {
                                                        cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
                                                        wrappers = cachedWrapperClasses;
                                                    }
                                                    wrappers.add(clazz);
                                                } catch (NoSuchMethodException e) {
                                                    clazz.getConstructor();
                                                    if (name == null || name.length() == 0) {
                                                        name = findAnnotationName(clazz);
                                                        if (name == null || name.length() == 0) {
                                                            if (clazz.getSimpleName().length() > type.getSimpleName().length()
                                                                    && clazz.getSimpleName().endsWith(type.getSimpleName())) {
                                                                name = clazz.getSimpleName().substring(0, clazz.getSimpleName().length() - type.getSimpleName().length()).toLowerCase();
                                                            } else {
                                                                throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + url);
                                                            }
                                                        }
                                                    }

                                                    // 5. 處理active注解榛斯,將擴展名對應active注解存入cachedActivates
                                                    String[] names = NAME_SEPARATOR.split(name);
                                                    if (names != null && names.length > 0) {
                                                        Activate activate = clazz.getAnnotation(Activate.class);
                                                        if (activate != null) {
                                                            cachedActivates.put(names[0], activate);
                                                        }
                                                        for (String n : names) {
                                                            if (! cachedNames.containsKey(clazz)) {
                                                                cachedNames.put(clazz, n);
                                                            }
                                                            Class<?> c = extensionClasses.get(n);
                                                            if (c == null) {
                                                                extensionClasses.put(n, clazz);
                                                            } else if (c != clazz) {
                                                                throw new IllegalStateException("Duplicate extension " + type.getName() + " name " + n + " on " + c.getName() + " and " + clazz.getName());
                                                            }
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                    } catch (Throwable t) {
                                        IllegalStateException e = new IllegalStateException("Failed to load extension class(interface: " + type + ", class line: " + line + ") in " + url + ", cause: " + t.getMessage(), t);
                                        exceptions.put(line, e);
                                    }
                                }
                            } // end of while read lines
                        } finally {
                            reader.close();
                        }
                    } catch (Throwable t) {
                        logger.error("Exception when load extension class(interface: " +
                                            type + ", class file: " + url + ") in " + url, t);
                    }
                } // end of while urls
            }
        } catch (Throwable t) {
            logger.error("Exception when load extension class(interface: " +
                    type + ", description file: " + fileName + ").", t);
        }
    }

以上配置文件加載步驟如下:

  1. 逐行讀取配置文件,提取出擴展名或擴展類路徑搂捧;

  2. 利用Class.forName方法進行類加載驮俗;

    Class<?> clazz = Class.forName(line, true, classLoader);
    
  3. 處理Adaptive注解,若存在則將該實現(xiàn)類保存至cachedAdaptiveClass屬性

    if (clazz.isAnnotationPresent(Adaptive.class)) {
       if(cachedAdaptiveClass == null) {
           cachedAdaptiveClass = clazz;
       } else if (! cachedAdaptiveClass.equals(clazz)) {
           throw new IllegalStateException("More than 1 adaptive class found:"    + cachedAdaptiveClass.getClass().getName()
                 + ", " + clazz.getClass().getName());
       }
    }
    
  4. 嘗試獲取參數(shù)類型為當前擴展類型的構造器方法允跑,若成功則表明存在該擴展的封裝類型王凑,將封裝類型存入wrappers集合;否則拋出異常轉入第五步聋丝;

    try {
        // 擴展類型參數(shù)的構造器是封裝器的約定特征索烹,目前dubbo中默認的只有Filter和Listener的封裝器
        clazz.getConstructor(type); 
        Set<Class<?>> wrappers = cachedWrapperClasses;
        if (wrappers == null) {
            cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
            wrappers = cachedWrapperClasses;
        }
        wrappers.add(clazz);
    } catch (NoSuchMethodException e) {
        // 第五步
    }
    
  5. 處理active注解,將擴展名對應active注解存入cachedActivates弱睦;

    Activate activate = clazz.getAnnotation(Activate.class);
    if (activate != null) {
        cachedActivates.put(names[0], activate);
    }
    

2.2.3 擴展適配器

在dubbo擴展中百姓,適配器模式被廣泛使用,其作用在于為同一擴展類型下的多個擴展實現(xiàn)的調(diào)用提供路由功能况木,如指定優(yōu)先級等瓣戚。dubbo提供了兩種方式來生成擴展適配器:

  1. 靜態(tài)適配器擴展

    所謂的靜態(tài)適配器擴展就是提前通過編碼的形式確定擴展的具體實現(xiàn)端圈,且該實現(xiàn)類由Adaptive注解標注,如:AdaptiveCompiler子库。在加載配置文件的loadFile方法中舱权,已經(jīng)描述過處理該類型擴展的邏輯,具體可參考上一小節(jié)loadFile()方法源碼仑嗅。

    @Adaptive
    public class AdaptiveCompiler implements Compiler {
    
        private static volatile String DEFAULT_COMPILER;
    
        public static void setDefaultCompiler(String compiler) {
            DEFAULT_COMPILER = compiler;
        }
    
        public Class<?> compile(String code, ClassLoader classLoader) {
            Compiler compiler;
            ExtensionLoader<Compiler> loader = ExtensionLoader.getExtensionLoader(Compiler.class);
            String name = DEFAULT_COMPILER; // copy reference
            if (name != null && name.length() > 0) {
                compiler = loader.getExtension(name);
            } else {
                compiler = loader.getDefaultExtension();
            }
            return compiler.compile(code, classLoader);
        }
    }
    
  2. 動態(tài)適配器擴展

    動態(tài)適配器擴展即通過動態(tài)代理生成擴展類的動態(tài)代理類宴倍,在dubbo中是通過javassist技術生成的。與傳統(tǒng)的jdk動態(tài)代理仓技、cglib不同鸵贬,javassist提供封裝后的API對字節(jié)碼進行間接操作,簡單易用脖捻,不關心具體字節(jié)碼阔逼,靈活性更高,且處理效率也較高地沮,是dubbo默認的編譯器嗜浮。

首先,從ExtensionLoader構造器中會調(diào)用getAdaptiveExtension()方法觸發(fā)為當前擴展類型生成適配器摩疑,源碼如下:

    private ExtensionLoader(Class<?> type) {
        this.type = type;
        objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
    }

    public T getAdaptiveExtension() {
        // 1. 首先危融,檢查是否存在當前擴展類靜態(tài)適配器
        Object instance = cachedAdaptiveInstance.get();
        if (instance == null) {
            if(createAdaptiveInstanceError == null) {
                synchronized (cachedAdaptiveInstance) {
                    instance = cachedAdaptiveInstance.get();
                    if (instance == null) {
                        try {
                            // 2. 創(chuàng)建當前擴展類動態(tài)適配器
                            instance = createAdaptiveExtension();
                            cachedAdaptiveInstance.set(instance);
                        } catch (Throwable t) {
                            createAdaptiveInstanceError = t;
                            throw new IllegalStateException("fail to create adaptive instance: " + t.toString(), t);
                        }
                    }
                }
            }
            else {
                throw new IllegalStateException("fail to create adaptive instance: " + createAdaptiveInstanceError.toString(), createAdaptiveInstanceError);
            }
        }

        return (T) instance;
    }

    private T createAdaptiveExtension() {
        try {
            // IOC屬性注入
            return injectExtension((T) getAdaptiveExtensionClass().newInstance());
        } catch (Exception e) {
            throw new IllegalStateException("Can not create adaptive extenstion " + type + ", cause: " + e.getMessage(), e);
        }
    }
    
    private Class<?> getAdaptiveExtensionClass() {
        getExtensionClasses();
        if (cachedAdaptiveClass != null) {
            return cachedAdaptiveClass;
        }
        return cachedAdaptiveClass = createAdaptiveExtensionClass();
    }
    
    private Class<?> createAdaptiveExtensionClass() {
        String code = createAdaptiveExtensionClassCode();
        ClassLoader classLoader = findClassLoader();
        // 得到Adaptive類代碼內(nèi)容,通過Compiler進行編譯和類加載
        com.alibaba.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
        return compiler.compile(code, classLoader);
    }
    
    // 創(chuàng)建當前擴展動態(tài)適配器
    private String createAdaptiveExtensionClassCode() {
        StringBuilder codeBuidler = new StringBuilder();
        Method[] methods = type.getMethods();
        boolean hasAdaptiveAnnotation = false;
        // 1. 檢查是否至少有一個方法有Adaptive注解雷袋,若不存在則拋出異常吉殃,即要完成動態(tài)代理,必須有方法標注了Adaptive注解
        for(Method m : methods) {
            if(m.isAnnotationPresent(Adaptive.class)) {
                hasAdaptiveAnnotation = true;
                break;
            }
        }
        // 完全沒有Adaptive方法楷怒,則不需要生成Adaptive類
        if(! hasAdaptiveAnnotation)
            throw new IllegalStateException("No adaptive method on extension " + type.getName() + ", refuse to create the adaptive class!");
        
        codeBuidler.append("package " + type.getPackage().getName() + ";");
        codeBuidler.append("\nimport " + ExtensionLoader.class.getName() + ";");
        codeBuidler.append("\npublic class " + type.getSimpleName() + "$Adpative" + " implements " + type.getCanonicalName() + " {");
        
        for (Method method : methods) {
            Class<?> rt = method.getReturnType();
            Class<?>[] pts = method.getParameterTypes();
            Class<?>[] ets = method.getExceptionTypes();

            Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
            StringBuilder code = new StringBuilder(512);
            if (adaptiveAnnotation == null) {
                code.append("throw new UnsupportedOperationException(\"method ")
                        .append(method.toString()).append(" of interface ")
                        .append(type.getName()).append(" is not adaptive method!\");");
            } else {
                int urlTypeIndex = -1;
                for (int i = 0; i < pts.length; ++i) {
                    if (pts[i].equals(URL.class)) {
                        urlTypeIndex = i;
                        break;
                    }
                }
                // 對于有Adaptive注解的方法蛋勺,判斷其入?yún)⒅惺欠裼蠻RL類型的參數(shù),或者復雜參數(shù)中是否有URL類型的屬性鸠删,若沒有則拋出異常抱完。
                // 這里體現(xiàn)出了為什么dubbo要提供動態(tài)適配器生成機制。dubbo中的URL總線提供了服務的全部信息冶共,而開發(fā)者可以定義差異化的服務配置乾蛤,因此生成的URL差異化也較大,若全部靠用戶硬編碼靜態(tài)適配器的話效率太低捅僵。
                // 有了動態(tài)代理家卖,dubbo可以根據(jù)URL參數(shù)動態(tài)地生成適配器的適配邏輯,確定擴展實現(xiàn)的獲取優(yōu)先級庙楚。因此上荡,URL作為參數(shù)直接或間接傳入是必須的,否則失去了動態(tài)生成的憑據(jù)。
                // 有類型為URL的參數(shù)
                if (urlTypeIndex != -1) {
                    // Null Point check
                    String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"url == null\");",
                                    urlTypeIndex);
                    code.append(s);
                    
                    s = String.format("\n%s url = arg%d;", URL.class.getName(), urlTypeIndex); 
                    code.append(s);
                }
                // 參數(shù)沒有URL類型
                else {
                    String attribMethod = null;
                    
                    // 找到參數(shù)的URL屬性
                    LBL_PTS:
                    for (int i = 0; i < pts.length; ++i) {
                        Method[] ms = pts[i].getMethods();
                        for (Method m : ms) {
                            String name = m.getName();
                            if ((name.startsWith("get") || name.length() > 3)
                                    && Modifier.isPublic(m.getModifiers())
                                    && !Modifier.isStatic(m.getModifiers())
                                    && m.getParameterTypes().length == 0
                                    && m.getReturnType() == URL.class) {
                                urlTypeIndex = i;
                                attribMethod = name;
                                break LBL_PTS;
                            }
                        }
                    }
                    if(attribMethod == null) {
                        throw new IllegalStateException("fail to create adative class for interface " + type.getName()
                                + ": not found url parameter or url attribute in parameters of method " + method.getName());
                    }
                    
                    // Null point check
                    String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"%s argument == null\");",
                                    urlTypeIndex, pts[urlTypeIndex].getName());
                    code.append(s);
                    s = String.format("\nif (arg%d.%s() == null) throw new IllegalArgumentException(\"%s argument %s() == null\");",
                                    urlTypeIndex, attribMethod, pts[urlTypeIndex].getName(), attribMethod);
                    code.append(s);

                    s = String.format("%s url = arg%d.%s();",URL.class.getName(), urlTypeIndex, attribMethod); 
                    code.append(s);
                }
                
                String[] value = adaptiveAnnotation.value();
                // 沒有設置Key酪捡,則使用“擴展點接口名的點分隔 作為Key
                if(value.length == 0) {
                    char[] charArray = type.getSimpleName().toCharArray();
                    StringBuilder sb = new StringBuilder(128);
                    for (int i = 0; i < charArray.length; i++) {
                        if(Character.isUpperCase(charArray[i])) {
                            if(i != 0) {
                                sb.append(".");
                            }
                            sb.append(Character.toLowerCase(charArray[i]));
                        }
                        else {
                            sb.append(charArray[i]);
                        }
                    }
                    value = new String[] {sb.toString()};
                }
                
                boolean hasInvocation = false;
                for (int i = 0; i < pts.length; ++i) {
                    if (pts[i].getName().equals("com.alibaba.dubbo.rpc.Invocation")) {
                        // Null Point check
                        String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"invocation == null\");", i);
                        code.append(s);
                        s = String.format("\nString methodName = arg%d.getMethodName();", i); 
                        code.append(s);
                        hasInvocation = true;
                        break;
                    }
                }
                
                String defaultExtName = cachedDefaultName;
                String getNameCode = null;
                for (int i = value.length - 1; i >= 0; --i) {
                    if(i == value.length - 1) {
                        if(null != defaultExtName) {
                            if(!"protocol".equals(value[i]))
                                if (hasInvocation) 
                                    getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
                                else
                                    getNameCode = String.format("url.getParameter(\"%s\", \"%s\")", value[i], defaultExtName);
                            else
                                getNameCode = String.format("( url.getProtocol() == null ? \"%s\" : url.getProtocol() )", defaultExtName);
                        }
                        else {
                            if(!"protocol".equals(value[i]))
                                if (hasInvocation) 
                                    getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
                                else
                                    getNameCode = String.format("url.getParameter(\"%s\")", value[i]);
                            else
                                getNameCode = "url.getProtocol()";
                        }
                    }
                    else {
                        if(!"protocol".equals(value[i]))
                            if (hasInvocation) 
                                getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
                            else
                                getNameCode = String.format("url.getParameter(\"%s\", %s)", value[i], getNameCode);
                        else
                            getNameCode = String.format("url.getProtocol() == null ? (%s) : url.getProtocol()", getNameCode);
                    }
                }
                code.append("\nString extName = ").append(getNameCode).append(";");
                // check extName == null?
                String s = String.format("\nif(extName == null) " +
                        "throw new IllegalStateException(\"Fail to get extension(%s) name from url(\" + url.toString() + \") use keys(%s)\");",
                        type.getName(), Arrays.toString(value));
                code.append(s);
                
                s = String.format("\n%s extension = (%<s)%s.getExtensionLoader(%s.class).getExtension(extName);",
                        type.getName(), ExtensionLoader.class.getSimpleName(), type.getName());
                code.append(s);
                
                // return statement
                if (!rt.equals(void.class)) {
                    code.append("\nreturn ");
                }

                s = String.format("extension.%s(", method.getName());
                code.append(s);
                for (int i = 0; i < pts.length; i++) {
                    if (i != 0)
                        code.append(", ");
                    code.append("arg").append(i);
                }
                code.append(");");
            }
            
            codeBuidler.append("\npublic " + rt.getCanonicalName() + " " + method.getName() + "(");
            for (int i = 0; i < pts.length; i ++) {
                if (i > 0) {
                    codeBuidler.append(", ");
                }
                codeBuidler.append(pts[i].getCanonicalName());
                codeBuidler.append(" ");
                codeBuidler.append("arg" + i);
            }
            codeBuidler.append(")");
            if (ets.length > 0) {
                codeBuidler.append(" throws ");
                for (int i = 0; i < ets.length; i ++) {
                    if (i > 0) {
                        codeBuidler.append(", ");
                    }
                    codeBuidler.append(ets[i].getCanonicalName());
                }
            }
            codeBuidler.append(" {");
            codeBuidler.append(code.toString());
            codeBuidler.append("\n}");
        }
        codeBuidler.append("\n}");
        if (logger.isDebugEnabled()) {
            logger.debug(codeBuidler.toString());
        }
        return codeBuidler.toString();
    }

根據(jù)adaptive注解的value數(shù)組值叁征,及SPI注解定義的默認擴展名,確定適配邏輯逛薇,即擴展獲取的優(yōu)先級捺疼,這里不再羅列代碼,下面為一個具體生成的適配器源碼:

package com.alibaba.dubbo.remoting;
import com.alibaba.dubbo.common.extension.ExtensionLoader;
public class Transporter$Adpative implements com.alibaba.dubbo.remoting.Transporter {
    public com.alibaba.dubbo.remoting.Client connect(com.alibaba.dubbo.common.URL arg0, com.alibaba.dubbo.remoting.ChannelHandler arg1) throws com.alibaba.dubbo.common.URL {
        if (arg0 == null) throw new IllegalArgumentException("url == null");
        com.alibaba.dubbo.common.URL url = arg0;
        String extName = url.getParameter("client", url.getParameter("transporter", "netty")); // 處理順序
        if(extName == null) 
            throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.remoting.Transporter) name from url(" + url.toString() + ") use keys([client, transporter])");
        com.alibaba.dubbo.remoting.Transporter extension = (com.alibaba.dubbo.remoting.Transporter)ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.remoting.Transporter.class).getExtension(extName);
        return extension.connect(arg0, arg1);
    }
    public com.alibaba.dubbo.remoting.Server bind(com.alibaba.dubbo.common.URL arg0, com.alibaba.dubbo.remoting.ChannelHandler arg1) throws com.alibaba.dubbo.common.URL {
        if (arg0 == null) throw new IllegalArgumentException("url == null");
        com.alibaba.dubbo.common.URL url = arg0;
        String extName = url.getParameter("server", url.getParameter("transporter", "netty")); // 處理順序
        if(extName == null) 
            throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.remoting.Transporter) name from url(" + url.toString() + ") use keys([server, transporter])");
        com.alibaba.dubbo.remoting.Transporter extension = (com.alibaba.dubbo.remoting.Transporter)ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.remoting.Transporter.class).getExtension(extName);
        return extension.bind(arg0, arg1);
    }
}

可以看到永罚,核心邏輯是獲取擴展名extName啤呼,以bind方法為例,其獲取優(yōu)先級是server呢袱,transporter官扣,netty,可參見URL的getParameter方法源碼羞福。其中netty是Transporter接口的SPI注解確定的默認值惕蹄,而server和transporter是bind方法的Adaptive注解定義的。

@SPI("netty")
public interface Transporter {
    @Adaptive({Constants.SERVER_KEY, Constants.TRANSPORTER_KEY})
    Server bind(URL url, ChannelHandler handler) throws RemotingException;
    @Adaptive({Constants.CLIENT_KEY, Constants.TRANSPORTER_KEY})
    Client connect(URL url, ChannelHandler handler) throws RemotingException;
}

拿到擴展名后治专,再從ExtensionLoader獲取到擴展實例卖陵,調(diào)用具體的bind方法

源碼生成后看靠,ExtensionLoader再調(diào)用默認的JavassitCompiler進行編譯和類加載赶促,其具體實現(xiàn)原理不在本文討論范圍液肌,有機會的話后續(xù)會介紹這部分內(nèi)容挟炬。

綜上可知,ExtensionLoader提供了獲取擴展適配器的方法嗦哆,優(yōu)先查看是否有靜態(tài)適配器谤祖,否則會使用動態(tài)適配器

2.2.4 封裝類

dubbo中存在 一種對于擴展的封裝類老速,其功能是將各擴展實例串聯(lián)起來粥喜,形成擴展鏈,比如過濾器鏈橘券,監(jiān)聽鏈额湘。當調(diào)用ExtensionLoader的getExtension方法時,會做攔截處理旁舰,如果存在封裝器锋华,則返回封裝器實現(xiàn),而將真實實現(xiàn)通過構造方法注入到封裝器中箭窜。

    @SuppressWarnings("unchecked")
    private T createExtension(String name) {
        Class<?> clazz = getExtensionClasses().get(name);
        if (clazz == null) {
            throw findException(name);
        }
        try {
            T instance = (T) EXTENSION_INSTANCES.get(clazz);
            if (instance == null) {
                EXTENSION_INSTANCES.putIfAbsent(clazz, (T) clazz.newInstance());
                instance = (T) EXTENSION_INSTANCES.get(clazz);
            }
            // IOC 注入
            injectExtension(instance);
            Set<Class<?>> wrapperClasses = cachedWrapperClasses;
            if (wrapperClasses != null && wrapperClasses.size() > 0) {
                for (Class<?> wrapperClass : wrapperClasses) {
                    instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
                }
            }
            return instance;
        } catch (Throwable t) {
            throw new IllegalStateException("Extension instance(name: " + name + ", class: " +
                    type + ")  could not be instantiated: " + t.getMessage(), t);
        }
    }
    
    private T injectExtension(T instance) {
        try {
            if (objectFactory != null) {
                // 遍歷當前實例所有方法毯焕,判斷是否需要進行set屬性注入
                for (Method method : instance.getClass().getMethods()) {
                    if (method.getName().startsWith("set")
                            && method.getParameterTypes().length == 1
                            && Modifier.isPublic(method.getModifiers())) {
                        Class<?> pt = method.getParameterTypes()[0];
                        try {
                            String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
                            // 通過ExtensionFactory獲取被注入set屬性實例
                            Object object = objectFactory.getExtension(pt, property);
                            if (object != null) {
                                method.invoke(instance, object);
                            }
                        } catch (Exception e) {
                            logger.error("fail to inject via method " + method.getName()
                                    + " of interface " + type.getName() + ": " + e.getMessage(), e);
                        }
                    }
                }
            }
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
        return instance;
    }

這里有個injectExtension方法,其作用是:

如果當前擴展實例存在其他的擴展屬性磺樱,則通過反射調(diào)用其set方法設置擴展屬性纳猫。若該擴展屬性是適配器類型婆咸,也是通過ExtensionLoader獲取的。

所以芜辕,ExtensionLoader作為一個IOC插件容器尚骄,為dubbo的插件體系運作提供了保障,可以說是dubbo中的核心侵续。掌握了其基本原理乖仇,才有助于我們更好地分析dubbo源碼。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末询兴,一起剝皮案震驚了整個濱河市乃沙,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌诗舰,老刑警劉巖警儒,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異眶根,居然都是意外死亡蜀铲,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進店門属百,熙熙樓的掌柜王于貴愁眉苦臉地迎上來记劝,“玉大人,你說我怎么就攤上這事族扰⊙岢螅” “怎么了?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵渔呵,是天一觀的道長怒竿。 經(jīng)常有香客問我,道長扩氢,這世上最難降的妖魔是什么耕驰? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮录豺,結果婚禮上朦肘,老公的妹妹穿的比我還像新娘。我一直安慰自己双饥,他們只是感情好媒抠,可當我...
    茶點故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著兢哭,像睡著了一般领舰。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天冲秽,我揣著相機與錄音舍咖,去河邊找鬼。 笑死锉桑,一個胖子當著我的面吹牛排霉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播民轴,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼攻柠,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了后裸?” 一聲冷哼從身側響起瑰钮,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎微驶,沒想到半個月后浪谴,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡因苹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年苟耻,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片扶檐。...
    茶點故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡凶杖,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出款筑,到底是詐尸還是另有隱情智蝠,我是刑警寧澤,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布醋虏,位于F島的核電站寻咒,受9級特大地震影響哮翘,放射性物質(zhì)發(fā)生泄漏颈嚼。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一饭寺、第九天 我趴在偏房一處隱蔽的房頂上張望阻课。 院中可真熱鬧,春花似錦艰匙、人聲如沸限煞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽署驻。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間旺上,已是汗流浹背瓶蚂。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留宣吱,地道東北人窃这。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像征候,于是被迫代替她去往敵國和親杭攻。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,092評論 2 355

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

  • 一.概覽 整體描述 dubbo利用spi擴展機制實現(xiàn)大量的動態(tài)擴展疤坝,要想充分了解dubbo的擴展機制兆解,首先必須弄明...
    致慮閱讀 902評論 0 2
  • 前面我們了解過了Java的SPI擴展機制,對于Java擴展機制的原理以及優(yōu)缺點也有了大概的了解跑揉,這里繼續(xù)深入一下D...
    加大裝益達閱讀 5,062評論 2 20
  • dubbo的spi機制 dubbo的擴展點加載機制源自于java的spi擴展機制痪宰。那么,何為java的spi擴展機...
    安迪豬閱讀 607評論 0 1
  • Dubbo采用微內(nèi)核+插件體系畔裕,使得設計優(yōu)雅衣撬,擴展性強。那所謂的微內(nèi)核+插件體系是如何實現(xiàn)的呢扮饶!大家是否熟悉spi...
    carl_zhao閱讀 937評論 1 3
  • 總結:Protocolrefprotocol = ExtensionLoader.getExtensionLoad...
    Ngcc閱讀 705評論 0 2