Dubbo的SPI机制

本系列参考官网文档、芋道源码的源码解读和《深入理解Apache Dubbo与实战》一书。阅读本文前可以参考阅读下面链接里的内容。Dubbo版本为2.6.1。
JavaSPI机制简介
Dubbo官网文档—扩展点加载

文章内容顺序:
1.Dubbo 自己实现了一套 SPI 机制,而不是使用 Java 标准的 SPI 。为什么要自己实现?
2.@SPI简单介绍
3.Dubbo SPI机制是怎么实现加载用到的类,而不是像JDK SPI中加载全部?
4.Dubbo SPI 机制是怎么实现AOP的?
5.如果一个扩展接口有多个包装类,怎么办?比如Protocol这个扩展点,他有两个Wrapper,该怎么生成?
6.那么Dubbo SPI是怎么实现IOC的呢?
7.如果扩展接口有多个实现类,具体注入哪个呢?引出@Adaptive
8.@Adaptive介绍,包括入口方法到最后的链路调用,样例代码的生成。
9.为什么源码里只有AdaptiveExtensionFactory和AdaptiveCompiler注解了?
10.为什么AdaptiveCompiler这个类是固定已知的?包括这个类的源码介绍
11.为什么AdaptiveExtensionFactory这个类是固定已知的?包括这个类的源码介绍
12@Activate注解用途,入口方法介绍

1.Dubbo 自己实现了一套 SPI 机制,而不是使用 Java 标准的 SPI 。为什么要自己实现?

  • 1.Dubbo 有很多的拓展点,例如 Protocol、Filter 等等。并且每个拓展点有多种的实现,例如 Protocol 有 DubboProtocol、InjvmProtocol、RestProtocol 等等。那么使用 JDK SPI 机制,会初始化无用的拓展点及其实现,造成不必要的耗时与资源浪费。
  • 2.Dubbo解决了JDK SPI机制吞异常的机制。
    例如:如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK 标准的 ScriptEngine,通过 getName() 获取脚本类型的名称,但如果 RubyScriptEngine 因为所依赖的 jruby.jar 不存在,导致 RubyScriptEngine 类加载失败,这个失败原因被吃掉了,和 ruby 对应不起来,当用户执行 ruby 脚本时,会报不支持 ruby,而不是真正失败的原因。
  • 3.增加了对扩展点 IoC(留待下面说明) 和 AOP (通过wapper实现,装饰器设计模式)的支持,一个扩展点可以直接 setter 注入其它扩展点。

这种扩展机制有三个注解,分别是@SPI、@Adaptive 、@Activate。我们将会将SPI、Adaptive放一块讲,Activate单独拎出来讲。

这些机制主要在com.alibaba.dubbo.common.extension.ExtensionLoader类中。
类主要方法和功能如下


image.png

2.@SPI

@SPI 主要用于标记该接口是SPI接口,即扩展点,可以有多个不同内置或用户定义的实现,下面是Dubbo的一个Protocol接口

@SPI("dubbo")
public interface Protocol {
    // ... 省略代码
}

他的扩展点如下:


image.png

上面的是Protocol的接口,@SPI整个注解的意义是用来说明这个是扩展类,并且默认的实现是DubboProtocol,可以通过修改XML文件中的来进行配置。

<dubbo:protocol name="dubbo" port="20800" />

3.Dubbo SPI机制是怎么实现加载用到的类,而不是像JDK SPI中加载全部?

image.png

这是 SPI 的配置目录,通过这个文件来加载SPI的加载对象。

而一个拓展( 拓展接口 )对应一个 ExtensionLoader 对象。例如,Protocol 和 Filter 分别对应一个 ExtensionLoader 中的对象。如下图红框所示


image.png

一个拓展接口通过其 ExtensionLoader 对象,加载它的拓展实现们。我们会发现多个属性都是 “cached“ 开头(图里只展示了一点)。ExtensionLoader 考虑到性能和资源的优化,读取拓展配置后,会首先进行缓存。等到 Dubbo 代码真正用到对应的拓展实现时,进行拓展实现的对象的初始化。并且,初始化完成后,也会进行缓存。也就是说:

  • (1).缓存加载的拓展配置
  • (2).缓存创建的拓展实现对象
    通过这种方式,就不用一开始加载就初始化所有对象,而是等需要他们的时候再初始化。

4.Dubbo SPI 机制是怎么实现AOP的?

com.alibaba.dubbo.common.extension包下的ExtensionLoader类中的loadFile()实现(方法过长,就不贴了),此方法用来加载配置文件,其判断加载到的扩展点有拷贝构造函数,则判定为扩展点 Wrapper 类。如下代码所示
Wrapper 类同样实现了扩展点接口(Protocol),但是 Wrapper 不是扩展点的真正实现。
通过 Wrapper 类可以把所有扩展点公共逻辑移至 Wrapper 中。新加的 Wrapper 在所有的扩展点上添加了逻辑,有些类似 AOP,即 Wrapper 代理了扩展点。
这里先卖个关子,也就是他的IOC流程,我们专注于他AOP的实现。

package com.alibaba.xxx;
 
import org.apache.dubbo.rpc.Protocol;
 
public class XxxProtocolWrapper implements Protocol {
    Protocol impl;
 
    public XxxProtocolWrapper(Protocol protocol) { impl = protocol; }
 
    // 接口方法做一个操作后,再调用extension的方法
    public void refer() {
        //... 一些操作
        impl.refer();
        // ... 一些操作
    }
 
    // ...
}

5.如果一个扩展接口有多个包装类,怎么办?比如Protocol这个扩展点,他有两个Wrapper,该怎么生成?

比如截取的这个Protocol,就有两个包装类。


image.png

ExtensionLoader#createExtension方法如下:

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);
            // 创建 Wrapper 拓展对象
            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);
        }
    }

注意上述代码中的:

 // 创建 Wrapper 拓展对象
            Set<Class<?>> wrapperClasses = cachedWrapperClasses;
            if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
                for (Class<?> wrapperClass : wrapperClasses) {
                    instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
                }
            }

这里的代码就很好理解了,简单来说就是——套娃。
instance在一开始是我们的InjvmProtocol
cachedWrapperClasses存储了他的所有我们wrapperClasses ,即两个包装类ProtocolFilterWrappeProtocolListenerWrapper
按照For循环取出来的顺序,第一次循环先把InjvmProtocol包装进ProtocolFilterWrappe,第二此循环的时候instance 已经是ProtocolFilterWrappe了,此时再把ProtocolFilterWrappe包装进ProtocolListenerWrapper最后返回,大功告成。

6.那么Dubbo SPI是怎么实现IOC的呢?

createExtension()(用以创建扩展类的方法)时会调用injectExtension()来注入属性

 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())) { // setting && public 方法
                        // 获得属性的类型
                        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;
    }

如代码所示,在injectExtension方法中,会判断这个类中是否有setter方法,如果有的话会通过ExtensionFactory自动注入对应的扩展点实例。

这个时候就出现问题了:
  • 1.如果扩展接口有多个实现类,具体注入哪个呢?
  • 2.ExtensionFactory是什么,怎么注入的?
    这个时候就需要另一个注解出场了:@Adaptive

@Adaptive

参考链接:
@Adaptive注解
Dubbo SPI之Adaptive详解

  1. 注解在类上时,直接使用被注解的类。也因此,一个拓展,只允许最多注解一个类(可以存在多个类,但是只能注解一个),否则会存在多个会是冲突。
    这里多提一嘴,整个Dubbo源码中用在类上的@Adaptive只有AdaptiveExtensionFactoryAdaptiveCompiler
  2. 注解在方法上时,代表自动生成和编译一个动态的Adpative类,它主要是用于SPI机制,因为spi的类是不固定、未知的扩展类,所以设计了动态$Adaptive类.
    例如 Protocol的spi类有 injvm dubbo registry filter listener等等 很多扩展未知类,
    而实现这种机制的入口主要在ExtensionLoader.getAdaptiveExtension()方法
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("fail to create adaptive instance: " + t.toString(), t);
                        }
                    }
                }
            // 若之前创建报错,则抛出异常 IllegalStateException
            } else {
                throw new IllegalStateException("fail to create adaptive instance: " + createAdaptiveInstanceError.toString(), createAdaptiveInstanceError);
            }
        }
        return (T) instance;
    }

当缓存不存在时,调用 #createAdaptiveExtension()方法,创建自适应拓展对象,并添加到 cachedAdaptiveInstance 中。

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

调用#getAdaptiveExtensionClass()方法,获得自适应拓展类。
调用Class#newInstance() 方法,创建自适应拓展对象。
调用 #injectExtension(instance)方法,向创建的自适应拓展对象,注入依赖的属性。

那我们再来看看getAdaptiveExtensionClass()

    private Class<?> getAdaptiveExtensionClass() {
        /**
         * 这边深入下去会用到loadFile(),也就是加载在配置文件里的类,
        如果有AdaptiveClass,会把AdaptiveClass缓存到cachedAdaptiveClass 
         */
        getExtensionClasses();
        /**
         * 如果通过上面的步骤可以获取到cachedAdaptiveClass直接返回,
        如果不行的话,就得考虑自己进行利用动态代理创建一个了
         */
        if (cachedAdaptiveClass != null) {
            return cachedAdaptiveClass;
        }
        /**
         * 利用动态代理创建一个扩展类
         */
        return cachedAdaptiveClass = createAdaptiveExtensionClass();
    }

先来看看loadFile()做了啥
在loadFile()方法中,会有判断类是否有@Adaptive注解,节选如下

                   /**
                      * 判断这个加载的类上,有没有Adaptive的注解,如果有
                      */
                         if (clazz.isAnnotationPresent(Adaptive.class)) {
                          if (cachedAdaptiveClass == null) {
                         /**
                           * 将此类作为cachedAdaptiveClass
                              */
                               cachedAdaptiveClass = clazz;
                          }
                               /**
                            * 多个adaptive类的实例,报错
                             */
                           else if (!cachedAdaptiveClass.equals(clazz)) {
                 throw new IllegalStateException("More than 1 adaptive class found: "
                                           + cachedAdaptiveClass.getClass().getName()
                                    + ", " + clazz.getClass().getName());
                                  }
                      }
                 /**
                   * 如果这个类,没有Adaptive注解
                     */
                    else {
                      ……
                          }

如果有的类上带有@Adaptive注解,那么将这个类赋值给cachedAdaptiveClass,如果没有,那么在else里面就会进入是否是wapper(包装类)的判定。

也就是说
如果类上没有@Adaptive注解,此时getAdaptiveExtensionClass() 中的
if (cachedAdaptiveClass != null)也为空,那么就要进入createAdaptiveExtensionClass()了。

private Class<?> createAdaptiveExtensionClass() {
        // 自动生成自适应拓展的代码实现的字符串
        String code = createAdaptiveExtensionClassCode();
        // 编译代码,并返回该类
        ClassLoader classLoader = findClassLoader();
        com.alibaba.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
        return compiler.compile(code, classLoader);
    }

思路很简单,将类以字符串的形式拼接出来,然后利用编译器进行编译,返回编译后的class对象。寻找编译器的过程和具体编译的过程不是我们此次所要关心的,我们关心的是这个createAdaptiveExtensionClassCode方法创建的字符串格式的数据是啥样的,用到了哪些数据。

样例代码如下:

@SPI("dubbo")
public interface AdaptiveExt2 {   
    @Adaptive({"t"})
    String echo(String msg, URL url);
}
    @Test
    public void test1() {
        ExtensionLoader<AdaptiveExt2> loader = ExtensionLoader.getExtensionLoader(AdaptiveExt2.class);
        AdaptiveExt2 adaptiveExtension = loader.getAdaptiveExtension();
        URL url = URL.valueOf("test://localhost/test?t=cloud");
        System.out.println(adaptiveExtension.echo("d", url));
    }
cloud

编译后代码如下:

package shuqi.dubbotest.d;

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

public class AdaptiveExt2$Adpative implements shuqi.dubbotest.spi.adaptive.AdaptiveExt2 {
    public java.lang.String echo(java.lang.String arg0, com.alibaba.dubbo.common.URL arg1) {
        if (arg1 == null) throw new IllegalArgumentException("url == null");
        com.alibaba.dubbo.common.URL url = arg1;
        String extName = url.getParameter("t", "dubbo");
        if (extName == null)
            throw new IllegalStateException("Fail to get extension(shuqi.dubbotest.spi.adaptive.AdaptiveExt2) name from url(" + url.toString() + ") use keys([t])");
        shuqi.dubbotest.spi.adaptive.AdaptiveExt2 extension = (shuqi.dubbotest.spi.adaptive.AdaptiveExt2) ExtensionLoader.getExtensionLoader(shuqi.dubbotest.spi.adaptive.AdaptiveExt2.class).getExtension(extName);
        return extension.echo(arg0, arg1);
    }
}

分析:
@Adaptive中,value 是个字符数组,通过该属性从 URL 中获取扩展名,来决定使用哪个扩展。分为几种情况:

  • 1.设置了 value,且从 URL 中找到了对应的扩展名,则使用该扩展;

  • 2.设置了 value,但从 URL 中找不到扩展名,则使用默认的扩展,即 @SPI 中配置的 value(在此例中为dubbo),还是找不到则抛出异常;

  • 3.未设置 value,则根据接口名生成 value,比如接口 YyyInvokerWrapper 生成 value = “yyy.invoker.wrapper”。

注意下面代码最后传入的extName,这个才是从url中获得的value,通过这个字符串从ExtensionLoader找到对应的扩展类,再通过这个扩展类来执行相应的操作,其他无非是一些判空操作。url.getParameter("t", "dubbo");也只是如果url中有key为t对应的value,则返回,否则返回dubbo。

shuqi.dubbotest.spi.adaptive.AdaptiveExt2 extension =
(shuqi.dubbotest.spi.adaptive.AdaptiveExt2) ExtensionLoader.getExtensionLoader(shuqi.dubbotest.spi.adaptive.AdaptiveExt2.class). (extName);

将上面生成的字符串编译成Class对象,作为适配器类,返回,然后实例化后,进行依赖注入需要的属性,随后缓存,备下次使用。

9.为什么源码里只有AdaptiveExtensionFactory和AdaptiveCompiler注解了@Adaptive?

此种情况,表示拓展的加载逻辑已经被确定下来了,一般不再进行除此以外额外的拓展。更多时候,Adaptive 是注解在接口方法上的,表示拓展的加载逻辑需由框架自动生成(生成代码后编译来生成新的类)。

10.为什么AdaptiveCompiler这个类是固定已知的?

因为整个框架仅支持Javassist和JdkCompiler。直接固定了对应的实现而不需要动态生成代码实现,就像策略模式直接确定实现类。在扩展点初始化时,如果发现实现类上有@Adaptive注解,会直接赋值给cachedAdaptiveClass,并把实例缓存到cachedAdaptiveInstance中。

他的UML图如下,

image.png

AdaptiveCompiler中只是作为一个管理作用,用来管理其他两个真正的Compiler,他的代码也非常简单。

@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;
        // 获得 Compiler 的 ExtensionLoader 对象。
        ExtensionLoader<Compiler> loader = ExtensionLoader.getExtensionLoader(Compiler.class);
        String name = DEFAULT_COMPILER; // copy reference
        // 使用设置的拓展名,获得 Compiler 拓展对象
        if (name != null && name.length() > 0) {
            compiler = loader.getExtension(name);
        // 获得默认的 Compiler 拓展对象
        } else {
            compiler = loader.getDefaultExtension();
        }
        // 编译类
        return compiler.compile(code, classLoader);
    }

}

setDefaultCompiler(compiler) 静态方法,设置默认编辑器的拓展名( DEFAULT_COMPILER )。该方法被 ApplicationConfig#setCompiler(compiler) 方法调用,代码如下:

 public void setCompiler(String compiler) {
        this.compiler = compiler;
        AdaptiveCompiler.setDefaultCompiler(compiler);
    }

在 <dubbo:application compiler="" /> 配置下,即可触发该方法。
如果没有配置,则使用接口上的@SPI("javassist")来进行

11.为什么AdaptiveExtensionFactory这个类是固定已知的?

同样的,因为整个框架仅支持2个objFactory,一个是spi,另一个是spring


image.png

AdaptiveExtensionFactory与上文的AdaptiveCompiler类似,都是用来管理其他两个扩展类的,在这个AdaptiveExtensionFactory里直接就加载了所有的ExtensionFactory实现并缓存起来,SpingExtensionFactory提供了保存Spring上下文的静态方法,SpiExtensionFactory可以从Dubbo 容器中获取扩展点实例,这样就打通了Spring容器与SPI容器,注入会从这两个容器里找。

AdaptiveExtensionFactory代码如下:

@Adaptive
public class AdaptiveExtensionFactory implements ExtensionFactory {

    /**
     * ExtensionFactory 拓展对象集合
     */
    private final List<ExtensionFactory> factories;

    public AdaptiveExtensionFactory() {
        // 使用 ExtensionLoader 加载拓展对象实现类。
        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);
    }

    public <T> T getExtension(Class<T> type, String name) {
        // 遍历工厂数组,直到获得到属性
        for (ExtensionFactory factory : factories) {
            T extension = factory.getExtension(type, name);
            if (extension != null) {
                return extension;
            }
        }
        return null;
    }

}

12.@Activate

Activate主要用在有多个扩展点实现,需要根据不同条件激活的场景中。
比如Filter需要多个同时激活,因为每个Filter实现的是不同的功能。

    public List<T> getActivateExtension(URL url, String[] values, String group) {
        List<T> exts = new ArrayList<T>();
        List<String> names = values == null ? new ArrayList<String>(0) : Arrays.asList(values);
        // 处理自动激活的拓展对象们
        // 判断不存在配置 `"-name"` 。例如,<dubbo:service filter="-default" /> ,代表移除所有默认过滤器。
        if (!names.contains(Constants.REMOVE_VALUE_PREFIX + Constants.DEFAULT_KEY)) {
            // 获得拓展实现类数组
            getExtensionClasses();
            // 循环
            for (Map.Entry<String, Activate> entry : cachedActivates.entrySet()) {
                String name = entry.getKey();
                Activate activate = entry.getValue();
                if (isMatchGroup(group, activate.group())) { // 匹配分组
                    // 获得拓展对象
                    T ext = getExtension(name);
                    if (!names.contains(name) // 不包含在自定义配置里。如果包含,会在下面的代码处理。
                            && !names.contains(Constants.REMOVE_VALUE_PREFIX + name) // 判断是否配置移除。例如 <dubbo:service filter="-monitor" />,则 MonitorFilter 会被移除
                            && isActive(activate, url)) { // 判断是否激活
                        exts.add(ext);
                    }
                }
            }
            // 排序
            Collections.sort(exts, ActivateComparator.COMPARATOR);
        }
        // 处理自定义配置的拓展对象们。例如在 <dubbo:service filter="demo" /> ,代表需要加入 DemoFilter (这个是笔者自定义的)。
        List<T> usrs = new ArrayList<T>();
        for (int i = 0; i < names.size(); i++) {
            String name = names.get(i);
            if (!name.startsWith(Constants.REMOVE_VALUE_PREFIX) && !names.contains(Constants.REMOVE_VALUE_PREFIX + name)) { // 判断非移除的
                // 将配置的自定义在自动激活的拓展对象们前面。例如,<dubbo:service filter="demo,default,demo2" /> ,则 DemoFilter 就会放在默认的过滤器前面。
                if (Constants.DEFAULT_KEY.equals(name)) {
                    if (!usrs.isEmpty()) {
                        exts.addAll(0, usrs);
                        usrs.clear();
                    }
                } else {
                    // 获得拓展对象
                    T ext = getExtension(name);
                    usrs.add(ext);
                }
            }
        }
        // 添加到结果集
        if (!usrs.isEmpty()) {
            exts.addAll(usrs);
        }
        return exts;
    }

从源码可以看到,主要流程分为四步

  • 1、检查缓存,如果缓存没有,就初始化所有扩展类实现的集合
  • 2、遍历整个@Activate集合,根据URL的匹配条件,得到所有符合激活条件的扩展类实现。然后根据@Activate中配置的before、after、order参数进行排序
  • 3.遍历所有用户自定义扩展类名称,将自定义扩展类放在自动激活的拓展对象们前面
  • 4.返回所有自动激活类集合。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342