Mybatis源码阅读(一) 配置文件的加载及查询过程

目标
1、 掌握 MyBatis 的工作流程
2、 掌握 MyBatis 的架构分层与模块划分
3、 掌握 MyBatis 缓存机制
4、 通过阅读 MyBatis 源码掌握 MyBatis 底层工作原理与设计思想

首先在 MyBatis 启动的时候我们要去解析配置文件,包括全局配置文件和映射器配置文件,这里面包含了我们怎么控制 MyBatis 的行为,和我们要对数据库下达的指令,也就是我们的 SQL 信息。我们会把它们解析成一个 Configuration 对象。

接下来就是我们操作数据库的接口,它在应用程序和数据库中间,代表我们跟数据库之间的一次连接:这个就是 SqlSession 对象。

我 们 要 获 得 一 个 会 话 , 必须有一 个会话工厂SqlSessionFactory 。SqlSessionFactory 里面又必须包含我们的所有的配置信息,所以我们会通过一个Builder 来创建工厂类。

我们知道,MyBatis 是对 JDBC 的封装,也就是意味着底层一定会出现 JDBC 的一些核心对象,比如执行 SQL 的 Statement,结果集 ResultSet。在 Mybatis 里面,SqlSession 只是提供给应用的一个接口,还不是 SQL 的真正的执行对象。SqlSession 持有了一个 Executor 对象,用来封装对数据库的操作。

在执行器 Executor 执行 query 或者 update 操作的时候我们创建一系列的对象,来处理参数、执行 SQL、处理结果集,这里我们把它简化成一个对象:StatementHandler,在阅读源码的时候我们再去了解还有什么其他的对象。

这个就是 MyBatis 主要的工作流程,如图:

MyBatis 架构分层与模块划分

在 MyBatis 的主要工作流程里面,不同的功能是由很多不同的类协作完成的,它们分布在 MyBatis jar 包的不同的 package 里面。
我们来看一下 MyBatis 的 jar 包(基于 3.5.4)



大概有 300 多个类,这样看起来不够清楚,不知道什么类在什么环节工作,属于什么层次。
跟 Spring 一样,MyBatis 按照功能职责的不同,所有的 package 可以分成不同的工作层次。
我们可以把 MyBatis 的工作流程类比成餐厅的服务流程。

第一个是跟客户打交道的服务员,它是用来接收程序的工作指令的,我们把它叫做接口层。

第二个是后台的厨师,他们根据客户的点菜单,把原材料加工成成品,然后传到窗口。这一层是真正去操作数据的,我们把它叫做核心层。

最后就是餐厅也需要有人做后勤(比如清洁、采购、财务),来支持厨师的工作和整个餐厅的运营。我们把它叫做基础层。

来看一下这张图,我们根据刚才的分层,和大体的执行流程,做了这么一个总结。当然,从不同的角度来描述,架构图的划分有所区别,这张图画起来也有很多形式。我们先从总体上建立一个印象。每一层的主要对象和主要的功能我们也给大家分析一下。

image.png

接口层

首先接口层是我们打交道最多的。核心对象是 SqlSession,它是上层应用和 MyBatis打交道的桥梁,SqlSession 上定义了非常多的对数据库的操作方法。接口层在接收到调用请求的时候,会调用核心处理层的相应模块来完成具体的数据库操作。

核心处理层

接下来是核心处理层。既然叫核心处理层,也就是跟数据库操作相关的动作都是在这一层完成的。核心处理层主要做了这几件事:

  1. 把接口中传入的参数解析并且映射成 JDBC 类型;
  2. 解析 xml 文件中的 SQL 语句,包括插入参数,和动态 SQL 的生成;
  3. 执行 SQL 语句;
  4. 处理结果集,并映射成 Java 对象。
    插件也属于核心层,这是由它的工作方式和拦截的对象决定的。

基础支持层

最后一个就是基础支持层。基础支持层主要是一些抽取出来的通用的功能(实现复用),用来支持核心处理层的功能。比如数据源、缓存、日志、xml 解析、反射、IO、事务等等这些功能。

好了,大体架构讲解清楚了,让我们正式开始阅读源码吧

MyBatis 源码解读

分析源码,我们还是从编程式的 demo 入手,先贴上我的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>

    <properties resource="db.properties"></properties>
    <settings>
        <!-- 打印查询语句 -->
        <setting name="logImpl" value="STDOUT_LOGGING" />

        <!-- 控制全局缓存(二级缓存)-->
        <setting name="cacheEnabled" value="true"/>

        <!-- 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。默认 false  -->
        <setting name="lazyLoadingEnabled" value="true"/>
        <!-- 当开启时,任何方法的调用都会加载该对象的所有属性。默认 false,可通过select标签的 fetchType来覆盖-->
        <setting name="aggressiveLazyLoading" value="false"/>
        <!--  Mybatis 创建具有延迟加载能力的对象所用到的代理工具,默认JAVASSIST -->
        <!--<setting name="proxyFactory" value="CGLIB" />-->
        <!-- STATEMENT级别的缓存,使一级缓存,只针对当前执行的这一statement有效 -->
        <!--
                <setting name="localCacheScope" value="STATEMENT"/>
        -->
        <setting name="localCacheScope" value="SESSION"/>
    </settings>

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

<!--    <typeHandlers>
        <typeHandler handler="com.caecc.type.MyTypeHandler"></typeHandler>
    </typeHandlers>-->

    <!-- 对象工厂 -->
<!--    <objectFactory type="com.caecc.objectfactory.GPObjectFactory">
        <property name="gupao" value="666"/>
    </objectFactory>-->

<!--    <plugins>
        <plugin interceptor="com.caecc.interceptor.SQLInterceptor">
            <property name="gupao" value="betterme" />
        </plugin>
        <plugin interceptor="com.caecc.interceptor.MyPageInterceptor">
        </plugin>
    </plugins>-->

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/><!-- 单独使用时配置成MANAGED没有事务 -->
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <mapper resource="BlogMapper.xml"/>
        <mapper resource="BlogMapperExt.xml"/>
    </mappers>

</configuration>

编写测试类:

    @Test
    public void testSelect() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        SqlSession session = sqlSessionFactory.openSession(); // ExecutorType.BATCH
        try {
            BlogMapper mapper = session.getMapper(BlogMapper.class);
            Blog blog = mapper.selectBlogById(1);
            System.out.println(blog);
        } finally {
            session.close();
        }
    }

把文件读取成流的这一步我们就省略了。所以下面我们分成四步来分析

第一步,我们通过建造者模式创建一个工厂类,配置文件的解析就是在这一步完成的,包括 mybatis-config.xml 和 Mapper 适配器文件。
问题:解析的时候怎么解析的,做了什么,产生了什么对象,结果存放到了哪里。解析的结果决定着我们后面有什么对象可以使用,和到哪里去取。

第二步,通过 SqlSessionFactory 创建一个 SqlSession。
问题:SqlSession 是用来操作数据库的,返回了什么实现类,除了 SqlSession,还创建了什么对象,创建了什么环境?

第三步,获得一个 Mapper 对象。
问题:Mapper 是一个接口,没有实现类,是不能被实例化的,那获取到的这个Mapper 对象是什么对象?为什么要从 SqlSession 里面去获取?为什么传进去一个接口,然后还要用接口类型来接收?

第四步,调用接口方法。
问题:我们的接口没有创建实现类,为什么可以调用它的方法?那它调用的是什么方法?它又是根据什么找到我们要执行的 SQL 的?也就是接口方法怎么和 XML 映射器里面的 StatementID 关联起来的?

此外,我们的方法参数是怎么转换成 SQL 参数的?获取到的结果集是怎么转换成对象的?接下来我们就会详细分析每一步的流程,包括里面有哪些核心的对象和关键的方法

一、 配置解析过程

首先我们要清楚的是配置解析的过程全部只解析了两种文件。一个是mybatis-config.xml 全局配置文件。另外就是可能有很多个的 Mapper.xml 文件,也包括在 Mapper 接口类上面定义的注解。

我们从 mybatis-config.xml 开始。

        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

SqlSessionFactoryBuilder这个类特别简单,处理构造函数就是build函数,看着挺多,实际都是重载的函数。

public class SqlSessionFactoryBuilder {

  public SqlSessionFactory build(Reader reader) {
    return build(reader, null, null);
  }

  public SqlSessionFactory build(Reader reader, String environment) {
    return build(reader, environment, null);
  }

  public SqlSessionFactory build(Reader reader, Properties properties) {
    return build(reader, null, properties);
  }

  public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
      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.
      }
    }
  }

  public SqlSessionFactory build(InputStream inputStream) {
    return build(inputStream, null, null);
  }

  public SqlSessionFactory build(InputStream inputStream, String environment) {
    return build(inputStream, environment, null);
  }

  public SqlSessionFactory build(InputStream inputStream, Properties properties) {
    return build(inputStream, null, properties);
  }

  public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      // 用于解析 mybatis-config.xml,同时创建了 Configuration 对象 >>
      XMLConfigBuilder parser   = new XMLConfigBuilder(inputStream, environment, properties);
      // 解析XML,最终返回一个 DefaultSqlSessionFactory >>
      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.
      }
    }
  }

  public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }

}

非常明显的建造者模式,它里面定义了很多个 build 方法的重载,最终返回的是一个SqlSessionFactory 对象(单例模式)。我们点进去 build 方法。build核心的重载函数:

  public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      // 用于解析 mybatis-config.xml,同时创建了 Configuration 对象 >>
      XMLConfigBuilder parser   = new XMLConfigBuilder(inputStream, environment, properties);
      // 解析XML,最终返回一个 DefaultSqlSessionFactory >>
      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 对象(Configuration 对象也是这个时候创建的)。

XMLConfigBuilder

XMLConfigBuilder 是抽象类 BaseBuilder 的一个子类,专门用来解析全局配置文件,针对不同的构建目标还有其他的一些子类,比如:
XMLMapperBuilder:解析 Mapper 映射器
XMLStatementBuilder:解析增删改查标签

image.png

根据我们解析的文件流,这里后面两个参数都是空的,创建了一个 parser。这里有两步,第一步是调用 parser 的 parse()方法,它会返回一个 Configuration类。其实BaseBuilder作为抽象类,它这个名字起的不好,如果是spring代码,肯定起AbstractBaseBuilder这个名字(懂的都懂)。插一嘴,程序员比的是代码量吗?不,是设计思想。

配置文件里面所有的信息都会放在Configuration 里面。其实Concifuration就放在基类BaseBuilder中:


番外:源码看多了的读者此时就能猜到,里面不是8个基本变量,就是各种集合。我工作写框架就这么干,看名字觉着牛逼,到最底层就是各种变量和集合的维护而已。

Configuration 类里面有很多的属性(自己去看,我不可能都贴不出来,想得美),有很多是跟 config 里面的标签直接对应的,一目了然。其实mybatis的源码特别直观易读,比Spring好读多了,没那么绕,Spring读完一遍,人疯了。那么Configuration啥时候创建的呢,就在XMLConfigBuilder的构造器里:



简单看一下Configuration的构造方法:



什么? 原来就是别名注册,啥叫注册?大部分源码框架里的注册就是往HashMap里面堆数据!不信你看:

这时候看一下的我配置文件的这个配置:


我们先看一下 parse()方法:
首先会检查是不是已经解析过,也就是说在应用的生命周期里面,config 配置文件只需要解析一次,生成的 Configuration 对象也会存在应用的整个生命周期中。

  public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    // XPathParser,dom 和 SAX 都有用到 >>
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

接下来就是 parseConfiguration 方法:

  private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      // 对于全局配置文件各种标签的解析
      propertiesElement(root.evalNode("properties"));
      // 解析 settings 标签
      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"));
      // settings 子标签赋值,默认值就是在这里提供的 >>
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      // 创建了数据源 >>
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      // 解析引用的Mapper映射器
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

全是解析的工作。这下面有十几个方法,对应着 config 文件里面的所有一级标签。接下来我们就挨个分析分析,我就喜欢一行一行扣源码,真有意思。

propertiesElement(root.evalNode("properties"));

第一个是解析<properties>标签,读取我们引入的外部配置文件。这里面又有两种类型,一种是放在 resource 目录下的,是相对路径,一种是写的绝对路径的。
看我的mybatis.config中有这么一个配置




看源码,我很用心写了注释,相信你看的懂:



解析的最终结果就是我们会把所有的配置信息放到名为 defaults 的 Properties 对象里面,最后把XPathParser 和 Configuration 的 Properties 属性都设置成我们填充后的 Properties对象。其实指向的都是同一个对象。

settingsAsProperties()

第二个,我们把<settings>标签也解析成了一个 Properties 对象,对于<settings>标签的子标签的处理在后面。



对应mybatis-config.xml配置文件中:


loadCustomVfs(settings)

对这个方法不感兴趣,我用不到!不读了。

loadCustomLogImpl(settings)

loadCustomLogImpl 是根据<logImpl>标签获取日志的实现类,我们可以用到很多的日志的方案,包括 LOG4J,LOG4J2,SLF4J 等等。这里生成了一个 Log 接口的实现类,并且赋值到 Configuration 中。
源码很简单,不贴了,反正没人看,我就是做笔记而已。

typeAliasesElement()

接下来,我们解析<typeAliases>标签,我们在讲配置的时候也讲过,它有两种定义方式,一种是直接定义一个类的别名,一种就是指定一个包,那么这个 package 下面所有的类的名字就会成为这个类全路径的别名。类的别名和类的关系,我们放在一个 TypeAliasRegistry 对象里面。



pluginElement()

接下来就是解析<plugins>标签,比如 Pagehelper 的翻页插件,或者我们自定义的插件。<plugins>标签里面只有<plugin>标签,<plugin>标签里面只有<property>标签。
标签解析完以后,会生成一个 Interceptor 对象,并且添加到 Configuration 的InterceptorChain 属性里面,它是一个 List。这个后面再说。

objectFactoryElement()、 objectWrapperFactoryElement()

接下来的两个标签是用来实例化对象用的 ,<objectFactory>和<objectWrapperFactory> 这两个标 签 ,分别生成ObjectFactory 、ObjectWrapperFactory对象,通过反射生成对象设置到 Configuration 的属性里面。



reflectorFactoryElement()

解析 reflectorFactory 标签,生成 ReflectorFactory 对象(在官方 的 pdf 文档里面没有找到这个配置)。反射工具箱。

settingsElement(settings)

这里就是对<settings>标签里面所有子标签的处理了,前面我们已经把子标签全部转换成了 Properties 对象,所以在这里处理 Properties 对象就可以了。

二级标签里面有很多的配置,比如二级缓存,延迟加载,自动生成主键这些。需要注意的是,我们之前提到的所有的默认值,都是在这里赋值的。如果说后面我们不知道这个属性的值是什么,也可以到这一步来确认一下。
所有的值,都会赋值到 Configuration 的属性里面去。



这些属性都设置在Configuration对象里,所以,当你不知道setting下有哪些属性可以设置时,不妨看看这里,我这里从别的文章中截图简要说明一下上面属性的意思,到目前为止只是解析配置,至于怎么用这些配置,别着急,后面会说。



image.png



大体有个印象,回查。

environmentsElement()

这一步是解析<environments>标签。一个 environment 就是对应一个数据源,所以在这里我们会根据配置的<transactionManager>创建一个事务工厂,根据<dataSource>标签创建一个数据源,最后把这两个对象设置成 Environment 对象的属性,放到 Configuration 里面。关于数据库连接池,读者(我自己)可以参考这篇文章https://blog.csdn.net/crave_shy/article/details/46611205

databaseIdProviderElement()

解析 databaseIdProvider 标签,生成 DatabaseIdProvider 对象(用来支持不同厂商的数据库)。

typeHandlerElement()

跟 TypeAlias 一样,TypeHandler 有两种配置方式,一种是单独配置一个类,一种是指定一个 package。最后我们得到的是JavaType和JdbcType,以及用来做相互映射的TypeHandler 之间的映射关系。最后存放在 TypeHandlerRegistry 对象里面。

mapperElement(root.evalNode("mappers"));

我们现在就要来定义 SQL 映射语句了。 但首先,我们需要告诉 MyBatis 到哪里去找到这些语句。 在自动查找资源方面,Java 并没有提供一个很好的解决方案,所以最好的办法是直接告诉 MyBatis 到哪里去找映射文件。 你可以使用相对于类路径的资源引用,或完全限定资源定位符(包括 file:/// 形式的 URL),或类名和包名等。例如:

<!-- 使用相对于类路径的资源引用 -->
<mappers>
  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
  <mapper resource="org/mybatis/builder/BlogMapper.xml"/>
  <mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>
<!-- 使用完全限定资源定位符(URL) -->
<mappers>
  <mapper url="file:///var/mappers/AuthorMapper.xml"/>
  <mapper url="file:///var/mappers/BlogMapper.xml"/>
  <mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>
<!-- 使用映射器接口实现类的完全限定类名 -->
<mappers>
  <mapper class="org.mybatis.builder.AuthorMapper"/>
  <mapper class="org.mybatis.builder.BlogMapper"/>
  <mapper class="org.mybatis.builder.PostMapper"/>
</mappers>
<!-- 将包内的映射器接口实现全部注册为映射器 -->
<mappers>
  <package name="org.mybatis.builder"/>
</mappers>

这些配置会告诉 MyBatis 去哪里找映射文件,剩下的细节就应该是每个 SQL 映射文件了,这个没啥还纠结的,定死的,代码无非就是按这几种方式解析,if..else..罢了。无论是按 package 扫描,还是按接口扫描,最后都会调用到 MapperRegistry 的addMapper()方法。MapperRegistry 里面维护的其实是一个 Map 容器,存储接口和代理工厂的映射关系。详细看一下如何解析的Mapper吧:

// 通过解析 mappers 节点,找到Mapper文件
  private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        // 查看mappers节点中是否有 package 节点,有就解析,否则就解析 mapper子节点
        /*
         <mappers>
          <package name="com.test"/>
          <!-- <mapper resource="mapper/DemoMapper.xml"/> -->
        </mappers>
         */
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          // 解析 mapper 节点,mapper 节点中有三个属性(resource、url、class),但是只能存在一个
          /**
           * resource:表示文件夹下xml文件
           * <mapper resource="mapper/DemoMapper.xml"/>
           *
           * class:DemoMapper 动态代理接口,DemoMapper.xml文件
           * <mapper class="com.test.DemoMapper" />
           *
           * url:表示盘符下的绝对路径,绝对不推荐使用
           * <mapper resource="F:\javaEE\workspace\MyBatisDemo_My\mybatis-3-master\src\main\resources\mapper\DemoMapper.xml"/>
           *
           */
          /*
         <mappers>
          <!-- <package name="com.test"/> -->
          <mapper resource="mapper/DemoMapper.xml"/>
        </mappers>
         */
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          // 三个属性只能是其中的一个有value,否则就抛异常
          if (resource != null && url == null && mapperClass == null) {
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource);
            // 如果resource有值,就解析mapper.xml文件
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url != null && mapperClass == null) {
            ErrorContext.instance().resource(url);
            // 如果url有值,则通过绝对路径获取输入流,获取mapper.xml并解析
            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有值,就获取Class对象
            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.");
          }
        }
      }
    }
  }

首先是包扫描,啥意思?比如我的mybaits-config.xml文件是这么配置的(实际在springboot项目中就是则会么配置的):


意思就是扫描该包下所有的mapper文件。

 if ("package".equals(child.getName())) {
          //获取包的名称
          String mapperPackage = child.getStringAttribute("name");
          //解析包下的mapper文件
          configuration.addMappers(mapperPackage);
 }

进入到addMappers方法里面详细看一下“

  protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
 public void addMappers(String packageName) {
  //
    mapperRegistry.addMappers(packageName);
  }

还是configuration中的mapper注册工厂mapperRegistry,里面就是封装了HashMap



进入到addMappers方法



就是遍历这个包下的所有class,然后调用addMapper方法,进入看看
  public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {//判断是不是接口,Mapper文件是一个接口,后面会动态代理生成一个实体
      if (hasMapper(type)) {//判断改接口是否已经注册过了
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
        // !Map<Class<?>, MapperProxyFactory<?>> 存放的是接口类型,和对应的工厂类的关系,工厂就是存放了接口的类型及相关方法,将来反射用
        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.

        // 注册了接口之后,根据接口,开始解析所有方法上的注解,例如 @Select >>
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }

拿到mapper,后就看是解析这个mapper,最主要的就是这个MapperAnnotationBuilder的parse方法,根据名字一看就是注解解析(其实我们也很少在Mapper里以注解的方式写sql),看看吧:

  public void parse() {
    String resource = type.toString();
    if (!configuration.isResourceLoaded(resource)) {
      // 先判断 Mapper.xml 有没有解析,没有的话先解析 Mapper.xml(例如定义 package 方式)
      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()) {
            // 解析方法上的注解,添加到 MappedStatement 集合中 >>
            parseStatement(method);
          }
        } catch (IncompleteElementException e) {
          configuration.addIncompleteMethod(new MethodResolver(this, method));
        }
      }
    }
    parsePendingMethods();
  }

不知道你曾经好奇过没有,Mapper接口是怎么跟xml文件对应起来的,就是在这里,loadXmlResource这个方法:

  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
      InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
      if (inputStream == null) {
        // Search XML mapper that is not in the module but in the classpath.
        try {
          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());
        // 解析 Mapper.xml >>
        xmlParser.parse();
      }
    }
  }

他会根据你的类全路径名,在后面改成.xml,然后在相同的目录下找到这个xml文件,所以你的xml文件资源全路径必须跟类全路径一致。
xmlParser.parse();才是真正的解析xml文件:

  public void parse() {
    // 总体上做了两件事情,对于语句的注册和接口的注册
    if (!configuration.isResourceLoaded(resource)) {
      // 1、具体增删改查标签的解析。
      // 一个标签一个MappedStatement。 >>
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      // 2、把namespace(接口类型)和工厂类绑定起来,放到一个map。
      // 一个namespace 一个 MapperProxyFactory >>
      bindMapperForNamespace();
    }

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

parser.evalNode("/mapper")相当于拿到了整个mapper.xml的文件内容,然后依次解析各个标签,后面备用。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"));
      // 解析 cache 属性,添加缓存对象
      cacheElement(context.evalNode("cache"));
      // 创建 ParameterMapping 对象
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      // 创建 List<ResultMapping>
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      // 解析可以复用的SQL
      sqlElement(context.evalNodes("/mapper/sql"));
      // 解析增删改查标签,得到 MappedStatement >>
      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);
    }
  }

方法很多,我就不一一解析了,反正就是解析,然后存内存里,后面用的话会慢慢从内存中拿到配置就行匹配。
如果你不是根据package方法配置的,就走下面的resource和url,其实都是一样的,无非就是少了一层Mapper接口到xml的映射。不想写了,自己看。

小结

在这一步,我们主要完成了 config 配置文件、Mapper 文件、Mapper 接口上的注解的解析。
我们得到了一个最重要的对象 Configuration,这里面存放了全部的配置信息,它在属性里面还有各种各样的容器后,返回了一个 DefaultSqlSessionFactory,里面持有了 Configuration 的实例。

二、会话创建过程

解析工作结束了,所有东西都在Configuration,都是静态的,悄悄的躺在内存里,现在我们让他们动起来。
下一步访问数据库,访问之前我们需要先跟数据库建立一条链接,也就是创建一条会话

        SqlSession session = sqlSessionFactory.openSession();

进入DefaultSqlSessionFactory的openSession方法,一直跟下去:

  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();
    }
  }

首先向我们走来的是enviroment代表队,就是他啦


image.png

然后我们根据这个配置呢,就开始创建事务工厂,这里从Environment对象中取出一个TransactionFactory,它是解析evironments标签的时候创建的。autoCommit默认为false。我们的事务管理器是JDBC类型,所以我们返回来的事务也是JDBC类型:

  public Transaction newTransaction(DataSource ds, TransactionIsolationLevel level, boolean autoCommit) {
    return new JdbcTransaction(ds, level, autoCommit);
  }

他会使用connection对象的commit()、rollbach()、close()管理事务,这个connection就是驱动提供的链接
源码也很简单



如果配置成MANAGED,会把事务交给容器来管理,比如JBOSS,Weblogic。因为我们跑的是本地程序配置成MANAGED不会有任何事务。如果是spring+mybati,则没有必要配置,因为我们会在applicationContxt.xml里面配置数据源和事务管理器,覆盖mybatis的配置。

下一步就是创建执行器final Executor executor = configuration.newExecutor(tx, execType);,可以细分成3个步骤:

  1. 创建执行器
    Executor的基本类型有3种:
public enum ExecutorType {
  SIMPLE, REUSE, BATCH
}

默认就是SIMPLE

    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      // 默认 SimpleExecutor
      executor = new SimpleExecutor(this, transaction);
    }
image.png

一看到这个Base开头的抽象类,就知道他肯定存储着公共变量和公共方法,提高了代码的复用性,在策略模式和模板方法模式中很常用。在这里就用到了模板方法模式

模板方法模式,定义了一个算法骨架,并允许子类为一个或多个步骤提供实现。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法的某些步骤。

  1. 缓存装饰
    如果cacheEnable=true,会用装饰器模式对executor进行装饰。
   // 二级缓存开关,settings 中的 cacheEnabled 默认是 true
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
  1. 插件代理
// 植入插件的逻辑,至此,四大对象已经全部拦截完毕
    executor = (Executor) interceptorChain.pluginAll(executor);
  1. 返回SqlSession的实现
    最终返回DefaultSession,它的属性包括Configuration、Eexecutor对象。
    return new DefaultSqlSession(configuration, executor, autoCommit);

小结

创建会话的过程,我们获得了一个DefaultSqlSession,里面包含了一个Executor,Executor是SQL的实际执行对象。

三、获得Mapper对象

现在我们已经有一个 DefaultSqlSession 了,必须找到 Mapper.xml 里面定义的Statement ID,才能执行对应的 SQL 语句。找到 Statement ID 有两种方式:一种是直接调用 session 的方法,在参数里面传入Statement ID,这种方式属于硬编码,我们没办法知道有多少处调用,修改起来也很麻烦。

在 MyBatis 后期的版本提供了第二种方式,就是定义一个接口,然后再调用Mapper 接口的方法。由于我们的接口名称跟 Mapper.xml 的 namespace 是对应的,接口的方法跟statement ID 也都是对应的,所以根据方法就能找到对应的要执行的 SQL。
BlogMapper mapper = session.getMapper(BlogMapper.class);

在这里我们主要研究一下 Mapper 对象是怎么获得的,它的本质是什么。

DefaultSqlSession 的 getMapper()方法,调用了 Configuration 的 getMapper()方法。

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

Configuration 的 getMapper()方法,又调用了 MapperRegistry 的 getMapper()方法。

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

我们知道,在解析 mapper 标签和 Mapper.xml 的时候已经把接口类型和类型对应的MapperProxyFactory 放到了一个 Map 中。获取 Mapper 代理对象,实际上是从Map 中获取对应的工厂类后,调用以下方法创建对象:
mapperProxyFactory.newInstance(sqlSession);
最终通过代理模式返回代理对象:

  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy<T> mapperProxy) {
    // 1:类加载器:2:被代理类实现的接口、3:实现了 InvocationHandler 的触发管理类
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

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

获得 Mapper 对象的过程,实质上是获取了一个 MapperProxy 的代理对象。MapperProxy 中有 sqlSession、mapperInterface、methodCache。


image.png

四、执行SQL

Blog blog = mapper.selectBlog(1);
由于所有的 Mapper 都是 MapperProxy 代理对象,所以任意的方法都是执行MapperProxy 的 invoke()方法

  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      // toString hashCode equals getClass等方法,无需走到执行SQL的流程
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else {
        // 提升获取 mapperMethod 的效率,到 MapperMethodInvoker(内部接口) 的 invoke
        // 普通方法会走到 PlainMethodInvoker(内部类) 的 invoke
        return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

首先判断是否需要去执行 SQL,还是直接执行方法。Object 本身的方法和 Java 8 中接口的默认方法不需要去执行 SQL。
cachedInvoker会对方法做一个缓存处理

  private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
    try {
      // Java8 中 Map 的方法,根据 key 获取值,如果值是 null,则把后面Object 的值赋给 key
      // 如果获取不到,就创建
      // 获取的是 MapperMethodInvoker(接口) 对象,只有一个invoke方法
      return methodCache.computeIfAbsent(method, m -> {
        if (m.isDefault()) {
          // 接口的默认方法(Java8),只要实现接口都会继承接口的默认方法,例如 List.sort()
          try {
            if (privateLookupInMethod == null) {
              return new DefaultMethodInvoker(getMethodHandleJava8(method));
            } else {
              return new DefaultMethodInvoker(getMethodHandleJava9(method));
            }
          } catch (IllegalAccessException | InstantiationException | InvocationTargetException
              | NoSuchMethodException e) {
            throw new RuntimeException(e);
          }
        } else {
          // 创建了一个 MapperMethod
          return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
        }
      });
    } catch (RuntimeException re) {
      Throwable cause = re.getCause();
      throw cause == null ? re : cause;
    }
  }

这里加入缓存是为了提升 MapperMethod 的获取速度,Map 的 computeIfAbsent()方法:只有 key 不存在或者 value 为 null 的时候才调用 mappingFunction()。最后调用了下面方法:

 @Override
    public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
      // SQL执行的真正起点
      return mapperMethod.execute(sqlSession, args);
    }

MapperMethod里面主要有两个属性,一个是SqlCommand,一个是MethodSignature,这两个都是 MapperMethod 的内部类。


另外,还定义了多个execute方法


在这一步,根据不同的 type 和返回类型:
调用 convertArgsToSqlCommandParam()将参数转换为 SQL 的参数。
Object param = method.convertArgsToSqlCommandParam(args);
调用 sqlSession 的 insert()、update()、delete()、selectOne ()方法,我们以查询
为例,会走到 selectOne()方法。
result = sqlSession.selectOne(command.getName(), param);

DefaultSqlSession.selectOne()

public <T> T selectOne(String statement, Object parameter) {
    // 来到了 DefaultSqlSession
    // Popular vote was to return null on 0 results and throw exception on too many.
    List<T> list = this.selectList(statement, parameter);
    if (list.size() == 1) {
      return list.get(0);
    } else if (list.size() > 1) {
      throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
    } else {
      return null;
    }
  }  

selectOne()最终也是调用了 selectList()。

 public <E> List<E> selectList(String statement, Object parameter) {
    // 为了提供多种重载(简化方法使用),和默认值
    // 让参数少的调用参数多的方法,只实现一次
    return this.selectList(statement, parameter, RowBounds.DEFAULT);
  }

在 SelectList()中,我们先根据 command name(Statement ID)从 Configuration中拿到 MappedStatement,这个 ms 上面有我们在 xml 中配置的所有属性,包括 id、statementType、sqlSource、useCache、入参、出参等等。


然后执行了 Executor 的 query()方法。
前面我们说到了 Executor 有三种基本类型,同学们还记得是哪几种么?
SIMPLE/REUSE/BATCH,还有一种包装类型,CachingExecutor。
那么在这里到底会选择哪一种执行器呢?
我们要回过头去看看 DefaultSqlSession 在初始化的时候是怎么赋值的,这个就是我们的会话创建过程。
如果启用了二级缓存,就会先调用 CachingExecutor 的 query()方法,里面有缓存相关的操作,然后才是再调用基本类型的执行器,比如默认的 SimpleExecutor。
在没有开启二级缓存的情况下,先会走到 BaseExecutor 的 query()方法(否则会先走到 CachingExecutor)。


  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    // 获取SQL
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    // 创建CacheKey:什么样的SQL是同一条SQL? >>
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

首先找到绑定的sql,然后从 Configuration 中获取 MappedStatement, 然后从 BoundSql 中获取 SQL 信息,创建 CacheKey。这个 CacheKey 就是缓存的 Key。


image.png

CachingExecutor 走完了会走BaseExecutor 的query

  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    // 异常体系之 ErrorContext
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      // flushCache="true"时,即使是查询,也清空一级缓存
      clearLocalCache();
    }
    List<E> list;
    try {
      // 防止递归查询重复处理缓存
      queryStack++;
      // 查询一级缓存
      // ResultHandler 和 ResultSetHandler的区别
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        // 真正的查询流程
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

queryStack 用于记录查询栈,防止递归查询重复处理缓存。
flushCache=true 的时候,会先清理本地缓存(一级缓存):clearLocalCache();

如果没有缓存,会从数据库查询:queryFromDatabase()
如果 LocalCacheScope == STATEMENT,会清理本地缓存。

queryFromDatabase()执行方法如下:


  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    // 先占位
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      // 三种 Executor 的区别,看doUpdate
      // 默认Simple
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      // 移除占位符
      localCache.removeObject(key);
    }
    // 写入一级缓存
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

先在缓存用占位符占位。执行查询后,移除占位符,放入数据。执行 Executor 的 doQuery(),默认是 SimpleExecutor。

  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();
      // 注意,已经来到SQL处理的关键对象 StatementHandler >>
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      // 获取一个 Statement对象
      stmt = prepareStatement(handler, ms.getStatementLog());
      // 执行查询
      return handler.query(stmt, resultHandler);
    } finally {
      // 用完就关闭
      closeStatement(stmt);
    }
  }

  1. 创建 StatementHandler
    在 configuration.newStatementHandler()中,new 一个 StatementHandler,先得到 RoutingStatementHandler。
    RoutingStatementHandler里面没有任何的实现 ,是用来创建基本的StatementHandler 的。这里会根据 MappedStatement 里面的 statementType 决定StatementHandler 的 类 型 。 默 认 是 PREPARED ( STATEMENT 、 PREPARED 、CALLABLE)。
    StatementHandler 里面包含了处理参数的 ParameterHandler 和处理结果集的ResultSetHandler。

  2. 创建 Statement
    用 new 出来的 StatementHandler 创建 Statement 对象——prepareStatement()方法对语句进行预编译,处理参数。
    handler.parameterize(stmt) ;

  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    // 获取 Statement 对象
    stmt = handler.prepare(connection, transaction.getTimeout());
    // 为 Statement 设置参数
    handler.parameterize(stmt);
    return stmt;
  }
  1. 执行的 StatementHandler 的 query()方法
    RoutingStatementHandler 的 query()方法。
    delegate 委派,最终执行 PreparedStatementHandler 的 query()方法。

  2. 执行 PreparedStatement 的 execute()方法
    后面就是 JDBC 包中的 PreparedStatement 的执行了。

  3. ResultSetHandler 处理结果集
    return resultSetHandler.handleResultSets(ps);
    ResultSetHandler 只有一个实现类:DefaultResultSetHandler。也就是执DefaultResultSetHandler 的 handleResultSets ()方法。
    首先我们会先拿到第一个结果集,如果没有配置一个查询返回多个结果集的情况,一般只有一个结果集。如果下面的这个 while 循环我们也不用,就是执行一次。然后会调用 handleResultSet()方法。

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

推荐阅读更多精彩内容