MyBatis 源碼分析篇 6:Mapper 方法執(zhí)行的“前因”

在前面的探索中,我們已經(jīng)知道了 MyBatis 是如何 getMapper 并執(zhí)行 Mapper 接口中的方法來進(jìn)行數(shù)據(jù)庫操作的雷滋。那么今天我們就來看看 Mapper 方法執(zhí)行的“前因”:獲取語句+參數(shù)映射内狗。

我們還是按照之前的方式凫碌,使用 debug 在入口代碼上打斷點(diǎn),步入源碼有送。入口代碼為:

List<Author> list = mapper.selectByName("Sylvia");

對(duì)應(yīng)的 SQL:

    <select id="selectByName" resultMap="AuthorMap" >
        select
          id, name, sex, phone
        from author
        where name = #{name}
    </select>

1 從 Mapper XML 讀取 SQL

首先淌喻,我們要思考一下,因?yàn)?SQL 語句是定義在 Mapper XML 中的雀摘,那么毫無疑問它會(huì)去讀取該 Mapper XML 中的內(nèi)容裸删。可是它會(huì)在什么時(shí)候讀取呢阵赠?答案是在獲取 SqlSessionFactory 的時(shí)候涯塔,即執(zhí)行:

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);  

為了驗(yàn)證,我們可以在這行打個(gè)斷點(diǎn)來一探究竟清蚀。

我們一直往下走匕荸,直到 org.apache.ibatis.builder.xml.XMLConfigBuilder 的 parseConfiguration 方法,我們?cè)谥耙呀?jīng)見過這個(gè)方法了枷邪,它的作用是解析 mybatis-config.xml 文件中的配置榛搔,將相應(yīng)的元素內(nèi)容轉(zhuǎn)換到 Configuration 等類中:

  private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

我們要看的是 Mapper XML 中 SQL 是不是在這個(gè)地方讀取的,理所應(yīng)當(dāng)進(jìn)入最后一行解析代碼:mapperElement(root.evalNode("mappers"));。這個(gè)時(shí)候我們會(huì)進(jìn)入到 org.apache.ibatis.builder.xml.XMLConfigBuilder 的 mapperElement 方法:

  private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        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");
          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();
          }//略...
        }
      }
    }
  }

在這個(gè)方法中我們可以看到它在解析 mybatis-config.xml 中的 mappers 元素內(nèi)容践惑。注意最后一行代碼:mapperParser.parse(); 看名字它似乎就是想要去解析 Mapper XML 的內(nèi)容绑洛,進(jìn)入 org.apache.ibatis.builder.xml.XMLMapperBuilder 的 parse() 方法:

  public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }

    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }

繼續(xù)進(jìn)入第 3 行代碼,進(jìn)入到 configurationElement() 方法:

  private void configurationElement(XNode context) {
    try {
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      builderAssistant.setCurrentNamespace(namespace);
      cacheRefElement(context.evalNode("cache-ref"));
      cacheElement(context.evalNode("cache"));
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      sqlElement(context.evalNodes("/mapper/sql"));
      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);
    }
  }

果然童本,這個(gè)方法就是用來解析 Mapper XML 的,我們可以看到平時(shí)在 Mapper XML 中常用的 resultMap脸候、sql 等穷娱。很明顯倒數(shù)第 5 行就是要讀取 SQL 語句的,所以我們直接進(jìn)入第 5 行:buildStatementFromContext(context.evalNodes("select|insert|update|delete"));运沦,進(jìn)入到 org.apache.ibatis.builder.xml.XMLStatementBuilder 的 parseStatementNode 方法()泵额。這時(shí),我們就能看到它對(duì)每個(gè)元素進(jìn)行了解析(由于篇幅過長携添,我們省略掉部分源代碼嫁盲,感興趣的同學(xué)可以動(dòng)手查看):

  public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");
    //太長省略啦...
    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    //太長省略啦...
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered, 
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }

我們先進(jìn)入第 6 行,直到 org.apache.ibatis.scripting.xmltags.XMLScriptBuilder 的 parseScriptNode() 方法:

  public SqlSource parseScriptNode() {
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource = null;
    if (isDynamic) {
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }

通過第 7 行代碼烈掠,我們會(huì)發(fā)現(xiàn)它將 sql 語句封裝在了 sqlSource 中(感興趣的話可以跟進(jìn)去看看)羞秤。此時(shí)的 sqlSource 是這個(gè)樣子噠:

包含 SQL 語句的 sqlSource

好了這個(gè)時(shí)候我們已經(jīng)拿到包含了 sql 語句的 sqlSource 了,那么我們來繼續(xù)看一下它要拿這個(gè) sqlSource 來做什么左敌●埃回到 parseStatementNode() 方法,繼續(xù)進(jìn)入 builderAssistant.addMappedStatement(...) 代碼:

public MappedStatement addMappedStatement(
      String id,
      SqlSource sqlSource,
      //參數(shù)太長省略啦...) {
    //...
    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
        .resource(resource)
        .fetchSize(fetchSize)
        .timeout(timeout)
        .statementType(statementType)
        .keyGenerator(keyGenerator)
        .keyProperty(keyProperty)
        .keyColumn(keyColumn)
        .databaseId(databaseId)
        .lang(lang)
        .resultOrdered(resultOrdered)
        .resultSets(resultSets)
        .resultMaps(getStatementResultMaps(resultMap, resultType, id))
        .resultSetType(resultSetType)
        .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
        .useCache(valueOrDefault(useCache, isSelect))
        .cache(currentCache);
    //...
    MappedStatement statement = statementBuilder.build();
    configuration.addMappedStatement(statement);
    return statement;
  }

首先我們要進(jìn)入第 6 行看一下 org.apache.ibatis.mapping.MappedStatement 類的 Builder 構(gòu)造方法矫限,我們可以看出此時(shí) sqlSource 賦值給了 mappedStatement 這個(gè)成員變量的 sqlSource哺哼,也就是說此時(shí)該 Builder 就持有了包含 SQL 的 sqlSource:

  public static class Builder {
    private MappedStatement mappedStatement = new MappedStatement();

    public Builder(Configuration configuration, String id, SqlSource sqlSource, SqlCommandType sqlCommandType) {
      mappedStatement.configuration = configuration;
      mappedStatement.id = id;
      mappedStatement.sqlSource = sqlSource;
      //略...
    }

那么接著我們從這個(gè)構(gòu)造方法出來繼續(xù)往下看,從倒數(shù)第 3 行代碼
configuration.addMappedStatement(statement); 就可以看出叼风,包含 SQL 的 statement 最終存進(jìn)了 configuration取董。Configuration 中有個(gè) Map 類型的成員變量 mappedStatements,如下:

protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");

那么點(diǎn)進(jìn)去 configuration.addMappedStatement(statement);无宿,不出意外茵汰,它一定是在給 mappedStatements 變量 put 內(nèi)容,而 key 就是語句的 id:

  public void addMappedStatement(MappedStatement ms) {
    mappedStatements.put(ms.getId(), ms);
  }

Bingo!

好了懈贺,現(xiàn)在就讓我們先牢記這個(gè)結(jié)論:Mapper XML 中的 SQL 語句(們)在構(gòu)建 SqlSessionFactory 的時(shí)候存入了 Configuration 實(shí)例的 mappedStatements (Map 類型)中经窖。

2 參數(shù)映射

現(xiàn)在就讓我們用文章開頭的入口代碼來 debug 進(jìn)入,看看 MyBatis 在執(zhí)行 SQL 之前是如何處理語句和參數(shù)的梭灿。其實(shí)上一節(jié)我們已經(jīng)跟蹤過 selectList 方法了画侣,只是上次我們的關(guān)注點(diǎn)在數(shù)據(jù)庫方法執(zhí)行上,現(xiàn)在我們就把關(guān)注點(diǎn)放在 SQL 處理和參數(shù)映射上堡妒。

我們跳過前面的代碼直到 org.apache.ibatis.executor.SimpleExecutor 的 doQuery(...) 方法:

  @Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.<E>query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

進(jìn)入第 7 行代碼配乱,走到 org.apache.ibatis.executor.SimpleExecutor 的 prepareStatement(...) 方法:

  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    stmt = handler.prepare(connection, transaction.getTimeout());
    handler.parameterize(stmt);
    return stmt;
  }

該方法即為處理 SQL 和參數(shù)的核心方法。其中,第 3 行代碼為獲取數(shù)據(jù)庫連接搬泥,第 4 行代碼為獲取 PreparedStatement桑寨,第 5 行代碼為參數(shù)映射。我們依次來看一下這三行代碼具體的實(shí)現(xiàn)忿檩。

2.1 獲取連接 Connection connection = getConnection(statementLog);

  protected Connection getConnection(Log statementLog) throws SQLException {
    Connection connection = transaction.getConnection();
    if (statementLog.isDebugEnabled()) {
      return ConnectionLogger.newInstance(connection, statementLog, queryStack);
    } else {
      return connection;
    }
  }

進(jìn)入方法尉尾,我們看到它先是通過 Transaction 獲取了一個(gè)連接,然后判斷日志級(jí)別是不是 debug 燥透,如果是沙咏,就會(huì)執(zhí)行 ConnectionLogger.newInstance(...) 方法,不是則直接返回 connection班套。其中肢藐,MyBatis 的 Transaction 接口主要負(fù)責(zé)獲取和關(guān)閉連接、提交和回滾事務(wù)吱韭,它有兩個(gè)實(shí)現(xiàn)類:

Transaction 的實(shí)現(xiàn)類
  • JdbcTransaction 直接使用了 JDBC 的提交和回滾設(shè)置吆豹。
  • ManagedTransaction 幾乎什么也不做,翻開源碼你會(huì)看到提交和回滾的方法里只有一行注釋代碼 “Does nothing”理盆,它讓容器來管理事務(wù)的全生命周期(例如 JEE 應(yīng)用服務(wù)器的上下文)痘煤。
  • 當(dāng)然我們也可以自定義自己的實(shí)現(xiàn)類,有興趣的同學(xué)可以自己玩兒一下(可參考文末附的項(xiàng)目實(shí)踐)猿规。

另外速勇,關(guān)于 ConnectionLogger.newInstance(...) 方法,根據(jù) statementLog.isDebugEnabled() 的判斷條件我們很容易能想到它是要處理連接時(shí)的日志輸出的坎拐》炒牛基于查看語句處理的目的,我們還是要跟進(jìn)去看一下是否進(jìn)行了其他操作:

  public static Connection newInstance(Connection conn, Log statementLog, int queryStack) {
    InvocationHandler handler = new ConnectionLogger(conn, statementLog, queryStack);
    ClassLoader cl = Connection.class.getClassLoader();
    return (Connection) Proxy.newProxyInstance(cl, new Class[]{Connection.class}, handler);
  }

還是熟悉的代碼哼勇,還是熟悉的配方:動(dòng)態(tài)代理都伪!通過 ConnectionLogger 的代理動(dòng)態(tài)生成 Connection 類。

2.2 獲取 PreparedStatement 對(duì)象 :stmt = handler.prepare(connection, transaction.getTimeout());

這行代碼用來獲取執(zhí)行語句的 PreparedStatement积担,我們跟入代碼直到 org.apache.ibatis.executor.statement.PreparedStatementHandler 的 instantiateStatement(...) 方法:

  @Override
  protected Statement instantiateStatement(Connection connection) throws SQLException {
    String sql = boundSql.getSql();
     //省略啦...
    } else {
      return connection.prepareStatement(sql);
    }
  }

我們看到在這里調(diào)用了 Connection 對(duì)象的 prepareStatement(...) 方法陨晶,從(1)的分析中我們知道,如果我們開啟了 debug 日志級(jí)別帝璧,那么此時(shí)的 Connection 為動(dòng)態(tài)代理生成的類先誉,調(diào)用其方法一定會(huì)進(jìn)入代理類的 invoke(...)。那么現(xiàn)在的烁,就讓我們跟進(jìn)代理類 ConnectionLogger 來一探究竟吧:

  @Override
  public Object invoke(Object proxy, Method method, Object[] params)
      throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, params);
      }    
      if ("prepareStatement".equals(method.getName())) {
        if (isDebugEnabled()) {
          debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
        }        
        PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
        stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
        return stmt;
      } else if ("prepareCall".equals(method.getName())) {
        if (isDebugEnabled()) {
          debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
        }        
        PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
        stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
        return stmt;
      } else if ("createStatement".equals(method.getName())) {
        Statement stmt = (Statement) method.invoke(connection, params);
        stmt = StatementLogger.newInstance(stmt, statementLog, queryStack);
        return stmt;
      } else {
        return method.invoke(connection, params);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

我們來解讀一下這個(gè)方法:首先它會(huì)在調(diào)用 connection 方法的時(shí)候根據(jù)方法名進(jìn)行判斷褐耳,我們以第 8 行處的
if 為例。如果是 connection.prepareStatement(...)渴庆,則首先會(huì)打印相應(yīng)的連接日志铃芦。然后雅镊,重點(diǎn)來了,它會(huì)獲取一個(gè) PreparedStatement刃滓,之后再調(diào)用 PreparedStatementLogger.newInstance(...) 方法并覆蓋前一行獲取的 PreparedStatement 對(duì)象仁烹。 PreparedStatementLogger 和 ConnectionLogger 的實(shí)現(xiàn)非常像:

  public static PreparedStatement newInstance(PreparedStatement stmt, Log statementLog, int queryStack) {
    InvocationHandler handler = new PreparedStatementLogger(stmt, statementLog, queryStack);
    ClassLoader cl = PreparedStatement.class.getClassLoader();
    return (PreparedStatement) Proxy.newProxyInstance(cl, new Class[]{PreparedStatement.class, CallableStatement.class}, handler);
  }

和獲取連接時(shí)打印日志和動(dòng)態(tài)生成 PreparedStatement 一樣,這里也通過動(dòng)態(tài)代理的方式打印日志并動(dòng)態(tài)生成下一步要用到的結(jié)果集 ResultSet 咧虎。這里不再深入探討了卓缰,等到我們看結(jié)果映射的時(shí)候再來討論。大家可以看出來砰诵,MyBatis 作為一個(gè)優(yōu)秀的 ORM 框架僚饭,有很多值得我們學(xué)習(xí)的地方,光是動(dòng)態(tài)代理的使用就很有意思了胧砰。

好了,我們已經(jīng)拿到 PreparedStatement 了苇瓣,下一步就是要處理參數(shù)了尉间。

2.3 參數(shù)映射:handler.parameterize(stmt);

我們?cè)谖臋n篇就知道了 MyBatis 通過 TypeHandler 來進(jìn)行參數(shù)和結(jié)果映射,這里既然要分析參數(shù)映射击罪,那么我們猜測它應(yīng)該會(huì)通過 TypeHandler 去實(shí)現(xiàn)哲嘲。不急,我們來慢慢驗(yàn)證一下:

handler 是 RoutingStatementHandler 類型的媳禁,但是它通過裝飾器模式委托給了 PreparedStatementHandler 來執(zhí)行(它們都是 StatementHandler 接口的實(shí)現(xiàn)類)眠副,并將上面我們得到的 PreparedStatement 作為參數(shù)傳遞了進(jìn)去。而跟入到 PreparedStatementHandler 我們又發(fā)現(xiàn)它通過 parameterHandler (DefaultParameterHandler 類型)來執(zhí)行竣稽,我們依次跟入會(huì)看到:

org.apache.ibatis.executor.statement.RoutingStatementHandler 類:

  @Override
  public void parameterize(Statement statement) throws SQLException {
    delegate.parameterize(statement);
  }

org.apache.ibatis.executor.statement.PreparedStatementHandler 類:

  @Override
  public void parameterize(Statement statement) throws SQLException {
    parameterHandler.setParameters((PreparedStatement) statement);
  }

org.apache.ibatis.scripting.defaults.DefaultParameterHandler 類:

  @Override
  public void setParameters(PreparedStatement ps) {
    //...
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
      for (int i = 0; i < parameterMappings.size(); i++) {
        ParameterMapping parameterMapping = parameterMappings.get(i);
        if (parameterMapping.getMode() != ParameterMode.OUT) {
          Object value;
          //這里是對(duì) value 的賦值囱怕,略...

          TypeHandler typeHandler = parameterMapping.getTypeHandler();
          JdbcType jdbcType = parameterMapping.getJdbcType();
          if (value == null && jdbcType == null) {
            jdbcType = configuration.getJdbcTypeForNull();
          }
          try {
            typeHandler.setParameter(ps, i + 1, value, jdbcType);//主要是這行P宓摹O劢唷磷蜀!
          } //...
        }
      }
    }
  }

注意 try 中的代碼:typeHandler丁侄!這里就驗(yàn)證了我們開始的猜測惹苗。它將參數(shù)值 value失尖,參數(shù)索引位置(i+1)和 jdbcType 作為參數(shù)傳遞給 typeHandler(該測試代碼中為 ObjectTypeHandler 類型) 的 setParameter 方法善榛。ObjectTypeHandler 繼承自 BaseTypeHandler 類戴而,同時(shí)繼承了其 setParameter 方法砾肺,于是下一步會(huì)進(jìn)入 BaseTypeHandler 類:

  @Override
  public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
    if (parameter == null) {
      //...
    } else {
      try {
        setNonNullParameter(ps, i, parameter, jdbcType);
      } catch (Exception e) {
        //...
      }
    }
  }

它又調(diào)用了自己(子類 ObjectTypeHandler)的 setNonNullParameter(...) 方法挽霉,進(jìn)入:

  @Override
  public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType)
      throws SQLException {
    TypeHandler handler = resolveTypeHandler(parameter, jdbcType);
    handler.setParameter(ps, i, parameter, jdbcType);
  }

setNonNullParameter(...) 方法的第一行代碼是通過參數(shù)值實(shí)際類型(parameter.getClass())和 jdbcType 自動(dòng)推算其 TypeHandler,這也印證了我們?cè)谖臋n篇中多次提到的 TypeHandler 不需要顯式地定義变汪,MyBatis 會(huì)自動(dòng)推算出來侠坎。我們繼續(xù)深入方法的第二行代碼,直到 StringTypeHandler 的 setNonNullParameter(...):

  @Override
  public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)
      throws SQLException {
    ps.setString(i, parameter);
  }

終于看到我們熟悉的 JDBC 代碼了裙盾,那么參數(shù)在這里就被填充進(jìn) PreparedStatement 中了硅蹦。

好了荣德,到此為止 PreparedStatement 就完全準(zhǔn)備好了,這時(shí)它就可以執(zhí)行了童芹。

附:

當(dāng)前版本:mybatis-3.5.0
官網(wǎng)文檔:MyBatis
項(xiàng)目實(shí)踐:MyBatis Learn
手寫源碼:MyBatis 簡易實(shí)現(xiàn)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末涮瞻,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子假褪,更是在濱河造成了極大的恐慌署咽,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,324評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件生音,死亡現(xiàn)場離奇詭異宁否,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)缀遍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門慕匠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人域醇,你說我怎么就攤上這事台谊。” “怎么了譬挚?”我有些...
    開封第一講書人閱讀 162,328評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵锅铅,是天一觀的道長。 經(jīng)常有香客問我减宣,道長盐须,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,147評(píng)論 1 292
  • 正文 為了忘掉前任漆腌,我火速辦了婚禮贼邓,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘闷尿。我一直安慰自己立帖,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評(píng)論 6 388
  • 文/花漫 我一把揭開白布悠砚。 她就那樣靜靜地躺著晓勇,像睡著了一般。 火紅的嫁衣襯著肌膚如雪灌旧。 梳的紋絲不亂的頭發(fā)上绑咱,一...
    開封第一講書人閱讀 51,115評(píng)論 1 296
  • 那天,我揣著相機(jī)與錄音枢泰,去河邊找鬼描融。 笑死,一個(gè)胖子當(dāng)著我的面吹牛衡蚂,可吹牛的內(nèi)容都是我干的窿克。 我是一名探鬼主播骏庸,決...
    沈念sama閱讀 40,025評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼年叮!你這毒婦竟也來了具被?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,867評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤只损,失蹤者是張志新(化名)和其女友劉穎一姿,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體跃惫,經(jīng)...
    沈念sama閱讀 45,307評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡叮叹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了爆存。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蛉顽。...
    茶點(diǎn)故事閱讀 39,688評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖先较,靈堂內(nèi)的尸體忽然破棺而出携冤,到底是詐尸還是另有隱情,我是刑警寧澤拇泣,帶...
    沈念sama閱讀 35,409評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站矮锈,受9級(jí)特大地震影響霉翔,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜苞笨,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評(píng)論 3 325
  • 文/蒙蒙 一债朵、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧瀑凝,春花似錦序芦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至寥枝,卻和暖如春宪塔,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背囊拜。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評(píng)論 1 268
  • 我被黑心中介騙來泰國打工某筐, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人冠跷。 一個(gè)月前我還...
    沈念sama閱讀 47,685評(píng)論 2 368
  • 正文 我出身青樓南誊,卻偏偏與公主長得像身诺,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子抄囚,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容