MyBatis流程及源码分析(一)

        扑街前言:本来dubbo结束之后,应该是要写SpringCloud相关的组件内容的,但是因为目前本人要去重新面试,所以先整理一下myBatis、Spring、SpringBoot之类的源码分析,至于SpringCloud后面有时间了再说。本次说一下mybatis的源码分析,原本计划是一篇文章就搞定的,结果发现还是不够写,那么本篇就只分析mybatis 的结构设计和传统方式调用的源码,后续的代理方式也就是Spring 结合的注解调用模式下篇文章再说。


目录

MyBatis的架构设计

MyBatis的各个组件

MyBatis源码分析

sqlSessionFactory的创建,又叫封装Configuration

解析配置文件,并封装到Configuration不同属性

解析sql配置文件,并封装到Configuration的mapperRegistry集合

构建sql对象,替换sql占位符,保存对应对象信息

封装sql对象至configuration的mappedStatements集合

SqlSessionFactory的openSession

CachingExecutor执行器的调用

StatementHandler语句执行的构建

PreparedStatement对象执行sql和结果集的处理


MyBatis的架构设计

        老规矩看框架源码先看框架的架构设计,先上图。说明一下下面几层的大体职责,首先是接口层:对外提供增删改查接口,接着就是数据处理层:设置参数、sql解析、封装结果集等,然后就是框架支撑层:提供了一些支持上层的基础功能,最后是引导层:就是提供配置信息。那么本篇文章的重点就是数据处理层和框架支撑层的源码解析。


MyBatis的各个组件

        先上图,从图来看,首先就是sqlSession 就是对外提供了一些课调用的方法,比如:selectList()等,然后就是把操作委派给了executor 做的,executor 执行器主要就是执行sql,维护两级别的缓存,然后就是StatementHandler 语句执行器,这个就是封装了jdbc statement,那么语句执行器去执行的时候,一定是要有sql 的,sql 来源就是configuration 全局配置对象,configuration 对象里面,又有一个属性对象叫做MappedStatement,它是由解析映射配置文件形成的map 集合,语句执行器真正要执行的sql 就是放在了MappedStatement 的SQLSource属性中,这里获取到的sql 还是配置文件中的sql 语句,是存在占位符的,所有要到下一层的parameterHandler 参数处理器,这里它还会用到TypeHandler 类型转换器,目的就是将sql 拼接完整,然后就是Statement 对象进行真正的sql 执行了,执行完整之后,如果有返回值的话,就会有一个返回结果集,然后就是经过resultSetHandler 结果集处理器,后面就是原来返回了。


MyBatis源码分析

        大体介绍了架构和组件,我们现在就开始本篇的正题MyBatis源码的分析,首先可以将我的资源中的注释版源码下载下来,地址:myBatis源码(注释版)-Java文档类资源-CSDN下载

        打开项目之后,我们可以先看到MyBatis 的测试类,首先我们看下通过xml配置的方式进行操作,看具体代码。第一个步是将配置文件通过类加载器,加载为字节输入流,然后构建sqlSessionFactory 工厂,再由工厂去获取具体的sqlSession,然后由sqlSession创建代理,再由代理调用具体的查询语句并返回接口集,然后就结束了。很简单的逻辑,我们要重点要看的是它怎么创建工厂的,代理又是怎么生成的,sql是怎么调用并返回结果集的,下面我们一步一步来。

@Test
public void test1() throws IOException {

  // 1. 通过类加载器对配置文件进行加载,加载成了字节输入流,存到内存中 注意:配置文件并没有被解析
  InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");

  // 2. (1)解析了配置文件,封装configuration对象 (2)创建了DefaultSqlSessionFactory工厂对象
  SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);

  // 3.问题:openSession()执行逻辑是什么?
  // 3. (1)创建事务对象 (2)创建了执行器对象cachingExecutor (3)创建了DefaultSqlSession对象
  SqlSession sqlSession = sqlSessionFactory.openSession();

  // 4. 委派给Executor来执行,Executor执行时又会调用很多其他组件(参数设置、解析sql的获取,sql的执行、结果集的封装)
  User user = sqlSession.selectOne("com.itheima.mapper.UserMapper.findByCondition", 1);

  System.out.println(user);
  System.out.println("MyBatis源码环境搭建成功....");

  sqlSession.close();

}

sqlSessionFactory的创建,又叫封装Configuration

        这里我们直接跟上面的代码内容,找到SqlSessionFactoryBuilder().build 方法,然后跟进就是下面代码。

        可以看到首先是获取到XMLConfigBuilder 对象,然后就是XMLConfigBuilder 对象的parse 方法,我为什么说是parse 方法而不是build 方法呢,因为build 方法就只是一个调用DefaultSqlSessionFactory 类的有参构造方法而已,就是将Configuration 放入而已,那么这里其实就可以知道XMLConfigBuilder 对象中就是为了将Configuration 创建出来。

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
	try {
	  // XMLConfigBuilder:用来解析XML配置文件
	  // 使用构建者模式:好处:降低耦合、分离复杂对象的创建
	  // 1.创建XPathParser解析器对象,根据inputStream解析成了document对象 2.创建全局配置对象Configuration对象
	  XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);

	  // parser.parse():使用XPATH解析XML配置文件,将配置文件封装到Configuration对象
	  // 返回DefaultSqlSessionFactory对象,该对象拥有Configuration对象(封装配置文件信息)
	  // parse():配置文件就解析完成了
	  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 的构建,这里是直接new 的有参构造,那么我们可以跟进去看下,代码如下。

        这里其实就是再次构建了一个XPathParser 对象,这个对象的目的就是基于java的xpath 解析器,将inputStream 字节输入流解析成了配置对象,这里的解析代码我们就不看了,有兴趣的可以搜一下这都是apache 提供的一些基础东西了。

public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
	// XPathParser基于 Java XPath 解析器,用于解析 MyBatis中的配置文件
	this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}

        我们继续跟到this 里面,这里就是本身的构造方法,代码如下。其实也没有什么东西,真正的解析并没有放在这里,super 也只是构建了一个基础的configuration,就是为了注册一些别名,为了后面的真正解析的时候使用,这快的代码太多我就截图展示,有兴趣可以自己跟一下。

private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
	//  创建Configuration对象,并通过TypeAliasRegistry注册一些Mybatis内部相关类的别名
	super(new Configuration());
	ErrorContext.instance().resource("SQL Mapper Configuration");
	this.configuration.setVariables(props);
	this.parsed = false;
	this.environment = environment;
	this.parser = parser;
}

解析配置文件,并封装到Configuration不同属性

         然后我们回到SqlSessionFactory 中,当XMLConfigBuilder 对象构建完成之后,跟着的就是parse 方法,这里我们就算不跟代码,现在已知道是就是为了生成一个configuration 对象,我们现在要看的也就是怎么生成的代码,先上代码。

        这里首先会判断parsed,这是一个私有的全局布尔值对象,只有先走了XMLConfigBuilder 的构造才会赋值为false。然后跟着的就是parseConfiguration 方法,这里其实就是根据一个一个的标签解析出不同的对象,注意这里的标签应该都是configuration的下级标签,解析到不同对象后再存入configuration 对象的不同属性中,这里逻辑相似,我们就不一个一个看看,我们选两个看下。

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

	// parser.evalNode("/configuration"):通过XPATH解析器,解析configuration根节点
	// 从configuration根节点开始解析,最终将解析出的内容封装到Configuration对象中
	parseConfiguration(parser.evalNode("/configuration"));

	return configuration;
}

private void parseConfiguration(XNode root) {
	try {
	  // issue #117 read properties first
	  // 解析</properties>标签
	  propertiesElement(root.evalNode("properties"));
	  // 解析</settings>标签
	  Properties settings = settingsAsProperties(root.evalNode("settings"));
	  loadCustomVfs(settings);
	  loadCustomLogImpl(settings);
	  // 解析</typeAliases>标签
	  typeAliasesElement(root.evalNode("typeAliases"));
	  // 解析</plugins>标签
	  pluginElement(root.evalNode("plugins"));
	  // 解析</objectFactory>标签
	  objectFactoryElement(root.evalNode("objectFactory"));
	  // 解析</objectWrapperFactory>标签
	  objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
	  // 解析</reflectorFactory>标签
	  reflectorFactoryElement(root.evalNode("reflectorFactory"));
	  settingsElement(settings);
	  // read it after objectFactory and objectWrapperFactory issue #631
	  // 解析</environments>标签
	  environmentsElement(root.evalNode("environments"));
	  // 解析</databaseIdProvider>标签
	  databaseIdProviderElement(root.evalNode("databaseIdProvider"));
	  // 解析</typeHandlers>标签
	  typeHandlerElement(root.evalNode("typeHandlers"));

	  // 解析</mappers>标签 加载映射文件流程主入口
	  mapperElement(root.evalNode("mappers"));
	} catch (Exception e) {
	  throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
	}
}

        首先是看数据源的配置标签environments,这里我们要找的方法也就是environmentsElement,先上代码。

        这个还是比较简单好理解,相对dubbo来说,看这个太幸福了。说正题,这个就是将标签一级一级解析,找到每一级标签的属性,然后构建不同的对象,比如事务的事务工厂对象TransactionFactory,还有数据源工厂对象DataSourceFactory,然后这些对象会被放入Environment 对象中,这里使用的是Builder 模式,最后可以看到就是将封装好的对象存入了全局配置对象configuration 对象中。

private void environmentsElement(XNode context) throws Exception {
  // 如果定义了environments
  if (context != null) {
    // 方法参数如何没有传递 environment,则解析sqlMapConfig.xml中的
    if (environment == null) {
      // <environments default="development" >
      environment = context.getStringAttribute("default");
    }

    //遍历解析environment节点
    for (XNode child : context.getChildren()) {
      //获得id属性值
      String id = child.getStringAttribute("id");
      //判断id和environment值是否相等
      if (isSpecifiedEnvironment(id)) {
        /*
            <transactionManager type="JDBC" />
            创建事务工厂对象
         */
        TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));

        /*
              <dataSource type="POOLED">
              创建数据源对象
         */
        DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
        DataSource dataSource = dsFactory.getDataSource();
        Environment.Builder environmentBuilder = new Environment.Builder(id)
            .transactionFactory(txFactory)
            .dataSource(dataSource);

        // 将Environment存到configuraion中
        configuration.setEnvironment(environmentBuilder.build());
        break;
      }
    }
  }
}

解析sql配置文件,并封装到Configuration的mapperRegistry集合

        这里还需要看一个关于sql 的配置文件解析,这里我们可以找mappers 标签的解析方法mapperElement 方法,先上代码。

        从代码中我们就可以知道这个标签是有两种不同的引入方式,一是:package,二是:其余方式,但是不管是哪种方式都是在configuration 对象的mapperRegistry 这个Map 集合对象属性中存入值。这里我们直接说循环里面的东西,首选判断当前标签是否是pakage 标签,如果是,则先获取标签的name 属性值,也就是引入的包名,然后以这个为key 值存入集合中,至于value 就是Object.class,这里所有存值value 目前都还只是Object.class,唯一的区别是key 的不同。

        下面说下当标签不为package 时,那么可以看到是获取其3个属性,resource、url、class,而且这三个属性只能配置一个属性有值,这里有意思的是resource、url 两种情况,又会走一遍解析文件的过程,也就是意味着resource、url 这两种情况都是引入其余的配置文件,而class 就是直接得到其class 对象,然后还是都存入了configuration 对象的mapperRegistry 集合属性中。注意这里封装的是每一个配置文件。

private void mapperElement(XNode parent) throws Exception {
  if (parent != null) {
    // 获取<mappers>标签的子标签
    for (XNode child : parent.getChildren()) {
      // <package>子标签
      if ("package".equals(child.getName())) {
        // 获取mapper接口和mapper映射文件对应的package包名
        String mapperPackage = child.getStringAttribute("name");
        // 将包下所有的mapper接口以及它的代理工厂对象存储到一个Map集合中,key为mapper接口类型,value为代理对象工厂
        configuration.addMappers(mapperPackage);
      } else {// <mapper>子标签
        // 获取<mapper>子标签的resource属性
        String resource = child.getStringAttribute("resource");
        // 获取<mapper>子标签的url属性
        String url = child.getStringAttribute("url");
        // 获取<mapper>子标签的class属性
        String mapperClass = child.getStringAttribute("class");
        // 它们是互斥的
        if (resource != null && url == null && mapperClass == null) {
          ErrorContext.instance().resource(resource);
          try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
            // 专门用来解析mapper映射文件
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            // 通过XMLMapperBuilder解析mapper映射文件
            mapperParser.parse();
          }
        } else if (resource == null && url != null && mapperClass == null) {
          ErrorContext.instance().resource(url);
          try(InputStream inputStream = Resources.getUrlAsStream(url)){
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            // 通过XMLMapperBuilder解析mapper映射文件
            mapperParser.parse();
          }
        } else if (resource == null && url == null && mapperClass != null) {
          Class<?> mapperInterface = Resources.classForName(mapperClass);
          // 将指定mapper接口以及它的代理对象存储到一个Map集合中,key为mapper接口类型,value为代理对象工厂
          configuration.addMapper(mapperInterface);
        } else {
          throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
        }
      }
    }
  }
}

       我们这在具体说一下resource 有值的情况下是如果解析sql 配置文件的,首先我们可以跳过的是InputStream 和XMLMapperBuilder 的获取,这个获取的流程和上面的配置文件转换流程是一样的,我们指点看parse 方法,代码如下。

        首先要确认的是所有的标签都是在mapper 标签下的,首先要判断的是这个文件是否已经解析过了,解析过了的话,就不再解析,然后就是去解析mapper 标签下的所有标签内容,跟进configurationElement 方法,代码也在下面了,可以看到跟上面的配置文件解析差不多,都是逐个逐个的标签进行解析,不过这里需要注意一点的就是它为命名空间,也就是mapper 标签的namespace 属性进行了存值。

        这里的解析标签我们就不一个一个看了,我们重点看下buildStatementFromContext 方法,这就是解析增删改查的sql 位置了,这里代码太多,我就不一个一个展示了我下面截图看重点。

public void parse() {
 // mapper映射文件是否已经加载过
 if (!configuration.isResourceLoaded(resource)) {

   // 从映射文件中的<mapper>根标签开始解析,直到完整的解析完毕
   configurationElement(parser.evalNode("/mapper"));
   // 标记已经解析
   configuration.addLoadedResource(resource);
   // 为命名空间绑定映射
   bindMapperForNamespace();
 }

 // 解析ResultMap
 parsePendingResultMaps();
 // 解析缓存
 parsePendingCacheRefs();
 // 解析statement
 parsePendingStatements();
}

/**
 *  解析映射文件
 * @param context 映射文件根节点<mapper>对应的XNode
 */
private void configurationElement(XNode context) {
  try {
    // 获取<mapper>标签的namespace值,也就是命名空间
    String namespace = context.getStringAttribute("namespace");
    // 命名空间不能为空
    if (namespace == null || namespace.isEmpty()) {
      throw new BuilderException("Mapper's namespace cannot be empty");
    }
    // MapperBuilderAssistant:构建MappedStatement对象的构建助手,设置当前的命名空间为namespace的值
    builderAssistant.setCurrentNamespace(namespace);
    // 解析<cache-ref>子标签
    cacheRefElement(context.evalNode("cache-ref"));
    // 解析<cache>子标签
    cacheElement(context.evalNode("cache"));

    // 解析<parameterMap>子标签
    parameterMapElement(context.evalNodes("/mapper/parameterMap"));
    // 解析<resultMap>子标签
    resultMapElements(context.evalNodes("/mapper/resultMap"));
    // 解析<sql>子标签,也就是SQL片段
    sqlElement(context.evalNodes("/mapper/sql"));
    // 解析<select>\<insert>\<update>\<delete>子标签
    // 将cache对象封装到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);
  }
}

 构建sql对象,替换sql占位符,保存对应对象信息

        我们最后是可以跟到statementParser.parseStatementNode() 的调用,这里我们可以看到解析出的入参类型,并生成对应的java对象,这段代码之前还要注意一个SqlCommandType 对象的构建,我们知道mybatis 的底层就是jdbc,而jdbc 其实大体就分两种操作executeQuery和executeUpdate,这个对象就是为了区分当前操作是属于哪种情况的。

        然后下面就是跟入参类型的解析差不多,就不多说了,然后可以跟到SqlSource 对象的生成,这个是因为jdbc 可识别的占位符是“?”号,而我们当前的sql 是存在#{}或者${}这种情况的,那么SqlSource 对象就是为了将sql 中的#{}或者${}对应的值进行记录,并且将其替换为“?”号占位符。这里我们可以下面跟一下代码,代码如下。

         在这里展示吧,跟进createSqlSource 方法,找到对应的XMLLanguageDriver 实现,其内容就是先构建出解析器,然后再解析动态sql,这里的解析器其实就包含了sql 中出现的if、foreach等等,其实可以发现mybatis 的工厂模式已经出现了很多次了。

        我们重点看解析动态sql 的内容,跟进来我们可以看到如下代码,${}和#{}对应的值分别存入了TextSqlNode 和StaticTextSqlNode 对象中,然后下面判断的isDynamic 布尔值,注意这个值只要是存在${}情况,那么就是true,就算是#{}和${}共存,那也是true。这里我们就跟一下RawSqlSource 方法,跟进去我们看的是 方法的调用,这里面会使用到一个叫做ParameterMappingTokenHandler 的解析器对象,用于解析sql,然后将sql中的#{}替换为“?”号,最后解析出来返回的就是一个StaticSqlSource 对象,而这个对象中存在一个获取BoundSql 对象的getBoundSql 方法,BoundSql 对象中又存在getSql 的方法可以直接获取sql。所以也就是我们之前分析的使用BoundSql 对象来获取使用的sql 给到语句执行器。

@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
  // 初始化了动态SQL标签处理器
  XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
  // 解析动态SQL
  return builder.parseScriptNode();
}

/**
 * 解析select\insert\ update\delete标签中的SQL语句,最终将解析到的SqlNode封装到MixedSqlNode中的List集合中
 * @return
 */
public SqlSource parseScriptNode() {
  // ****将带有${}号的SQL信息封装到TextSqlNode
  // ****将带有#{}号的SQL信息封装到StaticTextSqlNode
  // ****将动态SQL标签中的SQL信息分别封装到不同的SqlNode中
  MixedSqlNode rootSqlNode = parseDynamicTags(context);
  SqlSource sqlSource;
  // 如果SQL中包含${}和动态SQL语句,则将SqlNode封装到DynamicSqlSource
  if (isDynamic) {
    sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
  } else {
    // 如果SQL中包含#{},则将SqlNode封装到RawSqlSource中,并指定parameterType
    sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
  }
  return sqlSource;
}

封装sql对象至configuration的mappedStatements集合

        这里结束之后,我们可以回到XMLStatementBuilder 类中,下面的解析就没有什么要关注的了,大体都是一样的,当解析完成值,就会去构建一个MappedStatement 对象,也就是存入了configuration 的mappedStatements 集合属性中。注意,这里封装的是每一个配置文件对应的所有sql集合


SqlSessionFactory的openSession

        那么上述就已经结束了sqlSessionFactory 的构建,然后我们回到测试方法,测试方法的下一步就是通过工厂获取到具体的sqlSession 对象,这里的openSession 方法其实现类就是DefaultSqlSessionFactory 类,openSession 方法也就是调用openSessionFromDataSource 它有三个入参,参数1:执行器类型、参数2:事务隔离级别、参数三:指定事务是否自动提交,然后我们可以跟进这个方法,上代码。注意这个执行器我们使用的是默认执行器ExecutorType.SIMPLE。

        可以看到代码中首先获取的就是configuration 的environment 属性,这个就是我们之前解析environments 后获得的属性对象,然后获取这个对象中存入的事务工厂对象,然后在继续由事务工厂获取事务对象,然后就是执行器,最后由这些来构建出最后的sqlSession对象。注意这里的事务对象,在不配置的情况下,默认选择是交给容器做事务,可以看下代码中的默认事务对象是ManagedTransaction 对象这个对于提交和回滚的实现都是null的。

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
  Transaction tx = null;
  try {
    // 从configuration对象中获取environment对象
    final Environment environment = configuration.getEnvironment();
    // 获得事务工厂对象
    final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
    // 构建事务对象
    tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
    // 创建执行器对象
    final Executor executor = configuration.newExecutor(tx, execType);
    // 创建DefaultSqlSession对象
    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();
  }
}

        这里我们重点需要看的是Executor 执行器对象的构建,首先知道的是在执行器选择的情况,mybatis 提供了三个执行器,分别是BatchExecutor 批量执行器,ReuseExecutor 重复执行器,SimpleExecutor 简单执行器,我们这里选择的是SimpleExecutor 简单执行器,然后注意这里会判断一个cacheEnabled 这个表示的是否启动缓存,默认为true,也就是说在不设置的情况下,都会走进这一个逻辑,所以最后返回的对象就是CachingExecutor 代理执行器,这个里面是将原始执行器进行了封装,后面我们获取对应执行器的是就是通过CachingExecutor 来调用其delegate 属性获取。

        那么这里最后就是构建出来一个DefaultSqlSession 对象,进行返回,这个对应也是SqlSession 对象的子类。

 


 CachingExecutor执行器的调用

        进过了上面的openSession 得到了sqlSession 对象,我们再次回到测试方法,这里就走了具体sqlSession 调用,从sqlSession.selectOne 方法开始,最后跟到DefaultSqlSession 的selectList 方法,这里我们上代码看,代码如下。

        这里可以看到第一步就是获取到了上面封装的configuration 的mappedStatements 集合中对应的具体mappedStatement 也就是sql对象,注意之前我们没有说到这个集合的key 值是什么,这个key 就是命名空间,也就是mapper 标签的namespace 属性值 + select、update、delete、insert 标签的id 属性值。

        当获取了对应的mappedStatement 对象后,直接是执行器调用query 方法,注意我们现在的执行器是CachingExecutor。

private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
  try {
    // 根据传入的statementId,获取MappedStatement对象
    MappedStatement ms = configuration.getMappedStatement(statement);
    // 调用执行器的查询方法
    // wrapCollection(parameter)是用来装饰集合或者数组参数
    return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}

        先上代码。可以看到,上面也说过,首先通过的是mappedStatement 的getBoundSql 方法获取到BoundSql,这里的方法虽然是get 开头,但是这里并不是直接取值,而是构建出了一个BoundSql 对象,其属性值来源就是sqlSource 的属性值。

        然后我们直接跟到下一步生成缓存key 的createCacheKey 方法,这里具体的代码我们可以不用看,但是要知道它是由什么来生成的,这个是调用了之前封装到CachingExecutor 中的SimpleExecutor 执行器的createCacheKey 方法,其入参是mappedStatement 对象、parameterObject 传入参数对象、分页对象、BoundSql sql对象。这个面试的时候可以能会问到。

//第一步
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  // 获取绑定的SQL语句,比如 "SELECT * FROM user WHERE id = ? "
  BoundSql boundSql = ms.getBoundSql(parameterObject);
  // 生成缓存Key
  CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
  return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

        接下来就是调用query 方法,先看代码。

        首先需要从二级缓存中查询数据,通过mappedStatement 对象来获得缓存对象,这里只要在sql 配置文件中开启了二级缓存,那么这里就能获取到二级缓存对象,往下走如果在sql 配置中配置了flushCache 为true,那么就需要先刷新二级缓存,然后再去二级缓存中查询数据,这里二级缓存对象是TransactionalCacheManager 对象,这是Cache 对象的一个包装对象,也就是二级缓存对象。如果能获取到值,那么直接返回,反之再去查询一级缓存,这里的一级缓存就是调用SimpleExecutor 的query 方法,当查询成功之后再将查询内容放置二级缓存中,注意这里的key 就是上面封装的CacheKey,还有目前只是存入了一个map 集合中,只有真正的提交才会进行缓存。

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
    throws SQLException {
  // 获取二级缓存
  Cache cache = ms.getCache();
  if (cache != null) {
    // 刷新二级缓存 (存在缓存且flushCache为true时)
    flushCacheIfRequired(ms);
    if (ms.isUseCache() && resultHandler == null) {
      ensureNoOutParams(ms, boundSql);
      @SuppressWarnings("unchecked")
      // 从二级缓存中查询数据
      List<E> list = (List<E>) tcm.getObject(cache, key);
      // 如果二级缓存中没有查询到数据,则查询一级缓存及数据库
      if (list == null) {
        // 委托给BaseExecutor执行
        list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        // 将查询结果 要存到二级缓存中(注意:此处只是存到map集合中,没有真正存到二级缓存中)
        tcm.putObject(cache, key, list); // issue #578 and #116
      }
      return list;
    }
  }
  // 委托给BaseExecutor执行
  return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

        我们接着跟到delegate.query 方法中,这里调用的SimpleExecutor 的父类BaseExecutor,我们直接找到具体代码,这里个方法的实现代码太多了,我就截图展示了。

        首先就是一级缓存查询,这里我们可以看下一级缓存的数据结构,点进去我们可以看到就是用过map 集合的get 方法去获取对应值,而这个集合就是PerpetualCache 类cache 属性,同样二级缓存也是这个数据结构。二级缓存最后调用到了LruCache 类的getObject 方法,最后还是调用delegate.getObject 方法,跟一级缓存的取值一致。

        接下来如果在这里能获取到一级缓存那么就直接处理输出结果,如果没有则还是去数据库中查询,也就是queryFromDatabase 方法的调用。这个方法也很简单,就是用doQuery 方法进行查询数据库,然后将查询到的数据放入缓存,最会返回查询到的数据即可。

        我们这里要跟踪的是doQuery 方法,直接可以跟到SimpleExecutor 的doQuery 方法,代码如下。

        这里可以分为4步,第一步:获取配置实例,也就是从MappedStatement  获取的;第二步就是构建一个statementHandler 实例,也就是我们上面流程图中说的语句执行器,第三步准备处理器,主要包括创建statement 以及动态配置参数的设置等,第四步就是真正的数据库操作调用。

@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
  Statement stmt = null;
  try {
    // 1. 获取配置实例
    Configuration configuration = ms.getConfiguration();
    // 2. new一个StatementHandler实例
    StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
    // 3. 准备处理器,主要包括创建statement以及动态参数的设置
    stmt = prepareStatement(handler, ms.getStatementLog());
    // 4. 执行真正的数据库操作调用
    return handler.query(stmt, resultHandler);
  } finally {
    // 5. 关闭statement
    closeStatement(stmt);
  }
}

StatementHandler语句执行的构建

       这里我们就要先一个概念了,如下图,关于statementHandler 语句处理器的不同实现类解析。这里了解了之后,我们再看上面代码中的语句执行器的构建,跟进代码,代码如下。

        这里获取的是一个 RoutingStatementHandler 路由执行器,但是这个路由执行的构造方法里面可以看到,这里是通过传入不同的参数,选择出对应的不同执行器,这里我们现在debug 进来构建的是一个PreparedStatementHandler 预编译执行器,也就是说在没有配置的情况下,我们默认是预编译执行器,注意这里是可以在sql 配置文件中进行配置的,有一些情况是不需要预编译的。

        这里的还有一个插件机制,这里我们等下再介绍,先有个印象。

public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
  // 创建路由功能的StatementHandler,根据MappedStatement中的StatementType
  StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
  // 插件机制:对核心对象进行拦截
  statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
  return statementHandler;
}

         再回到SimpleExecutor 的doQuery 方法,在语句执行器构建成功之后,我们需要对其参数的设置,我们可以从prepareStatement 方法中的handler.parameterize 方法调用,可以跟到PreparedStatementHandler 类的parameterHandler.setParameters 方法调用,这里注意我们已经到了流程图中的parameterHandler 参数处理器层了,最后跟到DefaultParameterHandler 类的setParameters 方法,这里代码太多,就只展示重点,其余的可以自己跟代码看下。

        首先获取到参数映射,然后循环迭代,去封装每一个参数,不同类型转换是通过TypeHandler 也就是流程图中的类型转换器去做的,最后将每一个参数设置到预编译对象中。


PreparedStatement对象执行sql和结果集的处理

        当预编译对象封装好之后,我们再次回到SimpleExecutor 的doQuery 方法,进行下一步,也就是handler.query 方法的调用,我们这里可以跟到PreparedStatementHandler 的query 方法,这里就可以看到其实还是java jdbc的PreparedStatement 对象在执行sql,我们重点看下结果集的处理。

        这里因为代码太多了,我也就图片展示先关代码,具体的还是要请大家自己跟一下,我这里就提供一个流程的思路。

        首先我们先考虑如果自己做结果集的封装需要怎么做?那么首先需要拿到返回的结果集、然后获取对应的映射关系,最后通过映射关系和结果接封装相应的对象。我们看下mybatis 的源码做法。继上面的内容我们跟进resultSetHandler.handleResultSets 方法,找到DefaultResultSetHandler 类的handleResultSets 方法。

        首先获取结果集,并封装成一个wrapper 对象。

         然后获取相对应的映射关系,从mappedStatement 对象中获取,并循环迭代每一个映射进行封装处理,处理方法为handleResultSet 方法,

         跟进方法,首先是实例化一个DefaultResultHandler 对象,然后是对结果集进行映射,转换的结果存入defaultResultHandler,然后将转换的结果集放入multipleResults 集合中。映射具体我们就不看了,大体就是通过映射关系,获取到java 对象的实例,然后通过set 方法将结果集的数据进行存入,然后返回对象即可。这里也还会用到之前说过的TypeHandler 类型转换器对象,进行类型的转换。


        总结:上述基本上就是传统调用的全部过程了,总的来说其实就是跟着架构图在走,其中最重要的就是configuration 全局配置对象,基本上所有的配置信息都存在这个对象中,还有就是一、二级缓存的处理,其缓存对象就是PerpetualCache 类cache 属性,就是有一个常量的map集合。其余的就是结果集的处理比较复杂一点,这里还是得自己跟一下代码。就说这么多,看过文章有不会的,或者说我讲解的比较模糊,可以留言给我,我看到了会详细解答,有大佬看见我说的不对,也给我跟我说,我会及时改正。下篇我们再说代理模式下的调用。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值