经点面试题:mybatis的执行流程

回答这个问题前我们先来看一下mybatis的架构图,从架构图中可以简略的知道其执行流程(由接口层->数据处理层->基础支持层),实际上操作数据库时候的执行过程也是要经过这几个层的。

 

下面部分部分文字和图片参考大神的,大神连接

     MyBatis 最上面是接口层,接口层就是开发人员在 Mapper 或者是 Dao 接口中的接口定义,是查询、新增、更新还是删除操作;中间层是数据处理层,主要是配置 Mapper -> XML 层级之间的参数映射,SQL 解析,SQL 执行,结果映射的过程。上述两种流程都由基础支持层来提供功能支撑,基础支持层包括连接管理,事务管理,配置加载,缓存处理等。

 接口层

主要面向程序员操作数据库数据的接口API

InputStream is = Resources.getResourceAsStream("myBatis-config.xml");
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(is);
sqlSession = factory.openSession();

SqlSessionFactory和SqlSession是mybatis的核心接口,尤其是 SqlSession,这个接口是MyBatis 中最重要的接口,这个接口能够让你执行命令,获取映射,管理事务。

 数据处理层

1、配值解析

mybatis在启动的时候会加载mybatis-config.xml文件,并解析此文件,解析后的文件放进configuration对象中,configuration对象中的属性几乎和mybatis-config.xml文件中各个节点名字一样。之后会创建SqlSessionFactory对象,可以通过此对象创建出Sql Session对象如接口层代码那样。

2、SQL解析和scripting模块

Mybatis 实现的动态 SQL 语句,几乎可以编写出所有满足需要的 SQL。Mybatis 中 scripting 模块会根据用户传入的参数,解析映射文件中定义的动态 SQL 节点,形成数据库能执行的SQL 语句。

3、SQL执行

SQL 语句的执行涉及多个组件,包括 MyBatis 的四大核心。他们是: 

ExecutorStatementHandlerParameterHandlerResultSetHandler

sql执行流程如下图:

 

基础支持层

反射,类型转换,缓存,日志,事务,这些功能都在这一层

 

mybatis中的重要组件如下:

  • SqlSession: ,它是 MyBatis 核心 API,主要用来执行命令,获取映射,管理事务。接收开发人员提供 Statement Id 和参数。并返回操作结果。
  • Executor :执行器,是 MyBatis 调度的核心,负责 SQL 语句的生成以及查询缓存的维护。
  • StatementHandler : 封装了JDBC Statement 操作,负责对 JDBC Statement 的操作,如设置参数、将Statement 结果集转换成 List 集合。
  • ParameterHandler : 负责对用户传递的参数转换成 JDBC Statement 所需要的参数。
  • ResultSetHandler : 负责将 JDBC 返回的 ResultSet 结果集对象转换成 List 类型的集合。
  • TypeHandler : 用于 Java 类型和 JDBC 类型之间的转换。
  • MappedStatement : 映射文件的select,insert等标签都解析成此对象
  • SqlSource : 表示从 XML 文件或注释读取的映射语句的内容,它创建将从用户接收的输入参数传递给数据库的 SQL。
  • Configuration: MyBatis 所有的配置信息都维持在 Configuration 对象之中。

 

下面从源码的角度来看看这些组件如何初始化和工作的。

配值文件:




      
    
        
        
            
            
            
            
                
                
                
                
            
        
    


    
    
       
    

 

SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(is);

进入build方法:

经点面试题:mybatis的执行流程_第1张图片

接着跟进parse方法,

//在创建XMLConfigBuilder时,它的构造方法中解析器XPathParser已经读取了配置文件
//3. 进入XMLConfigBuilder 中的 parse()方法。
public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    //parser是XPathParser解析器对象,读取节点内数据,是MyBatis配置文件中的顶层标签
    parseConfiguration(parser.evalNode("/configuration"));
    //最后返回的是Configuration 对象
    return configuration;
}

//4. 进入parseConfiguration方法
//此方法中读取了各个标签内容并封装到Configuration中的属性中。
private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      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);
    }
}

根据配值文件,我们的代码会在  environmentsElement(root.evalNode("environments"));进去此函数:

经点面试题:mybatis的执行流程_第2张图片

 

private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
       第一次进入肯定为null,得到mybatis的数据库环境是mysql的还是其其他的
      if (environment == null) {
        environment = context.getStringAttribute("default");
      }
       遍历 节点的子节点即节点,这里可返回源码一开始分析的xml文件看看有哪些子节点
      for (XNode child : context.getChildren()) {
        String id = child.getStringAttribute("id");
        if (isSpecifiedEnvironment(id)) {
           //解析节点
          TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
         //解析节点
          DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
          DataSource dataSource = dsFactory.getDataSource();
          Environment.Builder environmentBuilder = new Environment.Builder(id)
              .transactionFactory(txFactory)
              .dataSource(dataSource);
           将解析到的对象封装成configuration的属性environment
          configuration.setEnvironment(environmentBuilder.build());
        }
      }
    }
  }

上面函数执行完毕后返回parseConfiguration函数,然后继续向下执行typeHandlers标签在上面已经详细讲过。

经点面试题:mybatis的执行流程_第3张图片

接下来我们看解析标签,在mapperElement函数里面

  private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
       //遍历子标签,子标签有好几种写法,这边对应好几种判断,上面提到过子标签的几种写法
      for (XNode child : parent.getChildren()) {
            // 对应
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          //对应 
          String resource = child.getStringAttribute("resource");
          //对应   
          String url = child.getStringAttribute("url");
           //对应 
          String mapperClass = child.getStringAttribute("class");
          if (resource != null && url == null && mapperClass == null) {
            ErrorContext.instance().resource(resource);
            //根据解析到的地址用流加载进内存
            InputStream inputStream = Resources.getResourceAsStream(resource);
             //拿到xml解析器
            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.");
          }
        }
      }
    }
  }

一步步到了mapperParser.parse();这个函数,我们重点分析,他是如何把xml的节点转换成mybatis的数据结构的。

先看看映射文件中的顶级标签

SQL 映射文件有很少的几个顶级元素(按照它们应该被定义的顺序)

1 cache 给定命名空间的缓存配置
2 cache-ref 其他命名空间的缓存配置的引用
3 resultMap 是最富复杂也是最强大的元素,是用来描述如何从数据库结果集中来加载对象
4 sql – 可被其他语句引用的可重用语句块
5 insert – 映射插入语句
6 update – 映射更新语句
7 delete – 映射删除语句
8 select – 映射查询语句
 

public void parse() {
     // 检测映射文件是否已经被解析过
    if (!configuration.isResourceLoaded(resource)) {
     // 解析 mapper 节点
      configurationElement(parser.evalNode("/mapper"));
     // 添加资源路径到“已解析资源集合”中
      configuration.addLoadedResource(resource);
     // 通过命名空间绑定 Mapper 接口
      bindMapperForNamespace();
    }
     // 处理未完成解析的节点
    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }

如上,映射文件解析入口逻辑包含三个核心操作,分别如下:

  1. 解析 mapper 节点
  2. 通过命名空间绑定 Mapper 接口
  3. 处理未完成解析的节点

 

进入解析mapper节点的函数,解析之前先看看映射文件的结构

经点面试题:mybatis的执行流程_第4张图片

 上面是一个比较简单的映射文件,还有一些的节点没有出现在上面。以上每种配置中的每种节点的解析逻辑都封装在了相应的方法中,这些方法由 XMLMapperBuilder 类的 configurationElement 方法统一调用

 private void configurationElement(XNode context) {
    try {
      // 获取 mapper 命名空间
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
       // 设置命名空间到 builderAssistant 中
      builderAssistant.setCurrentNamespace(namespace);
       // // 解析  节点
      cacheRefElement(context.evalNode("cache-ref"));
      // 解析  节点,MyBatis 提供了一、二级缓存,其中一级缓存是 SqlSession 级别的,默认为开启状态。二级缓存配置在映射文件中,使用者需要显示配置才能开启
      cacheElement(context.evalNode("cache"));
       // 已废弃配置,这里不做分析
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
       // 解析  节点
      resultMapElements(context.evalNodes("/mapper/resultMap"));
       // 解析  节点
      sqlElement(context.evalNodes("/mapper/sql"));
        // 解析