Dubbo SPI機制

SPI簡介

站在一個框架作者的角度來說,定義一個接口暴拄,自己默認給出幾個接口的實現(xiàn)類,同時 允許框架的使用者也能夠自定義接口的實現(xiàn)。現(xiàn)在一個簡單的問題就是:如何優(yōu)雅的根據一個接口來獲取該接口的所有實現(xiàn)類呢廊鸥?
JDK SPI 正是為了優(yōu)雅解決這個問題而生,SPI 全稱為 (Service Provider Interface)辖所,即服務提供商接口惰说,是JDK內置的一種服務提供發(fā)現(xiàn)機制。目前有不少框架用它來做服務的擴展發(fā)現(xiàn)缘回,簡單來說吆视,它就是一種動態(tài)替換發(fā)現(xiàn)服務實現(xiàn)者的機制。

JDK SPI

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


public class SpiMain {

    public static void main(String[] args) {
        ServiceLoader<IHello> loaders = ServiceLoader.load(IHello.class);
        System.out.println(loaders);

        for (IHello hello : loaders) {
            System.out.println(hello.sayHello("mergades"));
        }
    }
}

ServiceLoader 源碼

讀取文件

 private static final String PREFIX = "META-INF/services/";
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;
        }

JDK SPI缺點

  • 雖然ServiceLoader也算是使用的延遲加載,但是基本只能通過遍歷全部獲取肆糕,也就是接口的實現(xiàn)類全部加載并實例化一遍般堆。如果你并不想用某些實現(xiàn)類,它也被加載并實例化了诚啃,這就造成了浪費淮摔。
    獲取某個實現(xiàn)類的方式不夠靈活,只能通過Iterator形式獲取始赎,不能根據某個參數(shù)來獲取對應的實現(xiàn)類和橙。
  • 多個并發(fā)多線程使用ServiceLoader類的實例是不安全的

Dubbo SPI擴展

Dubbo通過ExtensionLoader 來加載自定義的SPI服務仔燕。
Dubbo為了應對各種場景,所有的內部組件都是通過SPI的方式來管理的胃碾,

擴展功能

  • 方便獲取擴展實現(xiàn):JDK的SPI機制僅僅只能通過接口類名獲取所有實現(xiàn)涨享,而ExtensionLoader則通過接口類名和key值獲取一個實現(xiàn)。
  • IOC依賴注入功能仆百。Adaptive實現(xiàn)厕隧,就是生成一個代理類,這樣子就根據實際調用參數(shù)動態(tài)決定要調用的類俄周。
  • 使用裝飾器模式進行自動增強吁讨,自動包裝實現(xiàn)。

擴展源碼分析

ExtensinLoader初始化


/**
 * Protocol. (API/SPI, Singleton, ThreadSafe)
 */
@SPI("dubbo")
public interface Protocol {

    /**
     * Get default port when user doesn't config the port.
     *
     * @return default port
     */
    int getDefaultPort();

    /**
     * Export service for remote invocation: <br>
     * 1. Protocol should record request source address after receive a request:
     * RpcContext.getContext().setRemoteAddress();<br>
     * 2. export() must be idempotent, that is, there's no difference between invoking once and invoking twice when
     * export the same URL<br>
     * 3. Invoker instance is passed in by the framework, protocol needs not to care <br>
     *
     * @param <T>     Service type
     * @param invoker Service invoker
     * @return exporter reference for exported service, useful for unexport the service later
     * @throws RpcException thrown when error occurs during export the service, for example: port is occupied
     */
    @Adaptive
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;

    /**
     * Refer a remote service: <br>
     * 1. When user calls `invoke()` method of `Invoker` object which's returned from `refer()` call, the protocol
     * needs to correspondingly execute `invoke()` method of `Invoker` object <br>
     * 2. It's protocol's responsibility to implement `Invoker` which's returned from `refer()`. Generally speaking,
     * protocol sends remote request in the `Invoker` implementation. <br>
     * 3. When there's check=false set in URL, the implementation must not throw exception but try to recover when
     * connection fails.
     *
     * @param <T>  Service type
     * @param type Service class
     * @param url  URL address for the remote service
     * @return invoker service's local proxy
     * @throws RpcException when there's any error while connecting to the service provider
     */
    @Adaptive
    <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;

    /**
     * Destroy protocol: <br>
     * 1. Cancel all services this protocol exports and refers <br>
     * 2. Release all occupied resources, for example: connection, port, etc. <br>
     * 3. Protocol can continue to export and refer new service even after it's destroyed.
     */
    void destroy();

}

使用實例

ExtensionLoader<Protocol> protocolLoader = ExtensionLoader.getExtensionLoader(Protocol.class);
Protocol dubboProtocol = protocolLoader.getExtension(DubboProtocol.NAME);

可以參考dubbo源碼單元測試 com.alibaba.dubbo.common.extensionloader.ExtensionLoaderTest

    @Test
    public void test_getDefaultExtension() throws Exception {
        SimpleExt ext = ExtensionLoader.getExtensionLoader(SimpleExt.class).getDefaultExtension();
        assertThat(ext, instanceOf(SimpleExtImpl1.class));

        String name = ExtensionLoader.getExtensionLoader(SimpleExt.class).getDefaultExtensionName();
        assertEquals("impl1", name);

        SimpleExt simpleExt = ExtensionLoader.getExtensionLoader(SimpleExt.class).getExtension("impl2");
        assertThat(simpleExt, instanceOf(SimpleExtImpl2.class));
    }

如上單元測試可以看到峦朗,可以通過ExtensionLoader.getExtensionLoader(SimpleExt.class).getExtension("impl2"); name來加載不同的擴展建丧,相比于JDK SPI機制,方便很多波势。

通過

  • ExtensionLoader.getExtensionLoader(Protocol.class) 獲取對應的ExtensionLoader 實例
  • protocolLoader.getExtension(DubboProtocol.NAME):根據Key獲取相應的擴展實現(xiàn)類實例

配置文件掃描

Dubbo默認依次掃描META-INF/dubbo/internal/翎朱、META-INF/dubbo/、META-INF/services/三個classpath目錄下的配置文件尺铣。配置文件以具體擴展接口全名命名拴曲,如:com.alibaba.dubbo.rpc.Filter

如下配置

# Comment 1
impl1=com.alibaba.dubbo.common.extensionloader.ext1.impl.SimpleExtImpl1#Hello World
impl2=com.alibaba.dubbo.common.extensionloader.ext1.impl.SimpleExtImpl2  # Comment 2
impl3=com.alibaba.dubbo.common.extensionloader.ext1.impl.SimpleExtImpl3 # with head space

ExtensionLoader#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;
    }
    private Map<String, Class<?>> loadExtensionClasses() {
        final SPI defaultAnnotation = type.getAnnotation(SPI.class);
        if (defaultAnnotation != null) {
            String value = defaultAnnotation.value();
            if ((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<?>>();
        loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
        loadDirectory(extensionClasses, DUBBO_DIRECTORY);
        loadDirectory(extensionClasses, SERVICES_DIRECTORY);
        return extensionClasses;
    }

擴展適配器

在dubbo擴展中,適配器模式被廣泛應用凛忿,其作用在于為同一個接口下面不同的多個實現(xiàn)的調用路由功能澈灼,如制定優(yōu)先級,dubbo提供靜態(tài)適配和動態(tài)適配器店溢。

靜態(tài)適配器

/**
 * AdaptiveCompiler. (SPI, Singleton, ThreadSafe)
 */
@Adaptive
public class AdaptiveCompiler implements Compiler {

    private static volatile String DEFAULT_COMPILER;

    public static void setDefaultCompiler(String compiler) {
        DEFAULT_COMPILER = compiler;
    }

    @Override
    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);
    }

}

動態(tài)適配器

動態(tài)適配器擴展即通過動態(tài)代理生成的動態(tài)代理類叁熔,在dubbo中通過javassist技術生成。
從ExtensionLoader構造器中會調用getAdaptiveExtension()方法觸發(fā)為當前擴展類型生成適配器

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

ExtensionFactory

@SPI
public interface ExtensionFactory {

    /**
     * Get extension.
     *
     * @param type object type.
     * @param name object name.
     * @return object instance.
     */
    <T> T getExtension(Class<T> type, String name);

}

初始化 ExtensionFactory 實例床牧,如果非ExtensionFactory 荣回,則直接獲取getAdaptiveExtension()

@SuppressWarnings("unchecked")
    public T getAdaptiveExtension() {
        Object instance = cachedAdaptiveInstance.get();
        if (instance == null) {
            if (createAdaptiveInstanceError == null) {
                synchronized (cachedAdaptiveInstance) {
                    instance = cachedAdaptiveInstance.get();
                    if (instance == null) {
                        try {
                            instance = createAdaptiveExtension();
                            cachedAdaptiveInstance.set(instance);
                        } catch (Throwable t) {
                            createAdaptiveInstanceError = t;
                            throw new IllegalStateException("Failed to create adaptive instance: " + t.toString(), t);
                        }
                    }
                }
            } else {
                throw new IllegalStateException("Failed to create adaptive instance: " + createAdaptiveInstanceError.toString(), createAdaptiveInstanceError);
            }
        }

        return (T) instance;
    }
    private T createAdaptiveExtension() {
        try {
            return injectExtension((T) getAdaptiveExtensionClass().newInstance());
        } catch (Exception e) {
            throw new IllegalStateException("Can't create adaptive extension " + type + ", cause: " + e.getMessage(), e);
        }
    }

第一步獲取getAdaptiveExtensionClass

    private Class<?> getAdaptiveExtensionClass() {
        getExtensionClasses();
        if (cachedAdaptiveClass != null) {
            return cachedAdaptiveClass;
        }
        return cachedAdaptiveClass = createAdaptiveExtensionClass();
    }

    private Class<?> createAdaptiveExtensionClass() {
        String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
        ClassLoader classLoader = findClassLoader();
        org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
        return compiler.compile(code, classLoader);
    }

getExtensionClasses中,再繼續(xù)調用loadExtensionClasses架子啊默認的擴展機制戈咳。
ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension() 中dubbo底層會動態(tài)編譯驹马,生成。
Protocol產生的AdaptiveExtension如下:

package com.alibaba.dubbo.rpc;

import com.alibaba.dubbo.common.extension.ExtensionLoader;


public class Protocol$Adaptive implements com.alibaba.dubbo.rpc.Protocol {
    public void destroy() {
        throw new UnsupportedOperationException(
            "method public abstract void com.alibaba.dubbo.rpc.Protocol.destroy() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
    }

    public int getDefaultPort() {
        throw new UnsupportedOperationException(
            "method public abstract int com.alibaba.dubbo.rpc.Protocol.getDefaultPort() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
    }

    public com.alibaba.dubbo.rpc.Exporter export( com.alibaba.dubbo.rpc.Invoker arg0)
        throws com.alibaba.dubbo.rpc.RpcException {
        if (arg0 == null) {
            throw new IllegalArgumentException(
                "com.alibaba.dubbo.rpc.Invoker argument == null");
        }

        if (arg0.getUrl() == null) {
            throw new IllegalArgumentException(
                "com.alibaba.dubbo.rpc.Invoker argument getUrl() == null");
        }

        com.alibaba.dubbo.common.URL url = arg0.getUrl();
        String extName = ((url.getProtocol() == null) ? "dubbo"
                                                      : url.getProtocol());

        if (extName == null) {
            throw new IllegalStateException(
                "Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" +
                url.toString() + ") use keys([protocol])");
        }

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

        return extension.export(arg0);
    }

    public com.alibaba.dubbo.rpc.Invoker refer(java.lang.Class arg0,
        com.alibaba.dubbo.common.URL arg1)
        throws com.alibaba.dubbo.rpc.RpcException {
        if (arg1 == null) {
            throw new IllegalArgumentException("url == null");
        }

        com.alibaba.dubbo.common.URL url = arg1;
        String extName = ((url.getProtocol() == null) ? "dubbo"
                                                      : url.getProtocol());

        if (extName == null) {
            throw new IllegalStateException(
                "Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" +
                url.toString() + ") use keys([protocol])");
        }

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

        return extension.refer(arg0, arg1);
    }
}

ExtensionFactory 類似Spring的IOC作用除秀,ExtensionLoader#injectExtension 主要完成IOC功能糯累。

 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, clazz.newInstance());
                instance = (T) EXTENSION_INSTANCES.get(clazz);
            }
            injectExtension(instance);
            Set<Class<?>> wrapperClasses = cachedWrapperClasses;
            if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
                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) {
                for (Method method : instance.getClass().getMethods()) {
                    if (method.getName().startsWith("set")
                            && method.getParameterTypes().length == 1
                            && Modifier.isPublic(method.getModifiers())) {
                        /**
                         * Check {@link DisableInject} to see if we need auto injection for this property
                         */
                        if (method.getAnnotation(DisableInject.class) != null) {
                            continue;
                        }
                        Class<?> pt = method.getParameterTypes()[0];
                        try {
                            String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
                            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;
    }

由以下代碼完成真正的注入:

 Object object = objectFactory.getExtension(pt, property);

AdaptiveExtensionFactory實例中的factories的size返回應為2,里面只會保存這兩個類實例:

spring=com.alibaba.dubbo.config.spring.extension.SpringExtensionFactory
spi=com.alibaba.dubbo.common.extension.factory.SpiExtensionFactory

因為adaptive=com.alibaba.dubbo.common.extension.factory.AdaptiveExtensionFactory是保存在cachedAdaptiveClass上的

    public AdaptiveExtensionFactory() {
        ExtensionLoader<ExtensionFactory> loader = ExtensionLoader.getExtensionLoader(ExtensionFactory.class);
        List<ExtensionFactory> list = new ArrayList<ExtensionFactory>();
        for (String name : loader.getSupportedExtensions()) {
            list.add(loader.getExtension(name));
        }
        factories = Collections.unmodifiableList(list);
    }

其中册踩,SpringExtensionFactory 中泳姐,會更具Spring的ApplicationContext 來獲取對應的bean實例。

SpringExtensionFactory

 public <T> T getExtension(Class<T> type, String name) {
        for (ApplicationContext context : contexts) {
            if (context.containsBean(name)) {
                Object bean = context.getBean(name);
                if (type.isInstance(bean)) {
                    return (T) bean;
                }
            }
        }

        logger.warn("No spring extension (bean) named:" + name + ", try to find an extension (bean) of type " + type.getName());

        if (Object.class == type) {
            return null;
        }

        for (ApplicationContext context : contexts) {
            try {
                return context.getBean(type);
            } catch (NoUniqueBeanDefinitionException multiBeanExe) {
                logger.warn("Find more than 1 spring extensions (beans) of type " + type.getName() + ", will stop auto injection. Please make sure you have specified the concrete parameter type and there's only one extension of that type.");
            } catch (NoSuchBeanDefinitionException noBeanExe) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Error when get spring extension(bean) for type:" + type.getName(), noBeanExe);
                }
            }
        }

        logger.warn("No spring extension (bean) named:" + name + ", type:" + type.getName() + " found, stop get bean.");

        return null;
    }

直接從Spring容器中獲取對應的bean

SpiExtensionFactory


    @Override
    public <T> T getExtension(Class<T> type, String name) {
        if (type.isInterface() && type.isAnnotationPresent(SPI.class)) {
            ExtensionLoader<T> loader = ExtensionLoader.getExtensionLoader(type);
            if (!loader.getSupportedExtensions().isEmpty()) {
                return loader.getAdaptiveExtension();
            }
        }
        return null;
    }

最終還有由 loader.getAdaptiveExtension() 完成暂吉。

AdaptiveExtensionFactory 作用

(1)就是只有當相應的SPI接口的所有方法上是否有帶Adaptive注解的方法胖秒,如果有就會生成動態(tài)類的代碼然后進行動態(tài)編譯(比如使用javassist框架),如果沒有帶Adaptive注解的方法 ,那就說明該SPI接口是沒有Adaptive性質的實現(xiàn)類的缎患,就會拋出異常
(2)動態(tài)類的本質也是在實現(xiàn)相應的SPI接口,它最終也是在調一個現(xiàn)成的SPI實現(xiàn)類來工作,這樣就會有這樣的疑問阎肝,那為何不直接指定呢挤渔,還非得用動態(tài)的呢,這就是為什么凡是在方法上出現(xiàn)Adaptive注解的SPI的Adaptive形式都要動態(tài)的原因了风题,因為dubbo這樣一來就可以做到用不同的Adaptive方法判导,調不同的SPI實現(xiàn)類去處理。

SPI的應用

37430218DFC22DC2B1BB2E691473DE26F.jpg

具體文件內容

dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol

配置了一個鍵值對沛硅,key為dubbo,值為org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol眼刃,在其它幾個子包下,也有名稱叫做org.apache.dubbo.rpc.Protocol的配置文件摇肌,說明Protocol插口有幾個對應的插件

可以猜測一下擂红,當<dubbo:protocol/>僅僅配置了name="dubbo",port="20880"時围小,會加載哪一個協(xié)議插件呢昵骤,根據名稱,可以猜測肯适,加載的DubboProtocol插件涉茧。那dubbo是怎樣做到的呢,我們來一探究竟疹娶。

三個注解

  • SPI:這個注解使用在接口上,標識接口是否是extension(擴展或插口)伦连,可以接收一個默認的extension名稱
  • Adaptive: 這個注解可以使用在類或方法上雨饺,決定加載哪一個extension,值為字符串數(shù)組,數(shù)組中的字符串是key值惑淳,比如new String[]{"key1","key2"};先在URL中尋找key1的值额港,如果找到,則使用此值加載extension歧焦,如果key1沒有移斩,則尋找key2的值,如果key2也沒有绢馍,則使用接口SPI注解的值向瓷,如果接口SPI注解,沒有配置默認值舰涌,則將接口名按照首字母大寫分成多個部分猖任,然后以'.'分隔,例如org.apache.dubbo.xxx.YyyInvokerWrapper接口名會變成yyy.invoker.wrapper瓷耙,然后以此名稱做為key到URL尋找朱躺,如果仍沒有找到刁赖,則拋出IllegalStateException異常;Adaptive注解用在類上长搀,表示此類是它實現(xiàn)接口(插口)的自適應插件
  • Activate:這個注解可以使用在類或方法上宇弛,用以根據URL的key值判斷當前extension是否生效,當一個extension有多個實現(xiàn)時源请,可以加載特定的extension實現(xiàn)類枪芒,例如extension實現(xiàn)類上有注解@Activate("cache, validation"),則當URL上出現(xiàn)"cache”或“validation" key時巢钓,當前extension才會生效

ExtensionLoader#getAdaptiveExtensionClass

    private Class<?> getAdaptiveExtensionClass() {
        getExtensionClasses(); //獲取所有插件
        if (cachedAdaptiveClass != null) {
            return cachedAdaptiveClass; //存在插件則返回
        }
        return cachedAdaptiveClass = createAdaptiveExtensionClass();//創(chuàng)建
    }

dubbo啟動時病苗,加載所有的SPI注解的插件,最終根據我們指定的name="dubbo"來加載對應的dubbo協(xié)議症汹。

參考 http://dubbo.apache.org/zh-cn/docs/source_code_guide/dubbo-spi.html

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末硫朦,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子背镇,更是在濱河造成了極大的恐慌咬展,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瞒斩,死亡現(xiàn)場離奇詭異破婆,居然都是意外死亡,警方通過查閱死者的電腦和手機胸囱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進店門祷舀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人烹笔,你說我怎么就攤上這事裳扯。” “怎么了谤职?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵饰豺,是天一觀的道長。 經常有香客問我允蜈,道長冤吨,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任饶套,我火速辦了婚禮漩蟆,結果婚禮上,老公的妹妹穿的比我還像新娘妓蛮。我一直安慰自己爆安,他們只是感情好,可當我...
    茶點故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著扔仓,像睡著了一般褐奥。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上翘簇,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天撬码,我揣著相機與錄音,去河邊找鬼版保。 笑死呜笑,一個胖子當著我的面吹牛,可吹牛的內容都是我干的彻犁。 我是一名探鬼主播叫胁,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼汞幢,長吁一口氣:“原來是場噩夢啊……” “哼驼鹅!你這毒婦竟也來了?” 一聲冷哼從身側響起森篷,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤买乃,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后功戚,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡茂浮,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年双谆,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片席揽。...
    茶點故事閱讀 38,577評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡顽馋,死狀恐怖,靈堂內的尸體忽然破棺而出幌羞,到底是詐尸還是另有隱情寸谜,我是刑警寧澤,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布属桦,位于F島的核電站熊痴,受9級特大地震影響他爸,放射性物質發(fā)生泄漏。R本人自食惡果不足惜果善,卻給世界環(huán)境...
    茶點故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一诊笤、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧巾陕,春花似錦讨跟、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至梯刚,卻和暖如春凉馆,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背乾巧。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工句喜, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人沟于。 一個月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓咳胃,卻偏偏與公主長得像,于是被迫代替她去往敵國和親旷太。 傳聞我的和親對象是個殘疾皇子展懈,可洞房花燭夜當晚...
    茶點故事閱讀 43,452評論 2 348