Dubbo SPI 机制

前提

Dubbo的SPI是从JDK的SPI 扩展加强而来的。

  1. JDK中SPI 机制如下(可以查看JDBC的实现)
  • 需要在 classpath 下创建一个目录,该目录命名必须是:META-INF/service

  • 在该目录下创建一个 properties 文件,该文件需要满足以下几个条件 :

    • 文件名必须是扩展的接口的全路径名称

    • 文件内部描述的是该扩展接口的所有实现类

    • 文件的编码格式是 UTF-8

  • 通过 java.util.ServiceLoader 的加载机制来发现

  1. Dubbo 改进了JDK标准的SPI以下问题:
  • JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。
  • 如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK 标准的 ScriptEngine,通过 getName() 获取脚本类型的名称,但如果 RubyScriptEngine 因为所依赖的 jruby.jar 不存在,导致 RubyScriptEngine 类加载失败,这个失败原因被吃掉了,和 ruby 对应不起来,当用户执行 ruby 脚本时,会报不支持 ruby,而不是真正失败的原因
  • 增加了对扩展点 IoC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点

Dubbo SPI实现

官网约定描述

在扩展类的 jar 包内 1,放置扩展点配置文件 META-INF/dubbo/接口全限定名,内容为:配置名=扩展实现类全限定名,多个实现类用换行符分隔

示例

以扩展 Dubbo 的协议为例,接口声明如下 ,需要定义成可扩展的接口,要增加 @SPI


@SPI("dubbo")
public interface Protocol {

    int getDefaultPort();

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

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

    void destroy();

    default List<ProtocolServer> getServers() {
        return Collections.emptyList();
    }

}

在协议的实现 jar 包内放置文本文件:META-INF/dubbo/org.apache.dubbo.rpc.Protocol,内容为:

dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol

实现类内容

package org.apache.dubbo.rpc.protocol.dubbo;
 
import org.apache.dubbo.rpc.Protocol;
 
public class DubboProtocol extends AbstractProtocol { { 
    // ...
}

在Dubbo中可以通过三种方式来获取对应的类

// 获取自适应扩展点
ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
// 获取指定名称的扩展点
ExtensionLoader.getExtensionLoader(Protocol.class).getExtension("dubbo");
// 获取激活扩展点
ExtensionLoader.getExtensionLoader(Protocol.class).getActivateExtension(url,"dubbo");

原理

  1. 获取自适应扩展点

    • 什么是自适应扩展点

      因为Dubbo每个接口可能都有很多的实现,那在运行的过程中是如何决定选择哪一个实现的方法呢?所以自适应就是为了解决这个问题

      首先来看下 @Adaptive注解,查看接口Protocol的声明

      • 标注在类上,表明该类为自定义的适配类
      • 标注在方法上,表明需要动态的为该方法创建适配类(会动态生成一个适配类)
    • @Adpative注解可以指定多个值,如果指定的值在URL中没有找到,则以@SPI中指定的值作为默认的扩展进行返回;如对应的值是 {"key1","key2"},如果key1在URL中有对应的值,则使用key1的值作为扩展名称,如果key1找不到,则使用key2的值进行扩展,如果key2也没有值或是找不到,则使用默认@SPI中指定的值进行加载,否则抛错

      • 如果没有指定值,则会按接口的名称进行按英文字母进行拼接,用. 进行拼接,如 org.apache.dubbo.xxx.YyyInvokerWrapper,则生成的名称为 yyy.invoker.wrapper
      @Documented
      @Retention(RetentionPolicy.RUNTIME)
      @Target({ElementType.TYPE, ElementType.METHOD})
      public @interface Adaptive {
          /**
           * Decide which target extension to be injected. The name of the target extension is decided by the parameter passed
           * in the URL, and the parameter names are given by this method.
           * <p>
           * If the specified parameters are not found from {@link URL}, then the default extension will be used for
           * dependency injection (specified in its interface's {@link SPI}).
           * <p>
           * For example, given <code>String[] {"key1", "key2"}</code>:
           * <ol>
           * <li>find parameter 'key1' in URL, use its value as the extension's name</li>
           * <li>try 'key2' for extension's name if 'key1' is not found (or its value is empty) in URL</li>
           * <li>use default extension if 'key2' doesn't exist either</li>
           * <li>otherwise, throw {@link IllegalStateException}</li>
           * </ol>
           * If the parameter names are empty, then a default parameter name is generated from interface's
           * class name with the rule: divide classname from capital char into several parts, and separate the parts with
           * dot '.', for example, for {@code org.apache.dubbo.xxx.YyyInvokerWrapper}, the generated name is
           * <code>String[] {"yyy.invoker.wrapper"}</code>.
           *
           * @return parameter names in URL
           */
          String[] value() default {};
      
      }
      

调用如下方法,获取自适应扩展点

  ExtensionLoader.getExtensionLoader(Protocol.class).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;
      }
  

     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() {
          // 从jar包中获取相应的SPI实现的类,这块会在下面进行分析
          getExtensionClasses();
          // 如果cachedAdaptiveClass不为空,则这个类是@Adaptive 直接标注的类作为扩展实现类
          if (cachedAdaptiveClass != null) {
              return cachedAdaptiveClass;
          }
          return cachedAdaptiveClass = createAdaptiveExtensionClass();
      }

以下来看下 getExtensionClasses的实现

   // 获取扩展实现类    
     private Map<String, Class<?>> getExtensionClasses() {
             Map<String, Class<?>> classes = cachedClasses.get();
             if (classes == null) {
                 synchronized (cachedClasses) {
                     classes = cachedClasses.get();
                     if (classes == null) {
                         // 缓存中如果没有,从jar包中加载
                         classes = loadExtensionClasses();
                         cachedClasses.set(classes);
                     }
                 }
             }
             return classes;
         }
       private Map<String, Class<?>> loadExtensionClasses() {
           // 对应的需要适配的接口,缓存默认的扩展点名称,是 @SPI指定的值
           cacheDefaultExtensionName();
     
             Map<String, Class<?>> extensionClasses = new HashMap<>();
             // 从加载策略中获取,主要有三种 
             // DubboInternalLoadingStrategy从META-INF/dubbo/internal/目录下搜索
             // ServicesLoadingStrategy从META-INF/services/目录搜索
             // DubboLoadingStrategy从META-INF/dubbo/目录搜索
             for (LoadingStrategy strategy : strategies) {
                 loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
                 loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
             }
     
             return extensionClasses;
         }
       private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type,
                                boolean extensionLoaderClassLoaderFirst, boolean overridden, String... excludedPackages) {
           String fileName = dir + type;
           try {
               Enumeration<java.net.URL> urls = null;
               ClassLoader classLoader = findClassLoader();
   
               // try to load from ExtensionLoader's ClassLoader first
               if (extensionLoaderClassLoaderFirst) {
                   ClassLoader extensionLoaderClassLoader = ExtensionLoader.class.getClassLoader();
                   if (ClassLoader.getSystemClassLoader() != extensionLoaderClassLoader) {
                       urls = extensionLoaderClassLoader.getResources(fileName);
                   }
               }
   
               if (urls == null || !urls.hasMoreElements()) {
                   if (classLoader != null) {
                       urls = classLoader.getResources(fileName);
                   } else {
                       urls = ClassLoader.getSystemResources(fileName);
                   }
               }
   
               if (urls != null) {
                   while (urls.hasMoreElements()) {
                       java.net.URL resourceURL = urls.nextElement();
                       // 获取到所有包含这些目录里的相关文件,并缓存对应的Class信息
                       loadResource(extensionClasses, classLoader, resourceURL, overridden, excludedPackages);
                   }
               }
           } 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, boolean overridden, String... excludedPackages) {
           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 && !isExcluded(line, excludedPackages)) {
                                   // 加载类信息
                                   loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name, overridden);
                               }
                           } 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);
           }
       }
  private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name,
                              boolean overridden) 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注解,如果有,则缓存Adaptive类
           if (clazz.isAnnotationPresent(Adaptive.class)) {
               cacheAdaptiveClass(clazz, overridden);
               // 如果是当前类的包装类(Wrapper),有以当前接口为唯一参数的构造函数
               // 在获取扩展类时,会使用扩展Wrapper类进行封装当前扩展类
               // 也可以使用 @Wrapper 注解来标识,具体的类需要使用哪个 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)) {
                   cacheActivateClass(clazz, names[0]);
                   for (String n : names) {
                       //缓存名称
                       cacheName(clazz, n);
                       // 加入到extensionClasses中进行缓存
                       saveInExtensionClass(extensionClasses, clazz, n, overridden);
                   }
               }
           }
       }

到此,``getExtensionClasses`结束 ,相关的Class信息已经加载完成

再来看 createAdaptiveExtensionClass

   private Class<?> createAdaptiveExtensionClass() {
          // 动态创建对应类的适配类,创建类的内容,生成的类名如 Protocol$Adaptive
          // Dubbo 主要是以URL作为参数传递,动态获取扩展类也是依赖于URL对象,对于增加了@Adaptive 注解的方法,必须有URL作为参数,或是参数必须有URL的成员
          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);
      }

生成的动态适配类

  package org.apache.dubbo.rpc;
  
  import org.apache.dubbo.common.extension.ExtensionLoader;
  // 生成的适配类
public class Protocol$Adaptive implements org.apache.dubbo.rpc.Protocol {
      public void destroy() {
        throw new UnsupportedOperationException("The method public abstract void org.apache.dubbo.rpc.Protocol.destroy() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
   }

      public int getDefaultPort() {
          throw new UnsupportedOperationException("The method public abstract int org.apache.dubbo.rpc.Protocol.getDefaultPort() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
      }
  
      public org.apache.dubbo.rpc.Exporter export(org.apache.dubbo.rpc.Invoker arg0) throws org.apache.dubbo.rpc.RpcException {
          if (arg0 == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null");
          if (arg0.getUrl() == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument getUrl() == null");
          //依据参数的中的对应参数,获取真实的Protocol实现类
          org.apache.dubbo.common.URL url = arg0.getUrl();
          String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
          if (extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (" + url.toString() + ") use keys([protocol])");
          org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
          return extension.export(arg0);
      }
  
      public org.apache.dubbo.rpc.Invoker refer(java.lang.Class arg0, org.apache.dubbo.common.URL arg1) throws org.apache.dubbo.rpc.RpcException {
          if (arg1 == null) throw new IllegalArgumentException("url == null");
          org.apache.dubbo.common.URL url = arg1;
          String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
          if (extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (" + url.toString() + ") use keys([protocol])");
          org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
          return extension.refer(arg0, arg1);
      }
  
      public java.util.List getServers() {
          throw new UnsupportedOperationException("The method public default java.util.List org.apache.dubbo.rpc.Protocol.getServers() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
    }
  }

从源码中,上面有讲到 cachedAdaptiveClass变量,这个是当一个接口的实现类有 加 @Adaptive时,会作为默认的适配类,不需要动态再创建了

如 上面的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;
          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);
      }
  
  }
  1. 获取指定名称的扩展点

    这种方式和获取适配类的方式,大致加载逻辑类似,但这里是明确了具体的实现类,是通过名称进行获取的,比较特殊的点如下:

     private T createExtension(String name, boolean wrap) {
            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);
    
                // 如果wrap为true时,则会从cachedWrapperClasses已经缓存的包装类进行
                // 如获取名为registry的 Protocol 接口的实例,会返回一个 ProtocolFilterWrapper
                if (wrap) {
    
                    List<Class<?>> wrapperClassesList = new ArrayList<>();
                    if (cachedWrapperClasses != null) {
                        wrapperClassesList.addAll(cachedWrapperClasses);
                        wrapperClassesList.sort(WrapperComparator.COMPARATOR);
                        Collections.reverse(wrapperClassesList);
                    }
                    // 判断是否有包装类,如果有确认是否有符合的内容
                    if (CollectionUtils.isNotEmpty(wrapperClassesList)) {
                        for (Class<?> wrapperClass : wrapperClassesList) {
                            Wrapper wrapper = wrapperClass.getAnnotation(Wrapper.class);
                            if (wrapper == null
                                    || (ArrayUtils.contains(wrapper.matches(), name) && !ArrayUtils.contains(wrapper.mismatches(), name))) {
                                instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
                            }
                        }
                    }
                }
                // 注入依赖后,初始化实例,调用 lifecycle.initlialize()
                initExtension(instance);
                return instance;
            } catch (Throwable t) {
                throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
                        type + ") couldn't be instantiated: " + t.getMessage(), t);
            }
        }
    
  1. 获取激活扩展点

    主要是 @Activate注解起作用,自动激活给定的值对应的扩展,比如 @Activate可以加载指定的 Filter扩展类,因为Filter可以有多种实现

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE, ElementType.METHOD})
    public @interface Activate {
        /**
         * Activate the current extension when one of the groups matches. The group passed into
         * {@link ExtensionLoader#getActivateExtension(URL, String, String)} will be used for matching.
         *
         * @return group names to match
         * @see ExtensionLoader#getActivateExtension(URL, String, String)
         */
        String[] group() default {};
    
        /**
         * Activate the current extension when the specified keys appear in the URL's parameters.
         * <p>
         * For example, given <code>@Activate("cache, validation")</code>, the current extension will be return only when
         * there's either <code>cache</code> or <code>validation</code> key appeared in the URL's parameters.
         * </p>
         *
         * @return URL parameter keys
         * @see ExtensionLoader#getActivateExtension(URL, String)
         * @see ExtensionLoader#getActivateExtension(URL, String, String)
         */
        String[] value() default {};
    
        /**
         * Relative ordering info, optional
         * Deprecated since 2.7.0
         *
         * @return extension list which should be put before the current one
         */
        @Deprecated
        String[] before() default {};
    
        /**
         * Relative ordering info, optional
         * Deprecated since 2.7.0
         *
         * @return extension list which should be put after the current one
         */
        @Deprecated
        String[] after() default {};
    
        /**
         * Absolute ordering info, optional
         *
         * @return absolute ordering info
         */
        int order() default 0;
    }
    

    来看源码

        public List<T> getActivateExtension(URL url, String key, String group) {
            String value = url.getParameter(key); // 从URL中获取对应key的值,可以逗号隔开
            return getActivateExtension(url, StringUtils.isEmpty(value) ? null : COMMA_SPLIT_PATTERN.split(value), group);
        }
    
     public List<T> getActivateExtension(URL url, String[] values, String group) {
            List<T> activateExtensions = new ArrayList<>();
                List<String> names = values == null ? new ArrayList<>(0) : asList(values);
                if (!names.contains(REMOVE_VALUE_PREFIX + DEFAULT_KEY)) {
                getExtensionClasses();// 在加载对应的接口的具体实现类时,会判断是否有加 `@Activate`注解进行标识,如果有会加入到cachedActivates进行缓存
                for (Map.Entry<String, Object> entry : cachedActivates.entrySet()) {
                    String name = entry.getKey();
                    Object activate = entry.getValue();
    
                    String[] activateGroup, activateValue;
    
                    if (activate instanceof Activate) {
                        activateGroup = ((Activate) activate).group();
                        activateValue = ((Activate) activate).value();
                    } else if (activate instanceof com.alibaba.dubbo.common.extension.Activate) {
                        activateGroup = ((com.alibaba.dubbo.common.extension.Activate) activate).group();
                        activateValue = ((com.alibaba.dubbo.common.extension.Activate) activate).value();
                    } else {
                        continue;
                    }
                    // 是否符合
                    // 也会排除类似 -dubbo
                    if (isMatchGroup(group, activateGroup)
                            && !names.contains(name)
                            && !names.contains(REMOVE_VALUE_PREFIX + name)
                            && isActive(activateValue, url)) {
                        activateExtensions.add(getExtension(name));
                    }
                }
                    // 进行排序
                activateExtensions.sort(ActivateComparator.COMPARATOR);
            }
            List<T> loadedExtensions = new ArrayList<>();
            for (int i = 0; i < names.size(); i++) {
                String name = names.get(i);
                if (!name.startsWith(REMOVE_VALUE_PREFIX)
                        && !names.contains(REMOVE_VALUE_PREFIX + name)) {
                    if (DEFAULT_KEY.equals(name)) {
                        if (!loadedExtensions.isEmpty()) {
                            activateExtensions.addAll(0, loadedExtensions);
                            loadedExtensions.clear();
                        }
                    } else {
                        loadedExtensions.add(getExtension(name));
                    }
                }
            }
            if (!loadedExtensions.isEmpty()) {
                activateExtensions.addAll(loadedExtensions);
            }
            return activateExtensions;
        }
    

总结

  1. 在Dubbo中使用SPI实现动态扩展类,并通过@Adaptive 实现在运行时动态选择扩展类

  2. 在Dubbo中,自适应适配类依赖于 URL对象,在参数或是参数的成员中需要有URL对象

  3. @Adaptive标注在类上,则会是作为自适应扩展类,不会动态生成相应的代理类

  4. @Adaptive标注在方法上,则会生成动态的自适应扩展类,如果没有参数会以@SPI的值作为默认值,并会URL中对应的key的值为参考

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

推荐阅读更多精彩内容