Mybati源码分析(一) 创建SqlSessionFactory

简介

Mybatis是一款优秀的ORM框架,是国内使用的ORM框架中的主流,它使用简单,入们很快。对于有过几年Java开发经验的老司机,我相信都对它如何使用已经能够轻车熟路了,但是,阅读Mybatis的源码的估计不多,如果各位朋友和要是和本人一样,对阅读源码很感兴趣,想要知道Mybatis庐山真面目的,可以一起探讨探讨。

Mybatis的使用的入口

我们先来看一下最简单的查询一个对象的实例 。

//读取配置
Reader reader = Resources.getResourceAsReader("mybatisCfg.xml"); 
//创建sqlSeessionFactory
SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(reader);
//打开sqlSession
SqlSession ssn = ssf.openSession();
//查询一个user对象
User user = ssn.selectOne("mapping.userMapper.getUserByName", "testUser");
//关闭会话
ssn.close();

上面的代码是Mybatis最简单的使用例子,麻雀虽小,五脏俱全,虽然只有短短5行代码,它已经完整的包括了Mybatis的使用周期。Mybatis的周期从大体上可分为三部分
在这里插入图片描述
我们要对Mybatis源码解析,首先我们需要先列出Mybatis整个流程当中可能会用到的对象类型列出来,先弄清大体的框架,再去抠细节
在这里插入图片描述
上面的类型,其实还可以分得更简单一些,分为三个过程,再将每个类型对位到不同的过程,像下图那样
在这里插入图片描述

初始化

那么如何从代码的角度去分析mybatis的初始化过程,首先我用一个图简单表示一下需要用到的类型前后关系
在这里插入图片描述
Mybatis的使用的第一步是传入根配置信息,我们拿最开头的事例的头两行代码出来看看

//Reader只是一个读文件的接口,这里还没有开始读
Reader reader = Resources.getResourceAsReader("mybatisCfg.xml"); 
//将配置文入口传给SqlSessionFactoryBuilder的实例方法
SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(reader);

我再点击进去看一下SqlSessionFactoryBuilder的build()方法的实现

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(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }

build代码很简单,它有几个重载但没有多大区别,我们看到new XMLConfigBuilder(reader, environment, properties),它有个reader参数就是我们在外面传的配置文件入口,environment是指定的环境(是一个字符串),properties是系统配置信息,我们上面的例子中,没有用到这两个参数,所以传的都是空。接下来,我们在看到build(parser.parse()),parse是解析的意思,它的作用是把配置信息解析成为Configuration对象,然后再创建DefaultSqlSessionFactory对象,DefaultSqlSessionFactory有一个Configuration类型的参数的构造函数。

我们知道DefaultSqlSessionFactory是怎么new来了,那我们再一探个究竟,看看DefaultSqlSessionFactory的部分源码和SqlSessionFactory接口定义

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

}
public class DefaultSqlSessionFactory implements SqlSessionFactory {
 private final Configuration configuration;
  public DefaultSqlSessionFactory(Configuration configuration) {
    this.configuration = configuration;
  }
}

从上面的代码可以看到SqlSessionFactory 的接口定义的都是openSession方法和getConfiguration方法,也就是说它的作用只有一个就是用来创建SqlSession对象,并且里面存有一个Configuration的字段。我们再来看看DefaultSqlSessionFactory 的构造函数的实现,DefaultSqlSessionFactory 构造函数只做一件事情,就是保存configuration字段变量。

看了上面的代码之后,其实我们还是不知道Mybatis怎么初始化,因为整个初始化的核心是其实不在SqlSessionFactoryBuiler上,而是在XMLConfigBuilder上。它才是解析整个配置的核心,XMLConfigBuilder的作用是读取根配置文件的配置信息,然后解析为Java对象,最终将它放到Configuration对象里面。

在讲解XMLConfigBuilder之前,我们先来看一下Configuration类内部情况,因为XMLConfigBuilder解析的过程离不开configuration对象

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
上面的代码很多,但我们不需要细看每句代码,从大体上看,它分为各种配置字段和工厂方法,它的构造函数会初始化一部分配置。那为什么要把工厂方法也放到Configuration上呢? 其实这里的工厂方法都是和配置相关的,例如创建MappedStatement对象,我们知道Mybatis的sql都是写在Mapper配置里面, 创建MappedStatement对象需要读取配置信息,所以把工厂方法放到Configuration里面也是合情合理的,所以不要把Configuration理解为只有配置信息(其实还包括工厂方法,不过个人觉得Configuration和工厂方法放在一起耦合有点大,如果要自己写个自定义Executor类型,想要简单的加到Configuration上是没有办法的)。

接下来,我们来分析加载过程的真正核心,XMLConfigBuilder的内部实现。下面列出XMLConfigBuilder的部分源码

public abstract class BaseBuilder {
  protected final Configuration configuration;
  protected final TypeAliasRegistry typeAliasRegistry;
  protected final TypeHandlerRegistry typeHandlerRegistry;
  
  public BaseBuilder(Configuration configuration) {
    this.configuration = configuration;
    this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
    this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
  }
 }

public class XMLConfigBuilder extends BaseBuilder {
 public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
    this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()),    environment, props);
  }
  private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
    super(new Configuration());
    ErrorContext.instance().resource("SQL Mapper Configuration");
    this.configuration.setVariables(props);
    this.parsed = false;
    this.environment = environment;
    this.parser = parser;
  }
}

上面列出了XMLConfigBuilder的构造函数和它的父类,先看构造函数,首先它new了一个configuration对象,然后调用父类的构造函数,父类的构造函数做了什么呢?BaseBuilder它只有有三个字段成员,分别为configuration ,typeAliasRegistry,typeHandlerRegistry。configuration是外面传进来的,只需把它存起来就行了, typeAliasRegistry和typeHandlerRegistry是从configuration内部取出来的,它的作用是什么了,看下面的截图,其实不难猜到,typeAliasRegistry是一个别名(Alias)和类型(Type)的映射器(有多个别名对应一个type),因为Java的基础类型和数据库的数据类型是不一样的,所以下面TypeHandlerRegistry可以看到,每个基础类型都注册了一个Handler类型处理程序,所以它的作用就是用来处理类型转换的。例如:我们从数据表里面拿到了一个int类型的字段, 那我们就需要从TypeHandlerRegistry里面找到一个IntegerTypeHandler对象,然后把它转为Java的Integer对象。现在调用完父类的构造函数后,接下来要处理的是设置environment (是环境的名称,比如dev,prod等等)和系统环境参数props, 设置parsed为false,最后设置parser解析器,parser的类型是XPathParser,它的作用是用来解析Xml文件。
在这里插入图片描述
在这里插入图片描述
接下来,我们再进一步分析,看看XMLConfigBuilder类里面的parse()方法是怎么解析的

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

首先执行parse方法时,需要判断是否已经解析过,因为Configuration实例的初始化不是幂等的,不能重复执行,不然会造成configuration配置重复的问题,接下来我们看到parser.evalNode("/configuration")方法,evalNode()方法的作用是找到Xml文件中的某个节点并将它转为XNode对象返回。
下面是一个简单的MyBatis的根配置信息

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <environments default="dev">
        <environment id="dev">
            <transactionManager type="JDBC" />
            <!-- 配置数据库连接信息 -->
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver" />
                <property name="url" value="jdbc:mysql://192.168.31.18:3306/UM?useUnicode=true&amp;characterEncoding=UTF-8" />
                <property name="username" value="root" />
                <property name="password" value="abc123" />
            </dataSource>
        </environment>
    </environments>
     <mappers>
         <mapper resource="mapping/userMapper.xml"/>
         <mapper class="mapping.account_mapper"/>
     </mappers>
</configuration>

通过parser.evalNode("/configuration")拿到了<configuration>节点信息之后,我们还需要给它解析下面的所有子节点.parse()方法里面通过parseConfiguration()方法解析configuration节点的每个子节点。接下来我们继续在XMLConfigBuilder类中找到parseConfiguration()方法的实现

  private void parseConfiguration(XNode root) {
    try {
      //解析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);
      //解析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);
    }
  }
 

上面看到configuration的子节点有十多种,每一种节点的特点和作用都不一样,但节点的读取统一都是evalNode()方法,不过拿到了它们的XNode节点之后,每个都是要通过不同的方式把它加到configuration里面去的。这里我们就只拿我们最关心的mapper节点去作介绍,Mybatis是怎么将Mapper读取到配置的,下面可以看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);
            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.");
          }
        }
      }
    }
  }

parent参数对应的是的<mappers>节点(如下图),由于mappers的是一个集合,所以上面的源码对它的子节点进行了遍历, maper节点有个name属性,如果name等于"package"值时,它是一个包(就是把多个配置包在一起),其他不等于"package"的对应的都是一个单独的Mapper。
在这里插入图片描述
如果节点的name属性是package的话,那么通过configuration.addMappers()方法直接加到configuration的配置里面(在addMappers()方法里面还会做进一步的解析)。

如果非package节点,又分为三种情况:
1.只存在resource的属性的节点:它是一个资源文件的地址,需要将它转为inputStream类型,再通过XMLMapperBuilder去解析,并且将解析后的结果存到configuration的里面。
2.只存在url属性的节点:和第1种很像,只是resource是项目内的路径,url是项目外部的路径,其实作用一样。
3.只存在class属性的节点:这种使用的是Java配置,采用注解的形式,它不需要将配置解析为XNode对象,而是拿到它的接口信息,再通过configuration.addMapper()方法直接加到configuration里面。

下面是XMLMapperBuilder的处理细节,由于代码太长,这里就不一一细说

public class XMLMapperBuilder extends BaseBuilder {
  //Xml解析器
  private final XPathParser parser;
  //Mapper辅助建造器
  private final MapperBuilderAssistant builderAssistant;
  private final Map<String, XNode> sqlFragments;
  //资源路径
  private final String resource;

  public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
    this(new XPathParser(inputStream, true, configuration.getVariables(), new XMLMapperEntityResolver()),
        configuration, resource, sqlFragments);
  }
  private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
    super(configuration);
    this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
    this.parser = parser;
    this.sqlFragments = sqlFragments;
    this.resource = resource;
  }
 private void configurationElement(XNode context) {
    try {
      //解析namespace
      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"));
      cacheElement(context.evalNode("cache"));
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      sqlElement(context.evalNodes("/mapper/sql"));
      //解析select|insert|update|delete节点
      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);
    }
  }
  private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteStatement(statementParser);
      }
    }
  }
}

mappers里面的mapper节点对应的是一个mapper.xml文件,XMLMapperBuilder是专门对它进行解析的,XMLMapperBuilder通过很多步骤之后,拿到context节点,它也是mapper.xml文件的根节点mapper,然后把它传给configurationElement()方法去处理,从中拿到"select、insert、update、delete"四种节点(如下图)再传给buildStatementFromContext()方法,最后通过XMLStatementBuilder类的parseStatementNode()方法将它们转成MappedStatement并添加到configuration的mappedStatements缓存
在这里插入图片描述

public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");
    String parameterType = context.getStringAttribute("parameterType");
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
   。。。。。。
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }

上面是parseStatementNode()方法的实现(省略了大部分代码),它从context节点里面拿到id,sql脚本等信息,然后再调用builderAssistant.addMappedStatement()方法。(Mybatis的builder的类有点多, BuilderAssistant也是继承了BaseBuilder类的,内部有configuration的成员变量,这里最终会调用configuration的addMappedStatement()方法)

结语

好吧,Mybatis的初始化过程已经讲完了,它的作用其实就是为了初始化configuration,然后把它放到SqlSessionFactory里面,虽然初始过程的代码很复杂,但是Mybatis的总体的结构还是很清晰的。
这里只讲了第最开始部分,下一篇继续介绍SqlSession管理

谢谢能够看到这里的各位小伙伴,本人能力有限,如果有什么说错的地方请指正。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章