Mybatis-执行流程源码概读

这里我们简要分析一下Mybatis的执行流程,内在的其他特性细节会有单独的文章进行分析;

贴一段简单的mybatis(3.4.5)使用时的测试代码

(配置文件省略了,如果要搭建环境推荐一个清晰的博客: https://www.cnblogs.com/xdp-gacl/p/4261895.html  )

配置文件

mybatis.cfg.xml

<?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="application.properties"></properties>
    <settings>
        <setting name="logImpl" value="STDOUT_LOGGING" />
        <setting name="cacheEnabled" value="true"/>
    </settings>

    <!-- 配置mybatis运行环境 -->
    <environments default="cybatis">
        <environment id="cybatis">
            <!-- type="JDBC" 代表使用JDBC的提交和回滚来管理事务 -->
            <transactionManager type="JDBC" />
            <!-- mybatis提供了3种数据源类型,分别是:POOLED,UNPOOLED,JNDI(POOLED 表示支持JDBC数据源连接池) -->
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver" />
                <property name="url" value="jdbc:mysql://localhost:3306/test?serverTimezone=UTC"/>
                <property name="username" value="root" />
                <property name="password" value="root" />
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <mapper resource="./mapper/user.xml"/>
        <mapper resource="./mapper/user-mapper.xml"/>
    </mappers>
</configuration>

user.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.uu.bean.User" >

  <resultMap id="User" type="com.uu.bean.User" >
    <result column="id" property="id" jdbcType="BIGINT" />
    <result column="roleId" property="roleId" jdbcType="BIGINT" />
    <result column="userName" property="userName" jdbcType="VARCHAR" />
  </resultMap>

  <cache flushInterval="60000" size="1024" readOnly="false"/>

  <select id="byroleId" resultType="com.uu.bean.User">
    select * from user where roleId in
    <foreach collection="collection" open="(" separator="," close=")" item="val">
      ${val}
    </foreach>
  </select>
</mapper>

user-mapper.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="mapper.UserMapper" >

  <select id="getById" resultType="com.uu.bean.User" parameterType="int">
    select * from user where id = #{id}
  </select>
</mapper>

User.java 

package com.uu.bean;

public class User implements java.io.Serializable{

    private int id;
    private int roleId;
    private String userName;

    //get set方法可以不写,Mybatis有自己的办法将值写入
}

测试示例代码

package test;

import java.io.IOException;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.uu.UserMapper;

public class T {
	
//通过namespace的方式执行sql
  @Test
  public void test()  {
    SqlSession sqlSession = sessionFactory.openSession();
    Set a = new HashSet();
    a.add(2);
    a.add(3);
    List<User> rs = sqlSession.selectList("com.uu.bean.User.byroleId",a);
    for(User i:rs){
       System.out.println(i);
    }
  }

//通过接口类的方式执行sql
	@Test
	public void test() {
		SqlSessionFactory sqlSessionFactory = getSessionFactory();
		SqlSession sqlSession = sqlSessionFactory.openSession();
		UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
		userMapper.deleteOne();
		System.out.println("success");
		sqlSession.commit();
	}

// Mybatis 通过SqlSessionFactory获取SqlSession, 然后才能通过SqlSession与数据库进行交互
	private static SqlSessionFactory getSessionFactory() {
		
		SqlSessionFactory sessionFactory = null;
		String resource = "mybatis-config.xml";
		try {
			sessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader(resource));
		} catch (IOException e) {
			e.printStackTrace();
		}
		return sessionFactory;
	}
}

SqlSessionFactory

代码入口是 SqlSessionFactoryBuilder
1、Resources.getResourceAsReader(resource) 是将配置文件解析封装成一个  java.io.Reader 对象,这个可以不用关注;

2、new SqlSessionFactoryBuilder 这一行代码也没有复杂的操作,只是返回一个 org.apache.ibatis.session.SqlSessionFactoryBuilder 对象;

3、构建SqlSessionFactory的逻辑在build里面,这个build方法里会创建一个XMLConfigBuilder对象,在执行 XMLConfigBuilder的构造方法时,会自动做DTD校验,如果配置不符合dtd的话,就会异常终止掉;

通过解析配置文件,并实例化 SqlSessionFactory对象

解析配置文件

配置文件在程序启动的时候会做一个全量的解析,这个过程被委托给下面流程

org.apache.ibatis.session.SqlSessionFactoryBuilder#build(java.io.Reader, java.lang.String, java.util.Properties)
↓
org.apache.ibatis.builder.xml.XMLConfigBuilder#parse
↓
org.apache.ibatis.session.SqlSessionFactoryBuilder#build(org.apache.ibatis.session.Configuration)

返回值就是一个 SqlSessionFactory对象,核心是 XMLConfigBuilder#parse方法;这里封装了解析配置文件的模板流程

  private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
//解析properties路径
      propertiesElement(root.evalNode("properties"));
//解析全局配置
      Properties settings = settingsAsProperties(root.evalNode("settings"));
//虚拟文件系统相关(在通过package方式扫描获得mapper接口类时会使用该类)
      loadCustomVfs(settings);
//日志打印相关
      loadCustomLogImpl(settings);
//java类与resulttype,parametertype别名映射
      typeAliasesElement(root.evalNode("typeAliases"));
//解析插件配置
      pluginElement(root.evalNode("plugins"));
//返回值java对象实例化工厂
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
//解析env相关配置,eg.数据源相关配置
      environmentsElement(root.evalNode("environments"));
//多数据库类型配置(更具当前的数据库种类和sql标记的databaseId属性执行不同的sql)
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
//注册数据库类型与java类型映射关系处理器
      typeHandlerElement(root.evalNode("typeHandlers"));
//解析mapper配置文件(sql文件)
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

settings

这里可以配置二级缓存,日志打印等配置

mappers

这里会解析全部sql语句配置,并将解析好的语句封装成 MappedStatement对象缓存在org.apache.ibatis.session.Configuration#mappedStatements中

配置mapper有3种方式:package、resource、url定位语句的资源路径

解析mapper配置的入口(Mybatis对应的配置文件解析类命名都很有特点的,解析Mapper的类中间会带有Mapper字样的XMLMapperBuilder,之前说的总配置文件解析器则是带Config的XMLConfigBuilder)

org.apache.ibatis.builder.xml.XMLMapperBuilder#parse

 代码来到:

org.apache.ibatis.builder.xml.XMLMapperBuilder#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");
      }
//解析namespace
      builderAssistant.setCurrentNamespace(namespace);
//解析二级缓存信息
      cacheRefElement(context.evalNode("cache-ref"));
      cacheElement(context.evalNode("cache"));
//解析parameterMap
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
//解析resultMap
      resultMapElements(context.evalNodes("/mapper/resultMap"));
//解析sql标签
      sqlElement(context.evalNodes("/mapper/sql"));
//解析crud标签
      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>会在各种crud标签之前被解析,这样设计为了满足 crud 标签里引用<sql>标签的需求;

我们重点还是看最后的crud标签,这里就需要语句相关的配置文件解析器(XMLStatementBuilder,看 类文件命名很有规律)来处理;

  private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
//配置文件中有多少个crud标签,那么list的大小就是多少
    for (XNode context : list) {
      //解析一个语句
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteStatement(statementParser);
      }
    }
  }

最终调用的是:

org.apache.ibatis.builder.xml.XMLStatementBuilder#parseStatementNode

方法,将解析好的语句封装成 MappedStatement 对象,放入内存中

SqlSession 执行语句

SqlSession sqlSession = sqlSessionFactory.openSession();

最终调用的是

org.apache.ibatis.session.defaults.DefaultSqlSessionFactory.openSessionFromDataSource(ExecutorType, TransactionIsolationLevel, boolean)
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();
    }
  }

这里创建了事务,并实例化了一个 Executor 对象(这里要说一句:如果我们在全局配置文件里设置了二级缓存可用的话,这个Executor将会是一个CachingExecutor对象,在CachingExecutor里将会引用一个SimpleExecutor对象,这就是用到了装饰器设计模式;如果关闭二级缓存的话,这里将会直接是一个 SimpleExecutor对象);

然后实例化一个 DefaultSqlSession 并返回,这应该是简单工厂设计模式;

namespace方式

使用namespace方式调用逻辑比较清晰,我们以selectList api为例子分析一下调用执行流程。

示例:List<User> rs = sqlSession.selectList("com.uu.bean.User.byroleId",param);

在执行过程中插件这个组件是不得不说的,插件的原理简单理解就是在程序里要获得某些特定的对象(目前来看有Executor,StatementHandler,ParameterHandler,ResultSetHandler)时,框架可以根据配置将这些类用代理封装起来(封装的逻辑可以看看org.apache.ibatis.plugin.Plugin#wrap 这个方法),然后在执行某些特定的方法时先执行代理的方法(这些规则都通过插件类上的 @Intercepts 注解来进行封装);

插件代理封装可以看看这里个方法

org.apache.ibatis.session.Configuration#newExecutor(org.apache.ibatis.transaction.Transaction, org.apache.ibatis.session.ExecutorType)
org.apache.ibatis.session.Configuration#newStatementHandler
org.apache.ibatis.session.Configuration#newParameterHandler
org.apache.ibatis.session.Configuration#newResultSetHandler

 代码简易流程跟踪:

org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object)
↓
org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)(如果开启二级缓存)
↓
org.apache.ibatis.executor.BaseExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler, org.apache.ibatis.cache.CacheKey, org.apache.ibatis.mapping.BoundSql)
↓
org.apache.ibatis.executor.SimpleExecutor#doQuery
↓
org.apache.ibatis.executor.SimpleExecutor#prepareStatement
↓
org.apache.ibatis.executor.statement.SimpleStatementHandler#query
↓
org.apache.ibatis.executor.resultset.DefaultResultSetHandler#handleResultSets
↓
org.apache.ibatis.executor.resultset.DefaultResultSetHandler#handleResultSet
↓
org.apache.ibatis.executor.resultset.DefaultResultSetHandler#handleRowValuesForSimpleResultMap(这里将会循环包装么一个数据库返回值)
↓
org.apache.ibatis.executor.resultset.DefaultResultSetHandler#getRowValue(org.apache.ibatis.executor.resultset.ResultSetWrapper, org.apache.ibatis.mapping.ResultMap, java.lang.String)(这个方法会通过Mybatis提供的机制将数据库值设置进入pojo,即使pojo没有get set方法)

mapper接口方式

接下是通过Mapper接口的代理类来执行sql方式

UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

最终调试调用到

org.apache.ibatis.binding.MapperRegistry.getMapper(Class<T>, SqlSession)

 @SuppressWarnings("unchecked")
  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }
public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

看到这里就大概明白了Mapper对象是通过 JDK 提供的动态代理机制实现的。

代理逻辑重点就在MapperProxy 这个类里面了,后续查询逻辑与直接使用namespace方式的应该是一致的,主要区别是中间有个代理类,跟踪代码:

org.apache.ibatis.binding.MapperProxy#invoke
↓
org.apache.ibatis.binding.MapperMethod#execute
↓
以select为例子:
case SELECT:
        if (method.returnsVoid() && method.hasResultHandler()) {
          executeWithResultHandler(sqlSession, args);
          result = null;
        } else if (method.returnsMany()) {
          result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {
          result = executeForMap(sqlSession, args);
        } else if (method.returnsCursor()) {
          result = executeForCursor(sqlSession, args);
        } else {
          Object param = method.convertArgsToSqlCommandParam(args);
//这里可以看到最终是使用的sqlSession的selectOne方法
          result = sqlSession.selectOne(command.getName(), param);
          if (method.returnsOptional()
              && (result == null || !method.getReturnType().equals(result.getClass()))) {
            result = Optional.ofNullable(result);
          }
        }
        break;

这下清晰了

代码已经上传到 code-demo: 常用框架集成示例(大部分基于springboot) - Gitee.com

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值