Mybatis源码解析之数据源和SQL构建

1、Mybatis官网简介

  • MyBatis一款半自动的ORM持久层框架,它支持自定义SQL、存储过程以及高级映射。
  • MyBatis对JDBC进行了封装,几乎免除了所有的 JDBC 代码以及设置参数和获取结果集的工作。
  • MyBatis可以通过简单的XML(mapper文件)或注解两种方式操作数据库。

MyBatis的执行流程图大致如下:
在这里插入图片描述

2、Demo搭建

Mybatis的Demo可以直接去官网下载,如果不下载的话那么可以根据下面的步骤搭建一个Demo,测试MyBatis
源码。

2.1、maven依赖

在maven项目中的pom文件引入Mybatis依赖,pom依赖项如下所示:

<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis</artifactId>
  <version>3.2.1</version>
</dependency>
2.2、mybatis配置

新建一个mybatis-config.xml文件作为Mybatis文件配置,这个配置文件对应mybatis包下面的Configuration类,内容如下:

<?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"/>
  <!--允许JDBC支持自动生成主键-->
  <setting name="useGeneratedKeys" value="false"/>
  <!--指定 MyBatis 应如何自动映射列到字段或属性-->
  <!--NONE 表示关闭自动映射;PARTIAL 只会自动映射没有定义嵌套结果映射的字段。
   FULL 会自动映射任何复杂的结果集(无论是否嵌套)--> 
  <setting name="autoMappingBehavior" value="PARTIAL"/>
  <!--配置默认的执行器->
  <!--SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(PreparedStatement); 
  BATCH 执行器不仅重用语句还会执行批量更新-->
  <setting name="defaultExecutorType" value="SIMPLE"/>
  <!--当没有为参数指定特定的 JDBC 类型时,空值的默认JDBC 类型->
  <setting name="jdbcTypeForNull" value="OTHER"/>
</settings>

    <!--定义SqlSession的环境配置-->
    <environments default="development">
        <environment id="development">
            <!--定义事务管理器配置-->
            <transactionManager type="JDBC"/>
            <!--数据源配置 采用连接池-->
            <dataSource type="POOLED">
                <!--jdbc驱动器-->
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <!--mysql的url-->
                <property name="url" value="实际路径"/>
                <!--用户名-->
                <property name="username" value="实际用户名"/>
                <!--密码-->
                <property name="password" value="实际密码"/>
            </dataSource>
        </environment>
    </environments>
    
    <!--配置mapper文件-->
    <mappers>
        <mapper resource="DemoMapper.xml"/>
    </mappers>

</configuration>

本文demo的Mybatis配置文件只配置了以下三个内容,实际使用当中这三个配置也比较常用,具体如下所示:

  • settings”标签描述的是MyBatis自身的一些属性配置,比如缓存和延迟加载等一系列属性,在这里配置的属性会覆盖掉Mybatis包下面的Configuration类的默认配置。
  • environments”标签配置的数据库操作相关的,比如事务管理配置、连接池配置、数据源配置。
    • 事务管理配置,MyBatis自身提供了两种事务管理器,分别为JDBC和MANAGED。
      • JDBC – 这个配置直接使用了 JDBC 的提交和回滚设施,它依赖从数据源获得的连接来管理事务作用域
      • MANAGED – 这个配置几乎没做什么。它从不提交或回滚一个连接,而是让容器来管理事务的整个生命周期。
    • 连接池配置,MyBatis共提供了三种连接池,分别如下:
      • UNPOOLED–这个数据源的实现会每次请求时打开和关闭连接。
      • POOLED–这种数据源的实现利用“池”的概念将JDBC连接对象组织起来,避免了创建新的连接实例时所必需的初始化和认证时间。
      • JNDI–这个数据源实现是为了能在如EJB或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的数据源引用。
    • 数据源配置就是用来连接数据库的,包括驱动器、URL、用户名和密码。
  • mapper文件里面存放的是SQL语句,但是我们需要告诉MyBatis去哪找到这些SQL文件,所以mappers标签就是告诉MyBatis去哪找这些SQL语句。MyBatis提供了四种方法映射mapper文件,分别如下:

下面的demo是摘抄自官网,实际中应根据项目的具体路径配置

<!-- 第一种使用相对于类路径的资源引用 也是本文中使用的方式-->
<mappers>
 <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
</mappers>

<!-- 第二种使用相对于类路径的资源引用 -->
<mappers>
   <mapper url="file:///var/mappers/AuthorMapper.xml"/>
</mappers>

<!-- 第三种使用映射器接口实现类的完全限定类名 -->
<mappers>
  <mapper class="org.mybatis.builder.AuthorMapper"/>
</mappers>

<!-- 第四种使用映射器接口实现类的完全限定类名 -->
<mappers>
  <package name="org.mybatis.builder"/>
</mappers>

根据官网介绍,MyBatis配置文件共有9个配置标签可供使用,具体如下:
在这里插入图片描述

2.3、mapper文件配置

DemoMapper接口内容如下:

public interface DemoMapper {
    /**
     * 查询用户信息
     *
     * @return 返回结果
     */
    List<BaseUserInfoDO> queryBaseUserInfoDO();
}

此次测试中选择的是base_user_info数据库表,本次只测试select查询语句,然后分析其底层源码实现。DemoMapper.xml文件内容如下。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC
        "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.test.mapper.DemoMapper">
 <!--用as将数据表中的字段映射到实体类字段上-->  
 <select id="queryBaseUserInfoDO" resultType="com.test.entity.BaseUserInfoDO">
        /**UserMapper.queryBaseUserInfoDO 用户信息查询*/
        SELECT
            id as id,
            user_id as userId,
            user_name as userName,
            phone as phone,
            status as status,
            sign_status as signStatus,
            sex as sex 
        FROM
        base_user_info
    </select>
</mapper>

对应接收的实体类如下:

@Getter
@Setter
@ToString(callSuper = true)
public class BaseUserInfoDO implements Serializable {
    private   Long     id;
    private   Long     userId;
    private   String   userName;
    private   String   phone;
    private   String   status;
    private   String   signStatus;
    private   String   sex;
}
2.3、测试类

测试类代码如下:

public class TestMyBatis {

    public static void main(String[] args) throws IOException {
        String resource = "mybatis-config.xml";
        //获取MyBatis配置文件
        InputStream inputStream = Resources.getResourceAsStream(resource);
        //通过SqlSessionFactoryBuilder的build()方法获取SqlSessionFactory 对象
        //此时已将MyBatis配置文件的相关配置已获取到,即Configuration类已获取到值
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        //通过SqlSessionFactory的openSession()方法获取SqlSession 对象
        SqlSession sqlSession = sqlSessionFactory.openSession();
        try {
            //通过SqlSession对象执行SQL语句
            List<BaseUserInfoDO> result = sqlSession.selectList("com.mybatis.DemoMapper.queryBaseUserInfoDO");
            System.out.println(result.toString());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            sqlSession.close();
        }
    }
}

Demo搭建过程如下:
在这里插入图片描述
Demo运行结果如下:
在这里插入图片描述
从上图中可以看到,MyBatis的的整个执行流程,解析如下:

  • 第一步创建数据库连接
  • 第二步使用连接,执行SQL语句并返回结果
  • 第三步关闭数据库连接,将关闭后的连接放回到连接池中

3、源码解析

终于到了源码分析阶段了,相信到了这一步,你应该非常想看到Myabtis到底是如何解析SQL语句的吧。不要急,慢慢来,心急可吃不了热豆腐哦。

通过官网的介绍,可以知道MyBatis的作用是用来连接数据库,然后执行SQL语句的,根据Demo的执行结果也验证了官网的正确性。所以此次源码分析主要分析这两大块内容,

  • MyBatis如何获取数据源的?
  • MyBatis是如何构建SQL语句的?
3.1、初始化阶段

根据Demo的内容可以知道,数据源和配置类信息都配置在mybatis-config.xml文件中,在测试类TestMyBatis 中,先是将配置文件解析成InputStream流,放到SqlSessionFactoryBuilder的build(inputStream)方法中,然后获取到SqlSessionFactory对象,所以现在就来看一下build()方法源码里面执行了哪些内容吧。 即解析下面这条语句的源码

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

源码执行过程如下所示。部分无关代码已省略:

public class SqlSessionFactoryBuilder {
  
 /**
 * build方法的入口,调用下面的build方法
 **/
 public SqlSessionFactory build(InputStream inputStream) {
    return build(inputStream, null, null);
  }
  /**
  * 在这里真正的对mybatis-config.xml文件进行解析
  **/
  public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
      //这一步中主要的操作是将mybatis-config.xml文件加载进parser对象,然后初始化Configuration类
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      //先调用parser.parse()方法获取mybatis-config.xml文件配置信息,然后将里面的信息赋值给Configuration类
      //然后调用下面的build方法,将已经赋值的Configuration类作为参数传递进DefaultSqlSessionFactory类中
      //这一步中Configuration类已经完全获取mybatis-config.xml文件配置信息
      return build(parser.parse());
   }
  /**
  *最后调用的返回方法
  **/
  public SqlSessionFactory build(Configuration config) {
    //将已经初始化的Configuration类作为参数传递进DefaultSqlSessionFactory类,并返回
    return new DefaultSqlSessionFactory(config);
  }
}

将上面的步骤拆开来分析,先看下面这个语句,

XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);

从名字就可以看出来,这个类的初始化应该是去加载mybatis-config.xml文件,但是还没有真正的去解析,下面看一下这个XMLConfigBuilder类的这个构造函数:

public class XMLConfigBuilder extends BaseBuilder {
  private boolean parsed;
  private XPathParser parser;
  private String environment;
  
  /**
  * 这个方法主要是加载mybatis-config.xml文件
  **/
  public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) 
  {
    //初始化XPathParser对象,加载mybatis-config.xml文件
    this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment,     props);
  }
  /**
  * 这个方法主要通过super(new Configuration())方法将初始化的Configuration类传递进去。
  * 主要是将Configuration类进行初始化
  **/
  private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
    //调用父类的构造函数,同时Configuration类进行初始化
    //非常关键
    super(new Configuration());
    ErrorContext.instance().resource("SQL Mapper Configuration");
    this.configuration.setVariables(props);
    this.parsed = false;
    this.environment = environment;
    this.parser = parser;
  }
}

先来看一下XMLConfigBuilder 类的父类BaseBuilder 类的构造函数吧,主要是看 super(new Configuration())这一个语句的一个执行内容。

public abstract class BaseBuilder {
  //MyBatis的配置类
  protected final Configuration configuration;
  //JAVA类型别名对象,主要对应JAVA类型。如整型、布尔型、Float类型等
  protected final TypeAliasRegistry typeAliasRegistry;
  //JDBC类型处理器,主要是将JDBC类型转换成JAVA类型,如将CHAR、VARCHAR类型转化为String类型
  protected final TypeHandlerRegistry typeHandlerRegistry;
  //super(new Configuration())调用的就是这个方法,
  //其实主要的就是Configuration类的初始化  
  public BaseBuilder(Configuration configuration) {
    //初始化后的Configuration类进行赋值
    this.configuration = configuration;
    //JAVA类型别名对象随着Configuration类的初始化也进行初始化了
    this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
    //JDBC类型处理器随着Configuration类的初始化也进行初始化了
    this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
  }
}  

下面看一下Configuration 类中成员变量吧,几乎整个MyBatis的配置数据以及存储数据都是通过Configuration类进行读和写的。 下面看一下Configuration类型主要成员变量,无关的成员变量暂时省略。

/**
* Configuration类包含了mybatis-config.xml中所有的配置选项
**/
public class Configuration {
  //Environment对象对应的就是mybatis-config.xml文件中environments标签
  //对应的就是数据库的事务管理和数据源配置
  protected Environment environment;
  //下面所有的变量对应的都是mybatis-config.xml文件中setting标签中的配置
  //settings标签映射开始
  protected boolean safeRowBoundsEnabled = false;
  protected boolean safeResultHandlerEnabled = true;
  protected boolean mapUnderscoreToCamelCase = false;
  protected boolean aggressiveLazyLoading = true;
  protected boolean multipleResultSetsEnabled = true;
  protected boolean useGeneratedKeys = false;
  protected boolean useColumnLabel = true;
  //MyBaits一级缓存默认是开启
  protected boolean cacheEnabled = true;
  protected boolean callSettersOnNulls = false;
  protected String logPrefix;
  protected Class <? extends Log> logImpl;
  protected ProxyFactory proxyFactory;
  protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
  protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
  protected Set<String> lazyLoadTriggerMethods = new HashSet<String>(Arrays.asList(new String[] { "equals", "clone", "hashCode", "toString" }));
  protected Integer defaultStatementTimeout;
  //默认执行器SIMPLE类型的,共有三种,其它两种是REUSE、BATCH
  protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
  protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;
  protected Properties variables = new Properties();
  protected ObjectFactory objectFactory = new DefaultObjectFactory();
  protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();
  protected MapperRegistry mapperRegistry = new MapperRegistry(this);
  //延时加载默认关闭
  protected boolean lazyLoadingEnabled = false;
  //settings标签映射结束


  //数据库ID
  protected String databaseId;
  //驱动器
  protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();
  //默认拦截器
  protected final InterceptorChain interceptorChain = new InterceptorChain();
  //JAVA类型别名对象 进行实例化
  protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry();
  //JDBC类型处理器 进行实例化
  protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
  //对应SQL操作对象,用Map进行存储
  protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");
   //mapper文件的节点容器
  protected final Map<String, XNode> sqlFragments = new StrictMap<XNode>("XML fragments parsed from previous mappers");
  //SQL语句查询结果的缓存容器,就是查询一次后,再次查询相同的SQL语句,可以从此处直接获取缓存数据
  protected final Map<String, Cache> caches = new StrictMap<Cache>("Caches collection");
  //SQL语句执行完成后返回的结果集
  protected final Map<String, ResultMap> resultMaps = new StrictMap<ResultMap>("Result Maps collection");
  //SQL语句的参数容器
  protected final Map<String, ParameterMap> parameterMaps = new StrictMap<ParameterMap>("Parameter Maps collection");
  //mapper文件(存放SQL语句)中SQL语句构造器容器
  protected final Collection<XMLStatementBuilder> incompleteStatements = new LinkedList<XMLStatementBuilder>();
  //SQL语句的主键容器
  protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<KeyGenerator>("Key Generators collection");
  protected final Set<String> loadedResources = new HashSet<String>();
  //缓存解析器容器
  protected final Collection<CacheRefResolver> incompleteCacheRefs = new LinkedList<CacheRefResolver>();
  //JDBC结果解析器容器
  protected final Collection<ResultMapResolver> incompleteResultMaps = new LinkedList<ResultMapResolver>();
  //方法解析器容器
  protected final Collection<MethodResolver> incompleteMethods = new LinkedList<MethodResolver>();

  //Configuration对象的默认构造函数
  //这个构造函数的主要作用是向JAVA类型别名对象中增加一些JAVA类型对象
  public Configuration() {
    //...已省略部分代码
    typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
    typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
    typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
    typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
    typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);
    typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
  }
}

JAVA类型别名对象TypeAliasRegistry初始化内容如下:

/**
 * 这个类只展示部分代码,主要展示它的内容,理解其功能
 **/
public class TypeAliasRegistry {
  //用Map来存储类型别名和对应的JAVA类型
  private final HashMap<String, Class<?>> TYPE_ALIASES = new HashMap<String, Class<?>>();
  public TypeAliasRegistry() {
    registerAlias("string", String.class);
    registerAlias("byte", Byte.class);
    registerAlias("long", Long.class);
    registerAlias("short", Short.class);
    registerAlias("int", Integer.class);
  }
  //registerAlias方法,主要是将JAVA类型别名和对应的JAVA类型放进Map容器中
 public void registerAlias(String alias, Class<?> value) {
    //省略无用代码.......
   
    //就是将JAVA类型的别名和对应的类传到TYPE_ALIASES这个Map容器中
    TYPE_ALIASES.put(key, value);
  }
}

JDBC类型处理器对象TypeHandlerRegistry,在查询完SQL语句之后会调用这个类将JDBC类型映射成对应的JAVA类型,它的初始化内容如下:

/**
 * 这个类只展示部分代码,主要展示它的内容,理解其功能
 **/
public final class TypeHandlerRegistry { 
  //JDBC类型处理器容器
  private final Map<JdbcType, TypeHandler<?>> JDBC_TYPE_HANDLER_MAP = new EnumMap<JdbcType, TypeHandler<?>>(JdbcType.class);
 /**
 * 初始化时将JDBC类型对应的类型处理器存放在Map容器中
 **/
  public TypeHandlerRegistry() {
    register(Boolean.class, new BooleanTypeHandler());
    register(boolean.class, new BooleanTypeHandler());
    register(JdbcType.BOOLEAN, new BooleanTypeHandler());
    register(JdbcType.BIT, new BooleanTypeHandler());

    register(Integer.class, new IntegerTypeHandler());
    register(int.class, new IntegerTypeHandler());
    register(JdbcType.INTEGER, new IntegerTypeHandler());
    
    register(Double.class, new DoubleTypeHandler());
    register(double.class, new DoubleTypeHandler());
    register(JdbcType.DOUBLE, new DoubleTypeHandler());

    register(String.class, new StringTypeHandler());
    register(JdbcType.CHAR, new StringTypeHandler());
    register(JdbcType.VARCHAR, new StringTypeHandler());

    //.......省略部分代码
  }
//....register函数就是将对应的Map中存储对应的类型和处理器对象

至此,下面的整个语句已经解析结束。

XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);

此时,Configuration类初始化结束,mybatis-config.xml配置文件也加载到parser对象,parser对象的内容截图如下:
在这里插入图片描述
其中

  • parser对象存储的是mybatis-config.xml配置文件
  • environment对象目前是空,但是执行完之后,它里面保存的是事务管理器和数据源
  • configuration对象存储的是MyBatis默认的初始化配置,执行完之后,配置文件的内容会覆盖默认配置
  • typeAliasRegistry存储的是JAVA类型别名对象
  • typeHandlerRegistry存储的是JDBC类型处理器对象
3.2、获取数据源

从上面的内容可以知道,mybatis-config.xml配置文件存储在parser对象中, 但是还没有进行解析, 那么接下来看下面这个函数是怎么解析mybatis-config.xml文件的吧

parser.parse()

这个方法的具体内容如下所示:

public class XMLConfigBuilder extends BaseBuilder {
 /**
 * parse()方法入口
 **/
 public Configuration parse() {
    //如果已解析,则抛一个已解析的异常出去,
    if (parsed) {
      throw new BuilderException("Each MapperConfigParser can only be used once.");
    }
    //将是否已解析的标志字段置为true,开始解析配置文件了
    parsed = true;
    //parser.evalNode("/configuration")方法是获取mybatis-config.xml文件中的标签
    //<configuration>...配置信息....</configuration>中的所有内容
    //parseConfiguration()方法则开始对中间的所有标签进行解析
    parseConfiguration(parser.evalNode("/configuration"));
    //返回结果
    return configuration;
  }
  /**
  * 开始对mybatis-config.xml文件进行解析
  **/
  private void parseConfiguration(XNode root) {
    try {
      //从这里就可以可以看到这些属性其实就是对应mybatis-config.xml文件中的所有配置标签
      //这个方法执行完成之后就可以获取到mybatis-config.xml文件中的所有信息了
      propertiesElement(root.evalNode("properties")); //issue #117 read properties first
      //解析JAVA类型别名类标签
      typeAliasesElement(root.evalNode("typeAliases"));
      //解析拦截器标签
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      //解析settings标签
      settingsElement(root.evalNode("settings"));
      //解析environments标签即获取事务管理和数据源
      environmentsElement(root.evalNode("environments")); 
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      //JDBC结果处理器
      typeHandlerElement(root.evalNode("typeHandlers"));
      //解析mapper文件标签 获取SQL语句
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }
}

此次测试类主要下面两个方法:

  • environmentsElement()方法:获取事务管理和数据源
  • mapperElement()方法:获取SQL语句的StateMent

environmentsElement() 方法源码如下:

public class XMLConfigBuilder extends BaseBuilder {
  /**
  * 方法入口:解析environments标签内容,获取数据源配置
  **/ 
  private void environmentsElement(XNode context) throws Exception {
    //节点内容肯定不为空
    if (context != null) {
       //如果environment标签内容为空,则获取默认配置
      if (environment == null) {
        environment = context.getStringAttribute("default");
      }
      //循环遍历environments标签下的所有节点,知道找到environment标签的ID
      for (XNode child : context.getChildren()) {
        //获取当前节点的ID
        String id = child.getStringAttribute("id");
        //environment标签的id与当前节点的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());
        }
      }
    }
  }
}

方法执行完成之后可以看到如下结果
在这里插入图片描述
environmentsElement() 方法至此结束,此时已经获取到了数据源和事务管理配置了。

3.3、构建SQL语句

接下来看mapperElement() 方法里面是如何获取SQL语句的,即获取StateMent对象,这个过程有点复杂,因为mapper文件中的sql一般如下所示:

<mapper namespace="com.mybatis.DemoMapper">
<select id="queryBaseUserInfoDO" resultType="com.test.entity.BaseUserInfoDO">
        /**UserMapper.queryBaseUserInfoDO 用户信息查询*/
        SELECT
            id as id,
            user_id as userId,
            user_name as userName,
            phone as phone,
            status as status,
            sign_status as signStatus,
            sex as sex 
        FROM
        base_user_info
    </select>
</mapper>

一个StateMent对象其实就是一个完整的SQL语句,要拼接完整的StateMent内容,需要先获取<select | insert | update | delete >这些标签,然后再获取标签里面的参数,比如SQL语句的入参是什么、结果参数是什么、是否要二级缓存等等,这个解析过程导致整个StateMent的拼接有点繁琐,下面就来看一下mapperElement()方法的源码吧。

public class XMLConfigBuilder extends BaseBuilder {
 /**
 * 方法入口:解析mapper文件,获取对应的SQL语句
 **/
 private void mapperElement(XNode parent) throws Exception {
    //mappers标签肯定不为空
    if (parent != null) {
      //遍历mappers标签下的所有子标签
      for (XNode child : parent.getChildren()) {
        //这里将package扫描放在第一位,所以package配置的优先级最高
        //但是我们此次是根据mapper resource进行配置的,所以主要跟踪这个源码
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          //获取根据resource配置的mapper文件
          String resource = child.getStringAttribute("resource");
          ///获取根据url配置的mapper文件
          String url = child.getStringAttribute("url");
          //获取根据class配置的mapper文件
          String mapperClass = child.getStringAttribute("class");
          //如果是根据resource配置的mapper文件则进入以下方法,这个方法也是本次测试类的配置方式
          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());
            //解析mapper.xml文件,获取对应的Statement
            mapperParser.parse();
          }
           //如果是根据url配置的mapper文件则进入以下方法
           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();
          } 
           //如果是根据class配置的mapper文件则进入以下方法
          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() 主要就是解析mapper.xml文件,获取对应的Statement,它的源码如下:

public class XMLMapperBuilder extends BaseBuilder { 
 /**
 * 方法入口:解析mapper文件,并将mapper文件内容设置到Configuration对象中去
 **/
 public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      //这个方法就是真正的解析mapper.xml文件,获取对应的Statement
      //然后把解析后的Statement对象设置到configuration对象中
      configurationElement(parser.evalNode("/mapper"));
      //添加mapper.xml文件到配置类configuration
      configuration.addLoadedResource(resource);
      //绑定namespace,即mapper.xml文件对应的mapper接口
      bindMapperForNamespace();
    }
  }
  /**
  * 进入到mapper.xml文件内部,进行解析
  **/
  private void configurationElement(XNode context) {
    try {
      String namespace = context.getStringAttribute("namespace");
      builderAssistant.setCurrentNamespace(namespace);
      cacheRefElement(context.evalNode("cache-ref"));
      //获取二级缓存标签
      cacheElement(context.evalNode("cache"));
      //获取参数集
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      //获取结果集
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      //获取sql标签内容,标签内容里面是数据库表字段
      sqlElement(context.evalNodes("/mapper/sql"));
      //构建StateMent对象, 根据(select|insert|update|delete)标签构建完整的SQL语句
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new RuntimeException("Error parsing Mapper XML. Cause: " + e, e);
    }
  }
  /**
  * 循环遍历select|insert|update|delete 节点,解析具体的StateMent
  **/
  private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    //循环遍历节点对象
    for (XNode context : list) {
      //根据节点标签获取到此节点具体的SQL内容
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
        //解析上面获取到的SQL内容,然后构建完整的Statement对象
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteStatement(statementParser);
      }
    }
  }
}

statementParser.parseStatementNode() 方法就是拼接具体的StateMent对象,这里直接看解析各个标签后获得的结果,然后将解析出来的结果当做参数传进addMappedStatement() 方法内,在这个方法里面将各个标签解析出来的结果拼接成完整的StateMent对象,然后将此Statement对象添加到Confiuration类中mappedStatements容器中,addMappedStatement() 方法源码内容如下:

public MappedStatement addMappedStatement
   (
      //当前NameSpace的StateMent的唯一ID
      String id,
      //具体的SQL对象
      SqlSource sqlSource,
      //一般默认的为PrepareStament
      StatementType statementType,
      //SQL命令类型 此次为Select
      SqlCommandType sqlCommandType,
      //查询数量
      Integer fetchSize,
      //超时设置
      Integer timeout,
      //SQL入参
      String parameterMap,
      //SQL参数类型
      Class<?> parameterType,
      //SQL返回结果
      String resultMap,
      //SQL返回结果类型
      Class<?> resultType,
      ResultSetType resultSetType,
      boolean flushCache,
      //默认使用一级缓存
      boolean useCache,
      boolean resultOrdered,
      KeyGenerator keyGenerator,
      String keyProperty,
      String keyColumn,
      String databaseId,
      LanguageDriver lang
   ) 
{
     
    if (unresolvedCacheRef) throw new IncompleteElementException("Cache-ref not yet resolved");
    
    id = applyCurrentNamespace(id, false);
    //查询语句
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    //构建SQL StateMent
    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType);
    statementBuilder.resource(resource);
    statementBuilder.fetchSize(fetchSize);
    statementBuilder.statementType(statementType);
    statementBuilder.keyGenerator(keyGenerator);
    statementBuilder.keyProperty(keyProperty);
    statementBuilder.keyColumn(keyColumn);
    statementBuilder.databaseId(databaseId);
    statementBuilder.lang(lang);
    statementBuilder.resultOrdered(resultOrdered);
    setStatementTimeout(timeout, statementBuilder);

    setStatementParameterMap(parameterMap, parameterType, statementBuilder);
    setStatementResultMap(resultMap, resultType, resultSetType, statementBuilder);
    setStatementCache(isSelect, flushCache, useCache, currentCache, statementBuilder);

    MappedStatement statement = statementBuilder.build();
    //将构建好的Statement设置到Configuration对象的MappedStatement容器中
    configuration.addMappedStatement(statement);
    //返回statement对象
    return statement;
  }

通过Debug,查看一下此时的Statement对象的内容如下:
在这里插入图片描述
至此mapperElement() 方法结束,Configuration对象已获取到完整的SQL语句了。

也标志着测试类的第一条语句已全部执行结束

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

此时 mybatis-config.xml文件内容已全部解析完成,并且将文件中的配置信息都赋值到sqlSessionFactory类的configuration对象中,语句执行结束后sqlSessionFactory 的environment对象内容如下,从图中可以知道此时已获取到数据源配置了。
在这里插入图片描述
sqlSessionFactory 的mappedStatements对象内容如下,从下图中可以知道,此时已获取到Statement对象了,即具体的SQL语句。
在这里插入图片描述

3.4、解析mybatis-config.xml过程总结

总结:解析整个mybatis-config.xml的整个流程如下图所示:
在这里插入图片描述

4、常见面试题

  1. 请简单介绍一下你对MyBatis的认识?
    答:

  2. MyBatis加载mapper文件的方式有几种?哪种加载方式的优先级最高?
    答:四种,分别是package、resource、url、class, 其中package的加载方式最高,因为加载mapper文件的时候是第一个对它进行判断的。

  3. 你知道MyBatis的缓存吗?请简单介绍一下。
    答:MyBatis的一级缓存是默认开启的,每查询一次SQL的时候都是将此次的结果保存到一个Map容器中,第二次查询相同的SQL语句时会从缓存中取数据,当然当数据有变动的时候,MyBatis会清除掉缓存。
    MyBatis的二级缓存需要自己在mapper文件中配置cache标签。

  4. MyBatis的执行器有几种?
    答:三种,分别是SIMPLE, REUSE, BATCH。

5、MyBatis数据库连接和SQL执行

链接如下:
Mybatis源码解析之数据库连接与SQL执行

发布了17 篇原创文章 · 获赞 280 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_36526036/article/details/105544825