Dubbo之SPI实现原理详解

开篇

 SPI全称为Service Provider Interface,是一种服务提供机制,比如在现实中我们经常会有这种场景,就是对于一个规范定义方而言(可以理解为一个或多个接口),具体的服务实现方是不可知的(可以理解为对这些接口的实现类),那么在定义这些规范的时候,就需要规范定义方能够通过一定的方式来获取到这些服务提供方具体提供的是哪些服务,而SPI就是进行这种定义的。

JDK SPI例子

说明:

  • 首先规范制定方会定义一个接口org.apache.jdk.spi.example.IHello 。

  • 其次在项目目录下的META-INF/service名称为org.apache.jdk.spi.example.IHello的文件,包含SPI实现接口全路径。

  • 通过ServiceLoader加载访问调用即可。

  • 对于jdk的SPI,其主要存在两个问题,为每个接口提供的服务一般尽量只提供一个,因为jdk的SPI默认会将所有目标文件中定义的所有子类都读取到返回使用;当定义多个子类实现时,无法动态的根据配置来使用不同的配置。

---- 定义接口
package org.apache.jdk.spi.example;

public interface IHello {
    void sayHello();
}

---- 定义实现1
package org.apache.jdk.spi.example;

public class HelloImpl1 implements IHello {
    @Override
    public void sayHello() {
        System.out.println("我是Impl1");
    }
}

---- 定义实现2
package org.apache.jdk.spi.example;

public class HelloImpl2 implements IHello {
    @Override
    public void sayHello() {
        System.out.println("我是Impl2");
    }
}

---- META-INF/services目录文件 org.apache.jdk.spi.example.IHello
org.apache.jdk.spi.example.HelloImpl1
org.apache.jdk.spi.example.HelloImpl2


---- 测试文件内容
package org.apache.jdk.spi.example;

import java.util.Iterator;
import java.util.ServiceLoader;

public class ServiceLoaderDemo {
    public static void main(String[] args){
        ServiceLoader<IHello> s = ServiceLoader.load(IHello.class);
        Iterator<IHello> iHelloIterator = s.iterator();

        while (iHelloIterator.hasNext()) {
            IHello iHello = iHelloIterator.next();
            iHello.sayHello();
        }
    }
}
jdk spi目录结构


Dubbo SPI例子

  • 定义PlantsWater的接口并通过@SPI注解进行注解,注解可选择带默认值。

  • 将watering()方法使用@Adaptive注解进行了标注,表示该方法在自动生成的子类中是需要动态实现的方法。

  • 增加grant()方法是为了表明不带@Adaptive在自动生成的子类方法内部会抛出异常。

  • 为PlantsWater增加两个实现,AppleWater和BananaWater,实际调用通过参数控制。

  • 在META-INF/dubbo下创建一个文件,该文件的名称是目标接口的全限定名,这里是org.apache.dubbo.spi.example.PlantsWater,在该文件中需要指定该接口所有可提供服务的子类。

  • 定义主函数ExtensionLoaderDemo模拟SPI调用的验证。

----定义基础应用类

public interface Fruit {}
public class Apple implements Fruit {}
public class Banana implements Fruit{}



----定义SPI类

@SPI("banana")
public interface PlantsWater {

    Fruit grant();

    @Adaptive
    String watering(URL url);
}


public class AppleWater implements PlantsWater {
    public Fruit grant() {
        return new Apple();
    }

    public String watering(URL url) {
        System.out.println("watering apple");
        return "watering finished";
    }
}


public class BananaWater implements PlantsWater {

    public Fruit grant() {
        return new Banana();
    }

    public String watering(URL url) {
        System.out.println("watering banana");
        return "watering success";
    }
}



----resources文件 org.apache.dubbo.spi.example.PlantsWater

apple=org.apache.dubbo.spi.example.AppleWater
banana=org.apache.dubbo.spi.example.BananaWater


------测试代码内容

public class ExtensionLoaderDemo {

    public static void main(String[] args) {
        // 首先创建一个模拟用的URL对象
        URL url = URL.valueOf("dubbo://192.168.0.101:20880?plants.water=apple");
        // 通过ExtensionLoader获取一个PlantsWater对象,getAdaptiveExtension已经加载了所有SPI类
        PlantsWater plantsWater = ExtensionLoader.getExtensionLoader(PlantsWater.class)
                .getAdaptiveExtension();
        // 使用该PlantsWater调用其"自适应标注的"方法,获取调用结果
        String result = plantsWater.watering(url);
        System.out.println(result);
    }
}


-----实际输出内容

十月 11, 2019 7:48:51 下午 org.apache.dubbo.common.logger.LoggerFactory info
信息: using logger: org.apache.dubbo.common.logger.jcl.JclLoggerAdapter
watering apple
watering finished

Process finished with exit code 0
dubbo spi目录结构


JDK 和 Dubbo SPI简单对比

Dubbo 的扩展点加载是基于JDK 标准的 SPI 扩展点发现机制增强而来的,Dubbo 改进了 JDK 标准的 SPI 的以下问题:

  • JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。

  • 如果扩展点加载失败,就失败了,给用户没有任何通知。比如:JDK 标准的ScriptEngine,如果Ruby ScriptEngine 因为所依赖的 jruby.jar 不存在,导致 Ruby ScriptEngine 类加载失败,这个失败原因被吃掉了,当用户执行 ruby 脚本时,会报空指针异常,而不是报Ruby ScriptEngine不存在。

  • 增加了对扩展点 IoC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点。


Dubbo SPI实现原理

dubbo对于SPI的实现主要是在ExtensionLoader这个类中,这个类主要有三个方法:

  • getExtension():主要用于获取名称为name的对应的子类的对象,这里如果子类对象如果有AOP相关的配置,这里也会对其进行封装;
  • getAdaptiveExtension():使用定义的装饰类来封装目标子类,具体使用哪个子类可以在定义的装饰类中通过一定的条件进行配置;
  • getExtensionLoader():加载当前接口的子类并且实例化一个ExtensionLoader对象。
public T getExtension(String name);
public T getAdaptiveExtension();
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type);


getExtension()

  • getExtension()方法的主要作用是获取name对应的子类对象返回。
  • 其实现方式是首先读取定义文件中的子类,然后根据不同的子类对象的功能的不同,比如使用@Adaptive修饰的装饰类和用于AOP的Wrapper类,将其封装到不同的缓存中。
  • 最后根据传入的name获取其对应的子类对象,并且使用相应的Wrapper类对其进行封装。

如下是getExtension()方法的源码:

    public T getExtension(String name) {
        if (StringUtils.isEmpty(name)) {
            throw new IllegalArgumentException("Extension name == null");
        }

        // 如果名称为true,则返回默认的子类对象,这里默认的子类对象的name定义在目标接口的@SPI注解中
        if ("true".equals(name)) {
            return getDefaultExtension();
        }

        // 查看当前是否已经缓存有保存目标对象的实例的Holder对象,缓存了则直接返回,
        // 没缓存则创建一个并缓存起来
        final Holder<Object> holder = getOrCreateHolder(name);
        Object instance = holder.get();

        // 如果无法从Holder中获取目标对象的实例,则使用双检查法为目标对象创建一个实例
        if (instance == null) {
            synchronized (holder) {
                instance = holder.get();
                if (instance == null) {
                    // 创建name对应的子类对象的实例
                    instance = createExtension(name);
                    holder.set(instance);
                }
            }
        }
        return (T) instance;
    }



    public T getDefaultExtension() {
        getExtensionClasses();
        if (StringUtils.isBlank(cachedDefaultName) || "true".equals(cachedDefaultName)) {
            return null;
        }
        // 通过cachedDefaultName去获取对应的子类实例
        return getExtension(cachedDefaultName);
    }


    private void cacheDefaultExtensionName() {
        // cachedDefaultName取自SPI的参数当中
        final SPI defaultAnnotation = type.getAnnotation(SPI.class);
        if (defaultAnnotation == null) {
            return;
        }

        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];
            }
        }
    }
  • 关于对于目标对象的获取,首先是从缓存里取,没取到才会进行创建。
  • 这里需要说明的是,如果传入的name为true,那么就会返回默认的子类实例,而默认的子类实例是通过其名称进行映射的,该名称存储在目标接口的@SPI注解中。



createExtension()方法的源码:

    private T createExtension(String name) {
        // 获取当前名称对应的子类类型,如果不存在,则抛出异常
        Class<?> clazz = getExtensionClasses().get(name);
        if (clazz == null) {
            throw findException(name);
        }
        try {
            // 获取当前class对应的实例,如果缓存中不存在,则实例化一个并缓存起来
            T instance = (T) EXTENSION_INSTANCES.get(clazz);
            if (instance == null) {
                EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
                instance = (T) EXTENSION_INSTANCES.get(clazz);
            }

            // 为生成的实例通过其set方法注入对应的实例,这里实例的获取方式不仅可以通过SPI的方式
            // 也可以通过Spring的bean工厂获取
            injectExtension(instance);
            Set<Class<?>> wrapperClasses = cachedWrapperClasses;
            if (CollectionUtils.isNotEmpty(wrapperClasses)) {
                for (Class<?> wrapperClass : wrapperClasses) {
                    // 实例化各个wrapper对象,并将目标对象通过wrapper的构造方法传入,
                    // 另外还会通过wrapper对象的set方法对其依赖的属性进行注入
                    instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
                }
            }
            return instance;
        } catch (Throwable t) {
            throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
                    type + ") couldn't be instantiated: " + t.getMessage(), t);
        }
    }

在createExtension()方法中,其主要做了三件事:

  • 加载定义文件中的各个子类,然后将目标name对应的子类返回后进行实例化。
  • 通过目标子类的set方法为其注入其所依赖的bean,这里既可以通过SPI,也可以通过Spring的BeanFactory获取所依赖的bean,injectExtension(instance)。
  • 获取定义文件中定义的wrapper对象,然后使用该wrapper对象封装目标对象,并且还会调用其set方法为wrapper对象注入其所依赖的属性。

关于wrapper对象,这里需要说明的是,其主要作用是为目标对象实现AOP。wrapper对象有两个特点:

  • a. 与目标对象实现了同一个接口;
  • b. 有一个以目标接口为参数类型的构造函数。这也就是上述createExtension()方法最后封装wrapper对象时传入的构造函数实例始终可以为instance实例的原因。



getExtensionClasses()方法的源码

    private Map<String, Class<?>> getExtensionClasses() {
        Map<String, Class<?>> classes = cachedClasses.get();
        if (classes == null) {
            synchronized (cachedClasses) {
                classes = cachedClasses.get();
                if (classes == null) {
                    // 加载定义文件,并且将定义的类按照功能缓存在不同的属性中,即:
                    // a. 目标class类型缓存在cachedClasses;
                    // b. wrapper的class类型缓存在cachedWrapperClasses;
                    // c. 用于装饰的class类型缓存在cachedAdaptiveClass;
                    classes = loadExtensionClasses();
                    cachedClasses.set(classes);
                }
            }
        }
        return classes;
    }



    private Map<String, Class<?>> loadExtensionClasses() {
        // 获取目标接口上通过@SPI注解定义的默认子类对应的名称,并将其缓存在cachedDefaultName中
        cacheDefaultExtensionName();


        // 分别在META-INF/dubbo/internal、META-INF/dubbo、META-INF/services目录下
        // 获取定义文件,并且读取定义文件中的内容,这里主要是通过META-INF/dubbo/internal
        // 获取目标定义文件
        Map<String, Class<?>> extensionClasses = new HashMap<>();
        loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName());
        loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
        loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName());
        loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
        loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName());
        loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
        return extensionClasses;
    }



    private void cacheDefaultExtensionName() {
        // 获取目标接口上通过@SPI注解定义的默认子类对应的名称,并将其缓存在cachedDefaultName中
        final SPI defaultAnnotation = type.getAnnotation(SPI.class);
        if (defaultAnnotation == null) {
            return;
        }

        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];
            }
        }
    }
  • loadExtensionClasses()主要是分别从三个目录中读取定义文件,读取该文件,并且进行缓存。



loadDirectory()方法的源码:

    private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type) {
        String fileName = dir + type;
        try {
            Enumeration<java.net.URL> urls;
            ClassLoader classLoader = findClassLoader();
            // 加载定义文件
            if (classLoader != null) {
                urls = classLoader.getResources(fileName);
            } else {
                urls = ClassLoader.getSystemResources(fileName);
            }
            if (urls != null) {
                while (urls.hasMoreElements()) {
                    // 对定义文件进行遍历,依次加载定义文件的内容
                    java.net.URL resourceURL = urls.nextElement();
                    loadResource(extensionClasses, classLoader, resourceURL);
                }
            }
        } catch (Throwable t) {
            logger.error("Exception occurred when loading extension class (interface: " +
                    type + ", description file: " + fileName + ").", t);
        }
    }



    private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) {
        try {
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {
                String line;
                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) {
                                loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);
                            }
                        } catch (Throwable t) {
                            IllegalStateException e = new IllegalStateException("Failed to load extension class (interface: " + type + ", class line: " + line + ") in " + resourceURL + ", cause: " + t.getMessage(), t);
                            exceptions.put(line, e);
                        }
                    }
                }
            }
        } catch (Throwable t) {
            logger.error("Exception occurred when loading extension class (interface: " +
                    type + ", class file: " + resourceURL + ") in " + resourceURL, t);
        }
    }
  • 这里主要是对每个目录进行加载,然后依次加载定义文件的内容,而对定义文件内容的处理主要是在loadResource()方法中,在对文件中每一行记录进行处理之后,其其最终是调用的loadClass()方法加载目标class的。



loadClass()方法的源码

    private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {

        // 如果加载得到的子类不是目标接口的实现类,则抛出异常
        if (!type.isAssignableFrom(clazz)) {
            throw new IllegalStateException("Error occurred when loading extension class (interface: " +
                    type + ", class line: " + clazz.getName() + "), class "
                    + clazz.getName() + " is not subtype of interface.");
        }

        // 如果子类上标注有@Adaptive注解,说明其是一个装饰类,则将其缓存在cachedAdaptiveClass中,
        // 需要注意的是,一个接口只能为其定义一个装饰类
        if (clazz.isAnnotationPresent(Adaptive.class)) {
            cacheAdaptiveClass(clazz);

        // 这里判断子类是否是一个wrapper类,判断方式就是检查其是否有只含一个目标接口类型参数的构造函数,
        // 有则说明其是一个AOP的wrapper类
        } else if (isWrapperClass(clazz)) {
            cacheWrapperClass(clazz);
        } else {
            // 走到这里说明当前子类不是一个功能型的类,而是最终实现具体目标的子类
            clazz.getConstructor();
            if (StringUtils.isEmpty(name)) {
                name = findAnnotationName(clazz);
                if (name.length() == 0) {
                    throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);
                }
            }

            String[] names = NAME_SEPARATOR.split(name);
            if (ArrayUtils.isNotEmpty(names)) {
                // 缓存ActivateClass类
                cacheActivateClass(clazz, names[0]);

                // 将目标子类缓存到extensionClasses中
                for (String n : names) {
                    cacheName(clazz, n);
                    saveInExtensionClass(extensionClasses, clazz, n);
                }
            }
        }
    }


    private void cacheActivateClass(Class<?> clazz, String name) {
        // 获取子类上的@Activate注解,该注解的主要作用是对子类进行分组的,
        // 对于分组之后的子类,可以通过getActivateExtension()来获取
        Activate activate = clazz.getAnnotation(Activate.class);
        if (activate != null) {
            cachedActivates.put(name, activate);
        } else {
            // 兼容alibaba版本的注解
            com.alibaba.dubbo.common.extension.Activate oldActivate = clazz.getAnnotation(com.alibaba.dubbo.common.extension.Activate.class);
            if (oldActivate != null) {
                cachedActivates.put(name, oldActivate);
            }
        }
    }


    private void saveInExtensionClass(Map<String, Class<?>> extensionClasses, Class<?> clazz, String name) {
        // 将目标子类缓存到extensionClasses中
        Class<?> c = extensionClasses.get(name);
        if (c == null) {
            extensionClasses.put(name, clazz);
        } else if (c != clazz) {
            String duplicateMsg = "Duplicate extension " + type.getName() + " name " + name + " on " + c.getName() + " and " + clazz.getName();
            logger.error(duplicateMsg);
            throw new IllegalStateException(duplicateMsg);
        }
    }

loadClass()方法主要作用是对子类进行划分,这里主要划分成了三部分:

  • 使用@Adaptive注解标注的装饰类;
  • 包含有目标接口类型参数构造函数的wrapper类
  • 目标处理具体业务的子类。

总结而言,getExtension()方法主要是获取指定名称对应的子类。在获取过程中,首先会从缓存中获取是否已经加载过该子类,如果没加载过则通过定义文件加载,并且使用获取到的wrapper对象封装目标对象返回。


getAdaptiveExtension()

  • ExtensionLoader在加载了定义文件之后会对子类进行一个划分,使用@Adaptive进行标注的子类和使用@Adaptive标注子类方法。

  • 使用@Adaptive进行标注的子类该子类的作用主要是用于对目标类进行装饰的,从而实现一定的目的。

  • 使用@Adaptive进行标注的方法,其使用的方式主要是在目标接口的某个方法上进行标注,这个时候,dubbo就会通过javassist字节码生成工具来动态的生成目标接口的子类对象,该子类会对该接口中标注了@Adaptive注解的方法进行重写,而其余的方法则默认抛出异常,通过这种方式可以达到对特定的方法进行修饰的目的。



getAdaptiveExtension()方法源码

    public T getAdaptiveExtension() {
        // 从缓存中获取装饰类的实例,存在则直接返回,不存在则创建一个缓存起来,然后返回
        Object instance = cachedAdaptiveInstance.get();
        if (instance == null) {
            if (createAdaptiveInstanceError != null) {
                throw new IllegalStateException("Failed to create adaptive instance: " +
                        createAdaptiveInstanceError.toString(),
                        createAdaptiveInstanceError);
            }

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

        return (T) instance;
    }
  • 从缓存中获取目标类的实例,不存在则创建一个该实例。



    createAdaptiveExtension()方法源码

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


    private Class<?> getAdaptiveExtensionClass() {
        // 获取目标extensionClasses,如果无法获取到,则在定义文件中进行加载
        getExtensionClasses();

       // 如果目标类型有使用@Adaptive标注的子类型,则直接使用该子类作为装饰类
        if (cachedAdaptiveClass != null) {
            return cachedAdaptiveClass;
        }

        // 如果目标类型没有使用@Adaptive标注的子类型,则尝试在目标接口中查找是否有使用@Adaptive标注的
        // 方法,如果有,则为该方法动态生成子类装饰代码
        return cachedAdaptiveClass = createAdaptiveExtensionClass();
    }

    private Class<?> createAdaptiveExtensionClass() {
        // 创建子类代码的字符串对象
        String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();

        // 获取当前dubbo SPI中定义的Compiler接口的子类对象,默认是使用javassist,
        // 然后通过该对象来编译生成的code,从而动态生成一个class对象
        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);
    }

  • createAdaptiveExtension()首先委托给getAdaptiveExtensionClass()方法获取一个装饰类实例,然后通过injectExtension()方法调用该实例的set方法来注入其所依赖的属性值。

  • 对于没有使用@Adaptive标注的子类时,才会使用Javassist来为目标接口生成其子类的装饰方法。

  • 对于使用@Adaptive标注的子类时,直接返回子类。

  • createAdaptiveExtensionClass()动态生成目标接口的子类字符串,然后通过javassit来编译该子类字符串,从而动态生成目标class。


getExtensionLoader()

    public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
        if (type == null) {
            throw new IllegalArgumentException("Extension type == null");
        }
        if (!type.isInterface()) {
            throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");
        }
        if (!withExtensionAnnotation(type)) {
            throw new IllegalArgumentException("Extension type (" + type +
                    ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
        }

        ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        if (loader == null) {
            EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
            loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        }
        return loader;
    }
  • 对于ExtensionLoader的获取,其实现过程比较简单,主要是从缓存中获取,如果缓存不存在,则实例化一个并且缓存起来。


ExtensionLoader加载流程图

Dubbo SPI

参考

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容