mybatis(一):加载和解析配置文件

在这里记录一下自己学习mybatis源码过程中的一些学习体会,文章内容基于mybatis3.5.3-SNAPSHOT:

下面是mybatis一个测试用例中配置文件的截图,配置文件详情参考mybatis中文官网

image

1.事例

下面是mybatis测试用例中加载配置文件,并且运行的过程,这篇文章主要记录一下mybatis加载配置文件的过程

@BeforeAll
static void setUp() throws Exception {
// create a SqlSessionFactory
try (Reader reader = Resources.getResourceAsReader("org/apache/ibatis/submitted/permissions/mybatis-config.xml")) {
  sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
}

// populate in-memory database
BaseDataTest.runScript(sqlSessionFactory.getConfiguration().getEnvironment().getDataSource(),
        "org/apache/ibatis/submitted/permissions/CreateDB.sql");
}

从以上的实例代码可以看到关于mybatis读取默认配置文件的过程,接下来就是详细的看看整体的过程。

2.源码分析

2.1创建SqlSessionFactory

SqlSession是mybatis的关键,这个接口包含了sql执行,事务,缓存等许多的方法。要获取SqlSession就要先得到SqlSessionFactory。为了得到SqlSessionFactory就需要使用SqlSessionFactoryBuilder来解析配置文件,SqlSessionFactoryBuilder有多个build方法,基本一致,挑一个来看看。

public SqlSessionFactorybuild(Reader reader, String environment, Properties properties) {

try {
    // 创建 XMLConfigBuilder 对象,底层使用的是jdk的XPath解析xml文件
    XMLConfigBuilder parser =new XMLConfigBuilder(reader, environment, properties);
    // 执行 XML 解析
    // 创建 DefaultSqlSessionFactory 对象
    return build(parser.parse());
  }catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
  }finally {
ErrorContext.instance().reset();
    try {
reader.close();
    }catch (IOException e) {
    // Intentionally ignore. Prefer previous error.
    }
}

}

2.2 解析配置文件

下面我们来看看parser.parse():

public Configurationparse() {
  // 判断是否已经加载过
  if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
  }
  parsed =true;
  // 解析configuration节点
  parseConfiguration(parser.evalNode("/configuration"));
  return configuration;
}

最重要的是parseConfiguration方法:

private void parseConfiguration(XNode root) {
try {
    //issue #117 read properties first
    // 属性
    propertiesElement(root.evalNode("properties"));
    // 设置,这是 MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为
    Properties settings = settingsAsProperties(root.evalNode("settings"));
    // 加载自定义 VFS 实现类
    loadCustomVfs(settings);
    // 指定 MyBatis 所用日志的具体实现,未指定时将自动查找
    loadCustomLogImpl(settings);
    // 类型别名,为 Java 类型设置一个短的名字
    typeAliasesElement(root.evalNode("typeAliases"));
    // 插件,在已映射语句执行过程中的某一点进行拦截调用
    pluginElement(root.evalNode("plugins"));
    // 对象工厂,MyBatis 每次创建结果对象的新实例时,它都会使用一个对象工厂实例来完成
    objectFactoryElement(root.evalNode("objectFactory"));
    // 对象包装工厂
    objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
    // 反射工厂
    reflectorFactoryElement(root.evalNode("reflectorFactory"));
    // 设置settings属性到configuration中,没有时设置默认配置
    settingsElement(settings);
    // read it after objectFactory and objectWrapperFactory issue #631
    // 环境配置
    environmentsElement(root.evalNode("environments"));
    // 数据库厂商标识
    databaseIdProviderElement(root.evalNode("databaseIdProvider"));
    // 类型处理器,MyBatis 在预处理语句(PreparedStatement)中设置一个参数时,
    // 还是从结果集中取出一个值时, 都会用类型处理器将获取的值以合适的方式转换成 Java 类型
    typeHandlerElement(root.evalNode("typeHandlers"));
    // SQL 映射语句
    mapperElement(root.evalNode("mappers"));
  }catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
  }
}

大多数都是属性的设置,最终所有的设置都会配置到XMLConfigBuilder以及父类BaseBuilder的属性对象中,其中mapperElement方法是解析mapper.xml,即我们的mapper.xml文件或者*mapper.java接口(针对在java文件中通过注解创建sql和加上一些配置等)。

2.3 xml文件以及接口解析

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        // 如果是配置的package那就扫描包,针对已经在方法上使用注解实现功能
        <1>
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          // 解析本地的xml文件
          <2>
          if (resource != null && url == null && mapperClass == null) {
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            mapperParser.parse();
          }
          // 解析远程地址上的xml文件
          <3>
          else if (resource == null && url != null && mapperClass == null) {
            ErrorContext.instance().resource(url);
            InputStream inputStream = Resources.getUrlAsStream(url);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            mapperParser.parse();
          }
          // 单个文件解析,也是针对已经在方法上使用注解实现功能
          <4>
          else if (resource == null && url == null && mapperClass != null) {
            Class<?> mapperInterface = Resources.classForName(mapperClass);
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }

<1>,<2>,<3>,<4>处的代码,最终的解析方式都是解析解析xml的同时解析对应的接口内的方法,或者是先解析接口内的方法再解析接口对应的xml文件
configuration.addMappers,先来看下MapperRegistry.addMapper方法:

public <T> void addMapper(Class<T> type) {
    // 判断必须是接口
    if (type.isInterface()) {
      // 判断是否解析过
      if (hasMapper(type)) {
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
        // 用于判断是否解析过
        knownMappers.put(type, new MapperProxyFactory<>(type));
        // It's important that the type is added before the parser is run
        // otherwise the binding may automatically be attempted by the
        // mapper parser. If the type is already known, it won't try.
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        <1>
        parser.parse();
        loadCompleted = true;
      } finally {
        // 解析错误,留到后面解析
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }

<1>处代码最关键

public void parse() {
    String resource = type.toString();
    // 判断是否加载过
    if (!configuration.isResourceLoaded(resource)) {
      // 加载对应的*mapper.xml文件
      <1>
      loadXmlResource();
      // 用于判断是否加载
      configuration.addLoadedResource(resource);
      // 设置当前命名空间,如果与当前命名空间不一致,抛出错误
      // 我理解可能是防止多线程下同时解析不同文件
      assistant.setCurrentNamespace(type.getName());
      // 解析@CacheNamespace,二级缓存相关
      parseCache();
      // 解析@CacheNamespaceRef,二级缓存相关
      parseCacheRef();
      Method[] methods = type.getMethods();
      // 遍历每个方法,解析其上的注解
      for (Method method : methods) {
        try {
          // issue #237
          if (!method.isBridge()) {
            <2>
            parseStatement(method);
          }
        } catch (IncompleteElementException e) {
          // 解析失败,添加到 configuration 中
          configuration.addIncompleteMethod(new MethodResolver(this, method));
        }
      }
    }
    // 解析上面for循环解析失败的方法
    parsePendingMethods();
  }

其中<1>处代码是解析xml文件的,<2>处代码是解析对应的java接口
先来看看<1>处代码是怎么找到并且解析xml文件的

private void loadXmlResource() {
    // Spring may not know the real resource name so we check a flag
    // to prevent loading again a resource twice
    // this flag is set at XMLMapperBuilder#bindMapperForNamespace
    // 判断是否加载过
    if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
      // 获取当前对应的xml的路径
      String xmlResource = type.getName().replace('.', '/') + ".xml";
      // #1347
      // 获取当前模块中的xml文件流对象
      InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
      if (inputStream == null) {
        // Search XML mapper that is not in the module but in the classpath.
        try {
          // 获取不在当前模块,但是在对应路径下的xml文件
          inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
        } catch (IOException e2) {
          // ignore, resource is not required
        }
      }
      if (inputStream != null) {
        XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
        xmlParser.parse();
      }
    }
  }

继续来看看XMLMapperBuilder.parse()是如何解析xml文件的。

2.3.1解析xml

public void parse() {
    // 如果没有加载过
    if (!configuration.isResourceLoaded(resource)) {
      // 解析xml文件中的所有标签
      <1>
      configurationElement(parser.evalNode("/mapper"));
      // 标记该 Mapper 已经加载过
      configuration.addLoadedResource(resource);
      // 解析对应的*mapper.java文件,
      // 解析xml或者java文件的时候都会去解析对应的另外一个文件
      // 在解析对应的文件时都要判断是否已经解析过
      bindMapperForNamespace();
    }

    // 解析待定的 <resultMap /> 节点
    parsePendingResultMaps();
    // 解析待定的 <cache-ref /> 节点
    parsePendingCacheRefs();
    // 解析待定的 SQL 语句的节点
    parsePendingStatements();
  }

重点看看<1>处的代码,是如何解析整个xml文件中的所有节点的,最后面的3个方法是继续尝试解析前面解析xml文件时没有解析成功的节点。

private void configurationElement(XNode context) {
    try {
      // 获得 namespace 属性
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      // 设置 namespace 属性
      builderAssistant.setCurrentNamespace(namespace);
      // 解析 <cache-ref> 节点
      cacheRefElement(context.evalNode("cache-ref"));
      // 解析 <cache> 节点
      cacheElement(context.evalNode("cache"));
      // 已废弃!老式风格的参数映射。内联参数是首选,这个元素可能在将来被移除,这里不会记录。
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      // <1> 解析 <resultMap> 节点们,解析成resultMap对象保存在 configuration 中
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      // 解析 <sql> 节点们,保存id和node对应关系到 sqlFragments 中
      sqlElement(context.evalNodes("/mapper/sql"));
      // <2> 解析 <select> <insert> <update> <delete> 节点们
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
  }

这里比较复杂的是<1>和<2>处的解析,别的比较简单,可自行看一下。

2.3.1.1resultMapElements(context.evalNodes("/mapper/resultMap"))

private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) throws Exception {
    ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
    // 获取 type
    String type = resultMapNode.getStringAttribute("type",
        resultMapNode.getStringAttribute("ofType",
            resultMapNode.getStringAttribute("resultType",
                resultMapNode.getStringAttribute("javaType"))));
    // 替换别名,获取 class 对象
    Class<?> typeClass = resolveClass(type);
    if (typeClass == null) {
      typeClass = inheritEnclosingType(resultMapNode, enclosingType);
    }
    Discriminator discriminator = null;
    List<ResultMapping> resultMappings = new ArrayList<>();
    resultMappings.addAll(additionalResultMappings);
    // 解析子节点
    List<XNode> resultChildren = resultMapNode.getChildren();
    // 解析所有的节点到 resultMappings 中
    for (XNode resultChild : resultChildren) {
      if ("constructor".equals(resultChild.getName())) {
        processConstructorElement(resultChild, typeClass, resultMappings);
      } else if ("discriminator".equals(resultChild.getName())) {
        discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
      } else {
        List<ResultFlag> flags = new ArrayList<>();
        if ("id".equals(resultChild.getName())) {
          flags.add(ResultFlag.ID);
        }
        resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
      }
    }
    String id = resultMapNode.getStringAttribute("id",
            resultMapNode.getValueBasedIdentifier());
    String extend = resultMapNode.getStringAttribute("extends");
    Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
    ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
    try {
      // 保存 resultMap 到 configuration 中
      return resultMapResolver.resolve();
    } catch (IncompleteElementException  e) {
      configuration.addIncompleteResultMap(resultMapResolver);
      throw e;
    }
  }

此处主要是解析<resultMap>节点,构造resultMap对象,并且保存到 configuration中

2.3.1.2buildStatementFromContext(context.evalNodes("select|insert|update|delete"));

private void buildStatementFromContext(List<XNode> list) {
    if (configuration.getDatabaseId() != null) {
      buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
  }

  private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteStatement(statementParser);
      }
    }
  }

public void parseStatementNode() {
    // 例如<select id="">
    String id = context.getStringAttribute("id");
    // 例如<select databaseId="">
    String databaseId = context.getStringAttribute("databaseId");

    // 判断当前节点是否已经解析过 以及 判断databaseId是否相等
    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }

    // 获取是什么类型的节点 如 :select
    String nodeName = context.getNode().getNodeName();
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    // Include Fragments before parsing
    // 解析sql的 include 片段
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    // 获取 parameterType 以及 别名转换
    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);

    // 国际化相关
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    // Parse selectKey after includes and remove them.
    // 在 configuration 中保存 KeyGenerator,用于自动生成主键
    processSelectKeyNodes(id, parameterTypeClass, langDriver);

    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    // 返回 currentNamespace + "." + id + "!selectKey"
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    if (configuration.hasKeyGenerator(keyStatementId)) {
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
      // 如果没有 <selectKey> 节点,就查看 该节点是否配置了 useGeneratedKeys,
      // 或者配置文件配置了 useGeneratedKeys 并且 该语句为 insert
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }

    // 如果是没有动态标签的sql,#{}会解析为?,并且保存对应的属性
    // 如果是动态标签的sql,按sql的顺序拆分sql为 单个 sqlNode(ifSqlNode,forEachSqlNode等)或者是MixedSqlNode包含多个SqlNode
    // 以及sql对应的 prefix,subffix,prefixesToOverride等属性
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    String resultType = context.getStringAttribute("resultType");
    Class<?> resultTypeClass = resolveClass(resultType);
    String resultMap = context.getStringAttribute("resultMap");
    String resultSetType = context.getStringAttribute("resultSetType");
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    if (resultSetTypeEnum == null) {
      resultSetTypeEnum = configuration.getDefaultResultSetType();
    }
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    String resultSets = context.getStringAttribute("resultSets");

    // configuration 中保存 MappedStatement
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }

最终新增的MappedStatement对象代表的是xml一个sql标签,包含这个sql的所有配置以及sql语句等属性。

2.3.2 解析java接口

代码入口在MapperAnnotationBuilder.parse()中的parseStatement(method)方法,这就是解析接口中的每个方法以及方法上的注解的。

  void parseStatement(Method method) {
    // 获取非分页的形参类型,多个参数用 ParamMap 表示
    Class<?> parameterTypeClass = getParameterType(method);
    // 注解式的动态sql
    LanguageDriver languageDriver = getLanguageDriver(method);
    // 获取 sqlsourse 对象
    SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
    if (sqlSource != null) {
      Options options = method.getAnnotation(Options.class);
      final String mappedStatementId = type.getName() + "." + method.getName();
      Integer fetchSize = null;
      Integer timeout = null;
      StatementType statementType = StatementType.PREPARED;
      ResultSetType resultSetType = configuration.getDefaultResultSetType();
      SqlCommandType sqlCommandType = getSqlCommandType(method);
      // 是否是 select sql 语句
      boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
      // 如果不是 select 默认开始二级缓存
      boolean flushCache = !isSelect;
      // select 默认开启一级缓存
      boolean useCache = isSelect;

      KeyGenerator keyGenerator;
      String keyProperty = null;
      String keyColumn = null;
      // 获取生成自动主键的 keyGenerator
      if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
        // first check for SelectKey annotation - that overrides everything else
        SelectKey selectKey = method.getAnnotation(SelectKey.class);
        if (selectKey != null) {
          keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver);
          keyProperty = selectKey.keyProperty();
        } else if (options == null) {
          keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
        } else {
          keyGenerator = options.useGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
          keyProperty = options.keyProperty();
          keyColumn = options.keyColumn();
        }
      } else {
        keyGenerator = NoKeyGenerator.INSTANCE;
      }

      if (options != null) {
        if (FlushCachePolicy.TRUE.equals(options.flushCache())) {
          flushCache = true;
        } else if (FlushCachePolicy.FALSE.equals(options.flushCache())) {
          flushCache = false;
        }
        useCache = options.useCache();
        fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null; //issue #348
        timeout = options.timeout() > -1 ? options.timeout() : null;
        statementType = options.statementType();
        if (options.resultSetType() != ResultSetType.DEFAULT) {
          resultSetType = options.resultSetType();
        }
      }

      String resultMapId = null;
      ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
      if (resultMapAnnotation != null) {
        resultMapId = String.join(",", resultMapAnnotation.value());
      } else if (isSelect) {
        resultMapId = parseResultMap(method);
      }

      // 构造 mappedstatement configuration 中
      assistant.addMappedStatement(
          mappedStatementId,
          sqlSource,
          statementType,
          sqlCommandType,
          fetchSize,
          timeout,
          // ParameterMapID
          null,
          parameterTypeClass,
          resultMapId,
          getReturnType(method),
          resultSetType,
          flushCache,
          useCache,
          // TODO gcode issue #577
          false,
          keyGenerator,
          keyProperty,
          keyColumn,
          // DatabaseID
          null,
          languageDriver,
          // ResultSets
          options != null ? nullOrEmpty(options.resultSets()) : null);
    }
  }

大致的解析解析过程就是这样的,总的来说就是解析所有的配置文件组成一个configuration对象,然后会调用SqlSessionFactory的build方法new 一个DefaultSqlSessionFactory对象,并且设置configuration,configuration几乎包含了mybatis所有的属性,贯穿几乎所有的mybatis流程。

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

推荐阅读更多精彩内容