MyBatis原理系列(二)-手把手带你了解MyBatis的启动流程

MyBatis原理系列(一)-手把手带你阅读MyBatis源码
MyBatis原理系列(二)-手把手带你了解MyBatis的启动流程
MyBatis原理系列(三)-手把手带你了解SqlSession,SqlSessionFactory,SqlSessionFactoryBuilder的关系
MyBatis原理系列(四)-手把手带你了解MyBatis的Executor执行器
MyBatis原理系列(五)-手把手带你了解Statement、StatementHandler、MappedStatement间的关系
MyBatis原理系列(六)-手把手带你了解BoundSql的创建过程
MyBatis原理系列(七)-手把手带你了解如何自定义插件
MyBatis原理系列(八)-手把手带你了解一级缓存和二级缓存
MyBatis原理系列(九)-手把手带你了解MyBatis事务管理机制

在上篇文章中,我们举了一个例子如何使用MyBatis,但是对其中dao层,entity层,mapper层间的关系不得而知,从此篇文章开始,笔者将从MyBatis的启动流程着手,真正的开始研究MyBatis源码了。

1. MyBatis启动代码示例

在上篇文章中,介绍了MyBatis的相关配置和各层代码编写,本文将以下代码展开描述和介绍MyBatis的启动流程,并简略的介绍各个模块的作用,各个模块的细节部分将在其它文章中呈现。

回顾下上文中使用mybatis的部分代码,包括七步。每步虽然都是一行代码,但是隐藏了很多细节。接下来我们将围绕这起步展开了解。

@Slf4j
public class MyBatisBootStrap {

    public static void main(String[] args) {
        try {
            // 1. 读取配置
            InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
            // 2. 创建SqlSessionFactory工厂
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            // 3. 获取sqlSession
            SqlSession sqlSession = sqlSessionFactory.openSession();
            // 4. 获取Mapper
            TTestUserMapper userMapper = sqlSession.getMapper(TTestUserMapper.class);
            // 5. 执行接口方法
            TTestUser userInfo = userMapper.selectByPrimaryKey(16L);
            System.out.println("userInfo = " + JSONUtil.toJsonStr(userInfo));
            // 6. 提交事物
            sqlSession.commit();
            // 7. 关闭资源
            sqlSession.close();
            inputStream.close();
        } catch (Exception e){
            log.error(e.getMessage(), e);
        }
    }
}

2. 读取配置

// 1. 读取配置
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");

在mybatis-config.xml中我们配置了属性,环境,映射文件路径等,其实不仅可以配置以上内容,还可以配置插件,反射工厂,类型处理器等等其它内容。在启动流程中的第一步我们就需要读取这个配置文件,并获取一个输入流为下一步解析配置文件作准备。

mybatis-config.xml 内容如下

<?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>

    <!--一些重要的全局配置-->
    <settings>
        <setting name="cacheEnabled" value="true"/>
        <!--<setting name="lazyLoadingEnabled" value="true"/>-->
        <!--<setting name="multipleResultSetsEnabled" value="true"/>-->
        <!--<setting name="useColumnLabel" value="true"/>-->
        <!--<setting name="useGeneratedKeys" value="false"/>-->
        <!--<setting name="autoMappingBehavior" value="PARTIAL"/>-->
        <!--<setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>-->
        <!--<setting name="defaultExecutorType" value="SIMPLE"/>-->
        <!--<setting name="defaultStatementTimeout" value="25"/>-->
        <!--<setting name="defaultFetchSize" value="100"/>-->
        <!--<setting name="safeRowBoundsEnabled" value="false"/>-->
        <!--<setting name="mapUnderscoreToCamelCase" value="false"/>-->
        <!--<setting name="localCacheScope" value="STATEMENT"/>-->
        <!--<setting name="jdbcTypeForNull" value="OTHER"/>-->
        <!--<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>-->
        <!--<setting name="logImpl" value="STDOUT_LOGGING" />-->
    </settings>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://10.255.0.50:3306/volvo_bev?useUnicode=true"/>
                <property name="username" value="appdev"/>
                <property name="password" value="FEGwo3EzsdDYS9ooYKGCjRQepkwG"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <!--这边可以使用package和resource两种方式加载mapper-->
        <!--<package name="包名"/>-->
        <!--<mapper resource="./mappers/SysUserMapper.xml"/>
        <package name="com.example.demo.dao"/> -->
        <mapper resource="./mapper/TTestUserMapper.xml"/>
    </mappers>

</configuration>

3. 创建SqlSessionFactory工厂

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

我们在学习Java的设计模式时,会学到工厂模式,工厂模式又分为简单工厂模式,工厂方法模式,抽象工厂模式等等。工厂模式就是为了创建对象提供接口,并将创建对象的具体细节屏蔽起来,从而可以提高灵活性。

public interface SqlSessionFactory {

  SqlSession openSession();

  SqlSession openSession(boolean autoCommit);

  SqlSession openSession(Connection connection);

  SqlSession openSession(TransactionIsolationLevel level);

  SqlSession openSession(ExecutorType execType);

  SqlSession openSession(ExecutorType execType, boolean autoCommit);

  SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);

  SqlSession openSession(ExecutorType execType, Connection connection);

  Configuration getConfiguration();

}

由此可知SqlSessionFactory工厂是为了创建一个对象而生的,其产出的对象就是SqlSession对象。SqlSession是MyBatis面向数据库的高级接口,其提供了执行查询sql,更新sql,提交事物,回滚事物,获取映射代理类等等方法。

在此笔者列出了主要方法,一些重载的方法就过滤掉了。

public interface SqlSession extends Closeable {

  /**
  * 查询一个结果对象
  **/ 
  <T> T selectOne(String statement, Object parameter);
  
   /**
  * 查询一个结果集合
  **/ 
  <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds);
  
   /**
  * 查询一个map
  **/ 
  <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds);

   /**
  * 查询游标
  **/ 
  <T> Cursor<T> selectCursor(String statement, Object parameter, RowBounds rowBounds);

  void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler);
  
     /**
  * 插入
  **/ 
  int insert(String statement, Object parameter);

    /**
  * 修改
  **/ 
  int update(String statement, Object parameter);

  /**
  * 删除
  **/
  int delete(String statement, Object parameter);

   /**
  * 提交事物
  **/
  void commit(boolean force);
  
   /**
  * 回滚事物
  **/
  void rollback(boolean force);

  List<BatchResult> flushStatements();

  void close();

  void clearCache();

  Configuration getConfiguration();

   /**
  * 获取映射代理类
  **/
  <T> T getMapper(Class<T> type);

   /**
  * 获取数据库连接
  **/
  Connection getConnection();
}

回到开始,SqlSessionFactory工厂是怎么创建的出来的呢?SqlSessionFactoryBuilder就是创建者,以Builder结尾我们很容易想到了Java设计模式中的建造者模式,一个对象的创建是由众多复杂对象组成的,建造者模式就是一个创建复杂对象的选择,它与工厂模式相比,建造者模式更加关注零件装配的顺序。

public class SqlSessionFactoryBuilder {

  public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        inputStream.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }

}

其中XMLConfigBuilder就是解析mybatis-config.xml中每个标签的内容,parse()方法返回的就是一个Configuration对象.Configuration也是MyBatis中一个很重要的组件,包括插件,对象工厂,反射工厂,映射文件,类型解析器等等都存储在Configuration对象中。

public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

  private void parseConfiguration(XNode root) {
    try {
      // issue #117 read properties first
      // 解析properties节点
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(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);
    }
  }

在获取到Configuration对象后,SqlSessionFactoryBuilder就会创建一个DefaultSqlSessionFactory对象,DefaultSqlSessionFactory是SqlSessionFactory的一个默认实现,还有一个实现是SqlSessionManager。

  public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }
创建SqlSessionFactory过程

4. 获取sqlSession

  // 3. 获取sqlSession
 SqlSession sqlSession = sqlSessionFactory.openSession();

在前面我们讲到,sqlSession是操作数据库的高级接口,我们操作数据库都是通过这个接口操作的。获取sqlSession有两种方式,一种是从数据源中获取的,还有一种是从连接中获取。
获取到的都是DefaultSqlSession对象,也就是sqlSession的默认实现。

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

private SqlSession openSessionFromConnection(ExecutorType execType, Connection connection) {
    try {
      boolean autoCommit;
      try {
        autoCommit = connection.getAutoCommit();
      } catch (SQLException e) {
        // Failover to true, as most poor drivers
        // or databases won't support transactions
        autoCommit = true;
      }
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      final Transaction tx = transactionFactory.newTransaction(connection);
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }
获取SqlSession步骤

5. 获取Mapper代理类

在上一步获取到sqlSession后,我们接下来就获取到了mapper代理类。

 // 4. 获取Mapper
 TTestUserMapper userMapper = sqlSession.getMapper(TTestUserMapper.class);

这个getMapper方法,我们看看DefaultSqlSession是怎么做的

DefaultSqlSession 的 getMapper 方法

  public <T> T getMapper(Class<T> type) {
       return this.configuration.getMapper(type, this);
   }

Configuration 的 getMapper 方法

  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        return this.mapperRegistry.getMapper(type, sqlSession);
    }

MapperRegistry 中有个getMapper方法,实际上是从成员变量knownMappers中获取的,这个knownMappers是个key-value形式的缓存,key是mapper接口的class对象,value是MapperProxyFactory代理工厂,这个工厂就是用来创建MapperProxy代理类的。


public class MapperRegistry {
    private final Configuration config;
    private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap();

    public MapperRegistry(Configuration config) {
        this.config = config;
    }

    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);
        if (mapperProxyFactory == null) {
            throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
        } else {
            try {
                return mapperProxyFactory.newInstance(sqlSession);
            } catch (Exception var5) {
                throw new BindingException("Error getting mapper instance. Cause: " + var5, var5);
            }
        }
    }
}

如果对java动态代理了解的同学就知道,Proxy.newProxyInstance()方法可以创建出一个目标对象一个代理对象。由此可知每次调用getMapper方法都会创建出一个代理类出来。

public class MapperProxyFactory<T> {
    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap();

    public MapperProxyFactory(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    public Class<T> getMapperInterface() {
        return this.mapperInterface;
    }

    public Map<Method, MapperMethod> getMethodCache() {
        return this.methodCache;
    }

    protected T newInstance(MapperProxy<T> mapperProxy) {
        return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
    }

    public T newInstance(SqlSession sqlSession) {
        MapperProxy<T> mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);
        return this.newInstance(mapperProxy);
    }
}

回到上面,那这个MapperProxyFactory是怎么加载到MapperRegistry的knownMappers缓存中的呢?

在上面的Configuration类的parseConfiguration方法中,我们会解析 mappers标签,mapperElement方法就会解析mapper接口。

private void parseConfiguration(XNode root) {
    try {
      // issue #117 read properties first
      // 解析properties节点
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(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);
    }
  }
  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();
          } 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();
          } 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.");
          }
        }
      }
    }
  }

解析完后,就讲这个mapper接口加到 mapperRegistry中,

configuration.addMapper(mapperInterface);

Configuration的addMapper方法

  public <T> void addMapper(Class<T> type) {
    mapperRegistry.addMapper(type);
  }

最后还是加载到了MapperRegistry的knownMappers中去了

  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);
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }
获取mapper代理类过程

6. 执行mapper接口方法

 // 5. 执行接口方法
 TTestUser userInfo = userMapper.selectByPrimaryKey(16L);

selectByPrimaryKey是TTestUserMapper接口中定义的一个方法,但是我们没有编写TTestUserMapper接口的的实现类,那么Mybatis是怎么帮我们执行的呢?前面讲到,获取mapper对象时,是会获取到一个MapperProxyFactory工厂类,并创建一个MapperProxy代理类,在执行Mapper接口的方法时,会调用MapperProxy的invoke方法。

 @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else {
        return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

如果是Object的方法就直接执行,否则执行cachedInvoker(method).invoke(proxy, method, args, sqlSession); 这行代码,到这里,想必有部分同学已经头晕了吧。怎么又来了个invoke方法。
cachedInvoker 是返回缓存的MapperMethodInvoker对象,MapperMethodInvoker的invoke方法会执行MapperMethod的execute方法。

public class MapperMethod {

  private final SqlCommand command;
  private final MethodSignature method;

  public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
    this.command = new SqlCommand(config, mapperInterface, method);
    this.method = new MethodSignature(config, mapperInterface, method);
  }

  public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case INSERT: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      case UPDATE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.update(command.getName(), param));
        break;
      }
      case DELETE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.delete(command.getName(), param));
        break;
      }
      case SELECT:
        if (method.returnsVoid() && method.hasResultHandler()) {
          executeWithResultHandler(sqlSession, args);
          result = null;
        } else if (method.returnsMany()) {
          result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {
          result = executeForMap(sqlSession, args);
        } else if (method.returnsCursor()) {
          result = executeForCursor(sqlSession, args);
        } else {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = sqlSession.selectOne(command.getName(), param);
          if (method.returnsOptional()
              && (result == null || !method.getReturnType().equals(result.getClass()))) {
            result = Optional.ofNullable(result);
          }
        }
        break;
      case FLUSH:
        result = sqlSession.flushStatements();
        break;
      default:
        throw new BindingException("Unknown execution method for: " + command.getName());
    }
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
      throw new BindingException("Mapper method '" + command.getName()
          + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
  }
}

然后根据执行的接口找到mapper.xml中配置的sql,并处理参数,然后执行返回结果处理结果等步骤。

7. 提交事物

// 6. 提交事物
sqlSession.commit();

事物就是将若干数据库操作看成一个单元,要么全部成功,要么全部失败,如果失败了,则会执行执行回滚操作,恢复到开始执行的数据库状态。

8. 关闭资源

 // 7. 关闭资源
sqlSession.close();
inputStream.close();

sqlSession是种共用资源,用完了要返回到池子中,以供其它地方使用。

9. 总结

至此我们已经大致了解了Mybatis启动时的大致流程,很多细节都还没有详细介绍,这是因为涉及到的层面又深又广,如果在一篇文章中介绍,反而会让读者如置云里雾里,不知所云。因此,在接下来我将每个模块的详细介绍。如果文章有什么错误或者需要改进的,希望同学们指出来,希望对大家有帮助。

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

推荐阅读更多精彩内容