Mybatis源碼學(xué)習(xí)(2)--XMLConfigBuilder

一盐数、解析配置文件

上一篇文章說到边琉,SqlSessionFactory的配置都是委托給XMLConfigBuilder的parse方法完成的抑进,本篇就來看看解析工程都做了哪些事情戈泼。

  //解析配置
  public Configuration parse() {
    //如果已經(jīng)解析過了骂倘,報錯
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
     //根節(jié)點是configuration
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

首先檢查下是否已經(jīng)解析過了眼滤,如果直接拋異常,在這里貼一份常見的mybatis配置文件历涝,便于理解:

<?xml version="1.0" encoding="UTF-8" ?> 
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" 
"http://mybatis.org/dtd/mybatis-3-config.dtd"> 
<configuration> 
        <environments default="development"> 
            <environment id="development"> 
                <transactionManager type="JDBC"/> 
                <dataSource type="POOLED"> 
                    <property name="driver" value="${driver}"/> 
                    <property name="url" value="${url}"/> 
                    <property name="username" value="${username}"/> 
                    <property name="password" value="${password}"/> 
                </dataSource> 
            </environment> 
        </environments>
        <mappers> 
            <mapper resource="org/mybatis/example/BlogMapper.xml"/> 
        </mappers> 
</configuration>

Mybatis解析xml用的是自己封裝的一個名為XPathParser的工具類(使用的全是JDK的類包)诅需,至于怎么具體解析的,這里不做展開荧库。如果是第一次解析堰塌,則從“/configuation”節(jié)點開始解析,代碼如下:

private void parseConfiguration(XNode root) {
  try {
    //分步驟解析
    //1.properties
    propertiesElement(root.evalNode("properties"));
    //2.類型別名
    typeAliasesElement(root.evalNode("typeAliases"));
    //3.插件
    pluginElement(root.evalNode("plugins"));
    //4.對象工廠
    objectFactoryElement(root.evalNode("objectFactory"));
    //5.對象包裝工廠
    objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
    //6.設(shè)置
    settingsElement(root.evalNode("settings"));
    // read it after objectFactory and objectWrapperFactory issue #631
    //7.環(huán)境
    environmentsElement(root.evalNode("environments"));
    //8.databaseIdProvider
    databaseIdProviderElement(root.evalNode("databaseIdProvider"));
    //9.類型處理器
    typeHandlerElement(root.evalNode("typeHandlers"));
    //10.映射器
    mapperElement(root.evalNode("mappers"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
  }
}

二分衫、十個解析項

上邊的代碼就是解析配置的核心场刑,總共10個步驟,我們一一來看看:

2.1丐箩、properties元素

在properties下配置如下屬性:

<properties resource="org/mybatis/example/config.properties">
    <property name="username" value="dev_user"/>
    <property name="password" value="F2Fa3!33TYyg"/>
</properties>

可以在該配置文件上全局使用摇邦,用于替換其它配置的變量,比如:

<dataSource type="POOLED">
  <property name="driver" value="${driver}"/>
  <property name="url" value="${url}"/>
  <property name="username" value="${username}"/>
  <property name="password" value="${password}"/>
</dataSource>

另外屎勘,配置的Properties還可以傳遞到SqlSessionFactoryBuilder.build() 方法中使用施籍,如下

SqlSessionFactory factory =
  sqlSessionFactoryBuilder.build(reader, props);

// ... or ...

SqlSessionFactory factory =
  new SqlSessionFactoryBuilder.build(reader, environment, props);

如果相同的配置項存在不同的位置,加載是怎樣的概漱?我們從代碼里一看究竟丑慎,代碼如下:

if (context != null) {
  //properties元素下的屬性會被首先加載
  Properties defaults = context.getChildrenAsProperties();
  //第二順序被加載的是類路徑下的resource或者url,如果與之前指定的屬性重復(fù),會覆蓋掉之前加載的屬性
  String resource = context.getStringAttribute("resource");
  String url = context.getStringAttribute("url");
  if (resource != null && url != null) {
    throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference.  Please specify one or the other.");
  }
  if (resource != null) {
    defaults.putAll(Resources.getResourceAsProperties(resource));
  } else if (url != null) {
    defaults.putAll(Resources.getUrlAsProperties(url));
  }
  //通過方法參數(shù)被傳遞進來的最后被加載竿裂,它會覆蓋掉前面兩步所有與之重復(fù)的屬性
  Properties vars = configuration.getVariables();
  if (vars != null) {
    defaults.putAll(vars);
  }
  parser.setVariables(defaults);
  configuration.setVariables(defaults);
}

看代碼邏輯玉吁,屬性加載的順序如下:

  1. properties元素下的屬性會被首先加載
  2. 第二順序被加載的是類路徑下的resource或者url,如果與之前指定的屬性重復(fù)腻异,會覆蓋掉之前加載的屬性进副。
  3. 通過方法參數(shù)被傳遞進來的最后被加載,它會覆蓋掉前面兩步所有與之重復(fù)的屬性悔常。

2.2影斑、typeAliases元素

<typeAliases>
  <typeAlias alias="Author" type="domain.blog.Author"/>
  <typeAlias alias="Blog" type="domain.blog.Blog"/>
</typeAliases>

別名是為了簡化java類型的實體全路徑名過長的問題,如上机打,通過配置矫户,Blog可以在任何地方代替domain.blog.Blog使用.

同理,也可以給J整個包起一個別名残邀,方便使用皆辽,如果沒有特殊說明使用全小寫的類名作為別名:

<typeAliases>
  <package name="domain.blog"/>
</typeAliases>

另外,也支持通過注解的形式指定別名

@Alias("author")
public class Author {
    ...
}

看一看解析別名的源碼

private void typeAliasesElement(XNode parent) {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      if ("package".equals(child.getName())) {
        //如果是package
        String typeAliasPackage = child.getStringAttribute("name");
        //(一)調(diào)用TypeAliasRegistry.registerAliases芥挣,去包下找所有類,然后注冊別名(有@Alias注解則用驱闷,沒有則取類的simpleName)
        configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
      } else {
        //如果是typeAlias
        String alias = child.getStringAttribute("alias");
        String type = child.getStringAttribute("type");
        try {
          Class<?> clazz = Resources.classForName(type);
          //根據(jù)Class名字來注冊類型別名
          //(二)調(diào)用TypeAliasRegistry.registerAlias
          if (alias == null) {
            //alias可以省略
            typeAliasRegistry.registerAlias(clazz);
          } else {
            typeAliasRegistry.registerAlias(alias, clazz);
          }
        } catch (ClassNotFoundException e) {
                    ...
        }
      }
    }
  }
}

可以看出在解析別名過程中會從上至下遍歷所有別名,登記到別名的map中九秀。

如果我在配置別名時寫成:

<typeAliases>
    <typeAlias alias="goods_alias" type="org.apache.ibatis.wyjdemo.MbGoods"/>
    <package name="org.apache.ibatis.wyjdemo"/>
</typeAliases>

即單獨指定和通過包名同時配置遗嗽,注冊時會變成怎么樣呢?打斷點看下

image-20200709235357972.png

可以看到兩種方式都生效了鼓蜒,兩個別名指向了一個bean.

2.3痹换、pluginElement元素

Mybatis支持拓展插件實現(xiàn)豐富的功能,它允許你在映射語句執(zhí)行過程匯中的某一點進行攔截調(diào)用都弹,通常來說娇豫,Mybatis允許插件在以下幾個方法調(diào)用攔截:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

這些類方法的詳細介紹可以在發(fā)行的源碼中方法簽名查看更多細節(jié)。想要編寫插件也非常簡單畅厢,只需要實現(xiàn)Interceptor接口冯痢,并指定要攔截的方法簽名即可。

我們自己實現(xiàn)一個非常簡單的插件來感受下框杜,插件的作用浦楣,首先要實現(xiàn)Interceptor接口,

@Intercepts({@Signature(type = Executor.class,
        method = "query",
        args = {MappedStatement.class,Object.class, RowBounds.class, ResultHandler.class }
)})
public class MySimplePlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 執(zhí)行目標(biāo)方法的前置處理
        Object[] args = invocation.getArgs();
        for (Object arg : args) {
            System.out.println(arg);
        }
        Object returnObject = invocation.proceed();
        // 執(zhí)行目標(biāo)方法的后置處理
        return returnObject;
    }


    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

簽名這里我配置的是Excutor,并且攔截query方法多一些文章咪辱,實現(xiàn)比較簡單就是打印出查詢參數(shù)振劳,然后再配置文件中加上,插件相關(guān)配置:

<!-- mybatis-config.xml -->
<plugins>
    <plugin interceptor="org.apache.ibatis.wyjdemo.MySimplePlugin">
    </plugin>
</plugins>

執(zhí)行輸出如下

org.apache.ibatis.mapping.MappedStatement@38082d64
{id=12, param1=12}
org.apache.ibatis.session.RowBounds@dfd3711
null

這個四個值依次對應(yīng)油狂,@Signature注解中args屬性配置的值历恐。

說回正題寸癌,我們看下源碼中是如何解析插件的配置的

private void pluginElement(XNode parent) throws Exception {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      String interceptor = child.getStringAttribute("interceptor");
      Properties properties = child.getChildrenAsProperties();
      Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
      interceptorInstance.setProperties(properties);
      //調(diào)用InterceptorChain.addInterceptor
      configuration.addInterceptor(interceptorInstance);
    }
  }
}

源碼很簡單,總體來說就是從配置文件中解拿到插件配置的攔截器名稱弱贼,然后實例化一個攔截器對象放到攔截器鏈中等待處理蒸苇。核心就是調(diào)用resovleClass方法根據(jù)配置的攔截名獲取一個該攔截的實例化對象。它又會調(diào)用resolveAlias實際去處理吮旅,resolveAlias源碼如下:

public <T> Class<T> resolveAlias(String string) {
  try {
    if (string == null) {
      return null;
    }
    String key = string.toLowerCase(Locale.ENGLISH);
    Class<T> value;
    //原理就很簡單了溪烤,從HashMap里找對應(yīng)的鍵值,找到則返回類型別名對應(yīng)的Class
    if (TYPE_ALIASES.containsKey(key)) {
      value = (Class<T>) TYPE_ALIASES.get(key);
    } else {
      //找不到庇勃,再試著將String直接轉(zhuǎn)成Class(這樣怪不得我們也可以直接用java.lang.Integer的方式定義氛什,也可以就int這么定義)
      value = (Class<T>) Resources.classForName(string);
    }
    return value;
  } catch (ClassNotFoundException e) {
    throw new TypeException("Could not resolve type alias '" + string + "'.  Cause: " + e, e);
  }
}

內(nèi)容就是先從上一步維護的別名映射map中找尋是否已經(jīng)擁有該實例(也就是攔截器中的配置也是支持別名的),沒有找到的話用反射得到一個實例返回匪凉。

2.4、objectFactoryElement元素

對象工廠主要的作用就是生產(chǎn)封裝結(jié)果集所需要的對象捺檬,Mybatis默認的對象工廠再层,不會對結(jié)果集做特殊的處理,僅是實例化一個目標(biāo)對象堡纬,如果需要對結(jié)果對象進行定制化的處理聂受,則要繼承DefaultObjectFactory,重寫父類方法烤镐,源碼也非常簡單蛋济,如下

private void objectFactoryElement(XNode context) throws Exception {
  if (context != null) {
    String type = context.getStringAttribute("type");
    Properties properties = context.getChildrenAsProperties();
    ObjectFactory factory = (ObjectFactory) resolveClass(type).newInstance();
    factory.setProperties(properties);
    configuration.setObjectFactory(factory);
  }
}

2.5、objectFactoryElement元素

對象包裝工程炮叶,官網(wǎng)文檔上沒有介紹碗旅,我自己也沒有搞明白,先略過镜悉。祟辟。。

2.6侣肄、settingsElement元素

顧名思義是對Mybatis進行設(shè)置旧困,這會影響Mybatis的運行方式,可以理解為是Mybatis的一些不同工能的開關(guān)稼锅,源碼非常簡單:

private void settingsElement(XNode context) throws Exception {
    if (context != null) {
      Properties props = context.getChildrenAsProperties();
      // Check that all settings are known to the configuration class
      //檢查下是否在Configuration類里都有相應(yīng)的setter方法(沒有拼寫錯誤)
      MetaClass metaConfig = MetaClass.forClass(Configuration.class);
      for (Object key : props.keySet()) {
        if (!metaConfig.hasSetter(String.valueOf(key))) {
          throw new BuilderException("The setting " + key + " is not known.  Make sure you spelled it correctly (case sensitive).");
        }
      }
      
      //下面非常簡單吼具,一個個設(shè)置屬性
      //如何自動映射列到字段/ 屬性
      configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
      ...
    }
  }
  

源碼中可以看到,設(shè)置前先檢查了setting節(jié)點中配置的屬性key值的有效性矩距,如果不是Configuration.class中的屬性則在啟動時就拋出異常拗盒,之后就是逐個讀配置設(shè)置到configuration中了。(另:這個過程中用到的Reflector類剩晴,可以學(xué)習(xí)下)

2.7锣咒、environmentsElement元素

Mybatis支持多個環(huán)境配置侵状,這允許我們寫的SQL可以映射到多個數(shù)據(jù)庫,例如毅整,我們的開發(fā)趣兄、測試、生產(chǎn)會有不同環(huán)境等悼嫉,在很多場景下都會有不同環(huán)境配置的需求艇潭,通過這個配置可以滿足。

但是要注意戏蔑,盡管我們可以配置多個環(huán)境蹋凝,但是每個環(huán)境只能選擇一個sqlSeesionFactory實例。如果我們需要連接兩個數(shù)據(jù)庫总棵,就需要創(chuàng)建兩個sqlSessionFactory,以此類推鳍寂。很容易記著:每個數(shù)據(jù)都需要一個sqlSessionFactroy與之對應(yīng)

看下配置文件:

<environments default="development">
  <environment id="development">
    <transactionManager type="JDBC">
      <property name="..." value="..."/>
    </transactionManager>
    <dataSource type="POOLED">
      <property name="driver" value="${driver}"/>
      <property name="url" value="${url}"/>
      <property name="username" value="${username}"/>
      <property name="password" value="${password}"/>
    </dataSource>
  </environment>
</environments>

源碼部分:

private void environmentsElement(XNode context) throws Exception {
  if (context != null) {
    if (environment == null) {
      environment = context.getStringAttribute("default");
    }
    for (XNode child : context.getChildren()) {
      String id = child.getStringAttribute("id");
//循環(huán)比較id是否就是指定的environment
      if (isSpecifiedEnvironment(id)) {
        //7.1事務(wù)管理器
        TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
        //7.2數(shù)據(jù)源
        DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
        DataSource dataSource = dsFactory.getDataSource();
        Environment.Builder environmentBuilder = new Environment.Builder(id)
            .transactionFactory(txFactory)
            .dataSource(dataSource);
        configuration.setEnvironment(environmentBuilder.build());
      }
    }
  }
}

首先判斷是否指定了環(huán)境,未指定就使用默認環(huán)境情龄,然后遍歷environment節(jié)點進行配置迄汛,由上邊代碼可以看到配置的主要內(nèi)容:加載事務(wù)管理器、數(shù)據(jù)源分別到TransactionFactory和DataSourceFactory骤视,然后賦值到configuration中鞍爱。

2.8、databaseIdProviderElement元素

Mybaits還支持根據(jù)不同數(shù)據(jù)庫廠商執(zhí)行不同的SQL語句专酗,其實不是很實用睹逃,很少生產(chǎn)項目會用不同廠商的數(shù)據(jù)庫,這里也不多做介紹祷肯。

2.9沉填、typeHandlerElement元素

類型處理器,Mybatis無論是在設(shè)置參數(shù)或者從結(jié)果集獲取數(shù)據(jù)都會用到類型處理器佑笋,它主要的功能是

獲取到的值轉(zhuǎn)換成對應(yīng)的Java類型數(shù)據(jù)拜轨。

<typeHandlers>
  <typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
</typeHandlers>

<typeHandlers>
  <package name="org.mybatis.example"/>
</typeHandlers>

看配置文件就可以知道,這個元素的方式也是同時支持單個處理器配置以及掃描包下的處理器兩種方式的允青,源碼如下:

private void typeHandlerElement(XNode parent) throws Exception {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      //如果是package
      if ("package".equals(child.getName())) {
        String typeHandlerPackage = child.getStringAttribute("name");
        //(一)調(diào)用TypeHandlerRegistry.register橄碾,去包下找所有類
        typeHandlerRegistry.register(typeHandlerPackage);
      } else {
        //如果是typeHandler
        String javaTypeName = child.getStringAttribute("javaType");
        String jdbcTypeName = child.getStringAttribute("jdbcType");
        String handlerTypeName = child.getStringAttribute("handler");
        Class<?> javaTypeClass = resolveClass(javaTypeName);
        JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
        Class<?> typeHandlerClass = resolveClass(handlerTypeName);
        //(二)調(diào)用TypeHandlerRegistry.register(以下是3種不同的參數(shù)形式)
        if (javaTypeClass != null) {
          if (jdbcType == null) {
            typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
          } else {
            typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
          }
        } else {
          typeHandlerRegistry.register(typeHandlerClass);
        }
      }
    }
  }
}

循環(huán)遍歷typeHandlers節(jié)點下元素,優(yōu)先掃描包下處理器颠锉,不存在則去單配總獲取法牲。可自行實現(xiàn)定制化的typeHandler,參考 自定義typeHandler

2.10琼掠、mapperElement元素

既然上邊幾個步驟我們配置了這么多的元素拒垃,最后我們要配置SQL映射了,但是怎么找到配置的映射關(guān)系呢瓷蛙?

Mybatis提供四種方法悼瓮,從配置文件可以看出

<!-- Using classpath relative resources -->
<mappers>
  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
  <mapper resource="org/mybatis/builder/BlogMapper.xml"/>
  <mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>

<!-- Using url fully qualified paths -->
<mappers>
  <mapper url="file:///var/mappers/AuthorMapper.xml"/>
  <mapper url="file:///var/mappers/BlogMapper.xml"/>
  <mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>

<!-- Using mapper interface classes -->
<mappers>
  <mapper class="org.mybatis.builder.AuthorMapper"/>
  <mapper class="org.mybatis.builder.BlogMapper"/>
  <mapper class="org.mybatis.builder.PostMapper"/>
</mappers>

<!-- Register all interfaces in a package as mappers -->
<mappers>
  <package name="org.mybatis.builder"/>
</mappers>

結(jié)合源碼看:

private void mapperElement(XNode parent) throws Exception {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      if ("package".equals(child.getName())) {
        //10.4自動掃描包下所有映射器
        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) {
          //10.1使用類路徑
          ErrorContext.instance().resource(resource);
          InputStream inputStream = Resources.getResourceAsStream(resource);
          //映射器比較復(fù)雜戈毒,調(diào)用XMLMapperBuilder
          //注意在for循環(huán)里每個mapper都重新new一個XMLMapperBuilder,來解析
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
          mapperParser.parse();
        } else if (resource == null && url != null && mapperClass == null) {
          //10.2使用絕對url路徑
          ErrorContext.instance().resource(url);
          InputStream inputStream = Resources.getUrlAsStream(url);
          //映射器比較復(fù)雜横堡,調(diào)用XMLMapperBuilder
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
          mapperParser.parse();
        } else if (resource == null && url == null && mapperClass != null) {
          //10.3使用java類名
          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. 如果是非包掃描方式,則會從resource命贴、url道宅、classs三種方式中按照順序加載一種方式。
  3. 上述方式都沒有采用的話胸蛛,則會拋出異常污茵。

總結(jié)

至此,XMLConfigbuilder初始化的過程我們已經(jīng)過了一遍葬项,這些步驟其實就是一個讀Mybatis配置文件的過程泞当,為后續(xù)的運行進行環(huán)境配置以及參數(shù)設(shè)置,到這一步民珍,Configuration已經(jīng)被“填充”完畢零蓉。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市穷缤,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌箩兽,老刑警劉巖津肛,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異汗贫,居然都是意外死亡身坐,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門落包,熙熙樓的掌柜王于貴愁眉苦臉地迎上來部蛇,“玉大人,你說我怎么就攤上這事咐蝇⊙穆常” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵有序,是天一觀的道長抹腿。 經(jīng)常有香客問我,道長旭寿,這世上最難降的妖魔是什么警绩? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮盅称,結(jié)果婚禮上肩祥,老公的妹妹穿的比我還像新娘后室。我一直安慰自己,他們只是感情好混狠,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布岸霹。 她就那樣靜靜地躺著,像睡著了一般檀蹋。 火紅的嫁衣襯著肌膚如雪松申。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天俯逾,我揣著相機與錄音贸桶,去河邊找鬼。 笑死桌肴,一個胖子當(dāng)著我的面吹牛皇筛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播坠七,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼水醋,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了彪置?” 一聲冷哼從身側(cè)響起拄踪,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎拳魁,沒想到半個月后惶桐,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡潘懊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年姚糊,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片授舟。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡救恨,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出释树,到底是詐尸還是另有隱情肠槽,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布奢啥,位于F島的核電站署浩,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏扫尺。R本人自食惡果不足惜筋栋,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望正驻。 院中可真熱鬧弊攘,春花似錦抢腐、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至捣域,卻和暖如春啼染,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背焕梅。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工迹鹅, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人贞言。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓斜棚,卻偏偏與公主長得像,于是被迫代替她去往敵國和親该窗。 傳聞我的和親對象是個殘疾皇子弟蚀,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344