Mybatis源码概览(一)

余生颓废 提交于 2019-11-29 07:11:34

      一般拿到源码会无从下手,我的基本思路一般就是根据一个基本的helloWorld Debug下去,把主线先大概理一遍,然后再具体分析细节,没有必要一个类一个类细看,看了也会忘掉。自己理源码的时候看不下去时,可以结合网上的分析文章,一边看别人的解析,一边自己对照源码。了解框架设计原理,以后项目中出了问题可以更容易定位。再往上一层面,以后自己可以根据需求扩展框架。

先执行个HelloWorld

   去github上 clone Mybatis代码,然后再其测试源码里添加如下代码

示例代码,里面未贴出来的类自行补全。

        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        SqlSession session = sqlSessionFactory.openSession(true);

        try {
            //后面介绍mybatis通过动态代理来避免手工调用session,直接调用dao接口;
            //BlogDao mapper = session.getMapper(BlogDao.class);
            //List<Blog> blogs= mapper.selectAll();
            List<Blog>blogs= session.selectList("org.apache.ibatis.robin.BlogDao.selectAll");
            System.out.println(blogs);

        } finally {
            session.close();
        }

  mybatis-config.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>
    
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/test?useUnicode=true&amp;characterEncoding=UTF-8"/>
                <property name="username" value="root"/>
                <property name="password" value="root"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="mapper/BlogMapper.xml"/>
    </mappers>
</configuration>

  BlogMapper.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="org.apache.ibatis.robin.BlogDao">
    <resultMap type="org.apache.ibatis.robin.Blog" id="blog">
        <result property="id"        column="id"/>
        <result property="context"     column="context"/>
        <result property="dateCreate"  column="date_create"/>
    </resultMap>
    <insert id="insert" parameterType="org.apache.ibatis.robin.Blog">
            insert into blog(id,context,date_create)
            values (#{id},#{context},now())
    </insert>

    <select id="selectAll" resultMap="blog" flushCache="true" >
        SELECT * from blog
    </select>
</mapper>

注意xml文件放到项目的resource位置,可以通过ide来设置,否则程序会获取不到。 


一步一步DEBUG

    解析配置,构建SqlSessionFactory

    从上面的代码可以看到mybatis重要的执行顺序:输入配置流,由SqlSessionFacotryBuilder来根据输入的配置构建出来一个SqlSessionFactory。

//简略代码 
//解析配置
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
//根据配置返回SqlSessionFactory
public SqlSessionFactory build(Configuration config) {
  return new DefaultSqlSessionFactory(config);
}

   

    构建打开SqlSession (executor生成,plugin织入,缓存开闭)

    获取到SqlSessionFactory后,下一步肯定是获得sqlSession。

sqlSessionFactory.openSession(true);
//这里的true 表示是否自动commit


//debug 进去 这部分是openSession的大体过程;
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
  Transaction tx = null;
  try {
    //先通过configuration获取我们再xml中配的environment,里面包含事务和DataSource;
    final Environment environment = configuration.getEnvironment();
    //这一步是获取JDBC事务
    final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
    tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
    
    //打开Executor过程,见下段落的分析
    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默认的类型是SIMPLE

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
  executorType = executorType == null ? defaultExecutorType : executorType;
  executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
  Executor executor;
  if (ExecutorType.BATCH == executorType) {
    executor = new BatchExecutor(this, transaction);
  } else if (ExecutorType.REUSE == executorType) {
    executor = new ReuseExecutor(this, transaction);
  } else {
    executor = new SimpleExecutor(this, transaction);
  }
  //如果在配置文件settings中打开 <setting name="cacheEnabled" value="true" /> 
  // cacheEnabled 着为true,开启全局缓存,也就是二级缓存;
  //下面查询具体分析CachingExecutor执行查询过程
  if (cacheEnabled) {
    executor = new CachingExecutor(executor);
  }
  //如果自定义了Plugin拦截器,在xml通过plugins配置后,这一步会通过JDK动态代理织入到executor中,
  //生成一个带了拦截器方法功能的Executor
  executor = (Executor) interceptorChain.pluginAll(executor);
  return executor;
}


在根BaseExecutor中可以看到 ,Executor不仅包含了事务,还同时加入localCache ,也就是Session级别的缓存,也是大家常叫的一级缓存,这里提一下一级缓存是默认的,如果非要去掉只能通过在select 语句配置中 flushCache="true"。

protected BaseExecutor(Configuration configuration, Transaction transaction) {
  this.transaction = transaction;
  this.deferredLoads = new ConcurrentLinkedQueue<DeferredLoad>();
  this.localCache = new PerpetualCache("LocalCache");
  this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
  this.closed = false;
  this.configuration = configuration;
  this.wrapper = this;
}

  Executor组装好之后通过new DefaultSqlSession返回我们的SqlSession;

new DefaultSqlSession(configuration, executor, autoCommit);
  
    执行具体查询(重要概念MappedStatement)

SqlSession是直接对数据库发号施令的组件。通过发起下面一个SQL查询,继续Debug进去

List<Blog>blogs= session.selectList("org.apache.ibatis.robin.BlogDao.selectAll");

  先mybatis自动补全一些默认参数(rowBounds主要是指代返回的行数限制)后,进去下面的代码

@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
  try {
    //MappedStatement是Mybatis的精髓,如果Bean对Spring是一样的道理;这里我们要重点介绍下
    //属性MappedStatement 是一个SQL执行语句的包装,里面的属性有SqlSource(指代SQL具体的语句);
    //属性StatementType 如果是PREPARED表明执行预编译执行,这样不仅防止SQL注入,而且还能避免SQL重复解析;
    //属性id 指代我们唯一确定MappedStatement唯一标识;
    //还有ResultMap 和ParameterMap等等,这里就不解释来;
    //可以说MappedStatement是发起SQL请求的所需的必备数据;
    MappedStatement ms = configuration.getMappedStatement(statement);
    //进行query查询,转下段分析
    return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  //获取SQL
  BoundSql boundSql = ms.getBoundSql(parameterObject);
  //通过参数,sql,和rowBounds一起拼装出cacheKey
  CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
  return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
//进入到查询
@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) {
    flushCacheIfRequired(ms);
    if (ms.isUseCache() && resultHandler == null) {
      ensureNoOutParams(ms, parameterObject, boundSql);
      @SuppressWarnings("unchecked")
      List<E> list = (List<E>) tcm.getObject(cache, key);
      if (list == null) {
        list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        tcm.putObject(cache, key, list); // issue #578 and #116
      }
      return list;
    }
  }
  //缓存中取不到,直接执行query,这里delegate指代我们前面生成的SimpleExecutor;
  return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
//跳转到下面的方法
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
  if (closed) {
    throw new ExecutorException("Executor was closed.");
  }
  //清掉cache
  if (queryStack == 0 && ms.isFlushCacheRequired()) {
    clearLocalCache();
  }
  List<E> list;
  try {
    queryStack++;
    list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
    if (list != null) {
      handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
    } else {
      //跳到下面到数据库中去查询数据
      list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }
  } finally {
    queryStack--;
  }
  if (queryStack == 0) {
    for (DeferredLoad deferredLoad : deferredLoads) {
      deferredLoad.load();
    }
    // issue #601
    deferredLoads.clear();
    if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
      // issue #482
      clearLocalCache();
    }
  }
  return list;
}
//从这个方法可以明显看出里面的localCache,指代前面说的Session缓存,即一级缓存
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  List<E> list;
  localCache.putObject(key, EXECUTION_PLACEHOLDER);
  try {
    //这一步主要是获取Connection,然后准备PreparedState,执行查询返回结果
    list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
  } finally {
    localCache.removeObject(key);
  }
  //把新结果缓存起来
  localCache.putObject(key, list);
  if (ms.getStatementType() == StatementType.CALLABLE) {
    localOutputParameterCache.putObject(key, parameter);
  }
  return list;
}

至此我们的查询SQL就执行完了。

这一路涉及了 Configuration --->SqlSessionFactory------>SqlSession(里面又包装了 Executor,Cache,MappedStatement) 这几个重要概念;

下节主要讲Mybatis通过代理 把原先自己需要直接调用 sqlSession来执行 改成只需调用相应dao接口类,在实际项目中省掉大量代码,以及与spring结合的实现原理;


本文链接 http://my.oschina.net/robinyao/blog/645263

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!