mybatis 源码分析(一)框架结构概览
本篇博客将主要对 mybatis 整体介绍,包括 mybatis 的项目结构,执行的主要流程,初始化流程,API 等各模块进行简单的串联,让你能够对 mybatis 有一个整体的把握。另外在 mybatis 源码的阅读过程中,如果不想写 demo 可以直接使用项目中的单元测试;
一、mybatis 结构介绍
mybatis的主要功能和使用 demo,在网上已经有很多了我就不再啰嗦了,同时 官方文档 也非常的详细;另外 mybatis 中使用了多种设计模式,包括建造者、动态代理、策略、装饰器模式等,在查看源码的时候,最好先对这些设计模式有一定的了解;
其中 mybatis 的模块结构如下:

mybatis 的执行流程如下:
- 首先通过 Java API 或者 XML 配置完成初始化,最终所有的配置都在 Configuration 类中维护;
 - 然后通过 SqlSessionFactory 得到 SqlSession,这里 SqlSession 就是 mybatis 的顶层 API 了,主要通过他完成数据库的增删改查等操作;
 - 然后 SqlSession 将具体的操作委托给 Executor 执行,Executor 就是 mybatis 的调度核心了,主要职责有 SQL 语句生成、一二级缓存维护和事务的相关操作;
 - 然后 Executor 将数据库相关的操作委托给 StatementHandler,StatementHandler 中完成了 mybatis 最核心的工作,包括参数绑定,指定 SQL 语句,结果集映射等;
 
具体过程如图所示:

二、初始化
mybatis 中包含了很多的配置项,具体每一项的讲解 官网 也很详细,其结构大致如下:(另外正如上面说的 mybatis 的配置项最后都由 Configuration 类维护,这其实就是外观模式)
configuration(配置)
  properties(属性)
  settings(设置)
  typeAliases(类型别名)
  typeHandlers(类型处理器)
  objectFactory(对象工厂)
  plugins(插件)
  environments(环境配置)
    environment(环境变量)
      transactionManager(事务管理器)
      dataSource(数据源)
  mappers(映射器)
1. Java API 初始化
Java API 初始化的方式虽然不常用,但是相较于 XML 的方式可以更清楚的看到 Configuration 的构成,其示例如下:
PooledDataSource dataSource = new PooledDataSource();
dataSource.setDriver("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/mybatis?serverTimezone=GMT");
dataSource.setUsername("root");
dataSource.setPassword("root");
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("development", transactionFactory, dataSource);
Configuration configuration = new Configuration(environment);
configuration.addMapper(UserMapper.class);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
2. XML 配置初始化
相交于 Java API 的方式,XML 配置初始化,必然会多出 XML 的解析部分;代码如下:
String resource = "org/apache/ibatis/builder/MapperConfig.xml";
Reader reader = Resources.getResourceAsReader(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
SqlSession sqlSession = sqlSessionFactory.openSession();
下面是一个相对完整的配置示例:
<?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="org/apache/ibatis/databases/blog/blog-derby.properties"/>
  <settings>
    <setting name="cacheEnabled" value="true"/>
    <setting name="lazyLoadingEnabled" value="false"/>
    ...
  </settings>
  <typeAliases>
    <typeAlias alias="Author" type="org.apache.ibatis.domain.blog.Author"/>
    <typeAlias alias="Blog" type="org.apache.ibatis.domain.blog.Blog"/>
    ...
  </typeAliases>
  <typeHandlers>
    <typeHandler javaType="String" jdbcType="VARCHAR" handler="org.apache.ibatis.builder.CustomStringTypeHandler"/>
  </typeHandlers>
  <objectFactory type="org.apache.ibatis.builder.ExampleObjectFactory">
    <property name="objectFactoryProperty" value="100"/>
  </objectFactory>
  <plugins>
    <plugin interceptor="org.apache.ibatis.builder.ExamplePlugin">
      <property name="pluginProperty" value="100"/>
    </plugin>
  </plugins>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC">
        <property name="" value=""/>
      </transactionManager>
      <!--<dataSource type="UNPOOLED">-->
      <dataSource type="POOLED">
        <property name="driver" value="${driver}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <mapper resource="org/apache/ibatis/builder/AuthorMapper.xml"/>
    <mapper resource="org/apache/ibatis/builder/BlogMapper.xml"/>
    ...
  </mappers>
</configuration>
其解析的流程如下:

主要代码如下:
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
  try {
    XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
    return build(parser.parse());
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error building SqlSession.", e);
  } finally {
    ErrorContext.instance().reset();
    try {
  	inputStream.close();
    } catch (IOException e) { }
  }
}
public SqlSessionFactory build(Configuration config) {
  return new DefaultSqlSessionFactory(config);
}
从上面的代码和流程图中可以看到,XML 初始化的主要流程被封装到了 XMLConfigBuilder 当中;主要的代码逻辑如下:
public Configuration parse() {
  if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); }
  parsed = true;
  parseConfiguration(parser.evalNode("/configuration"));
  return 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);
  }
}
三、SqlSession 使用方式
1. 直接指定 MappedStatement
try (SqlSession session = sqlMapper.openSession()) {
  Author author = session.selectOne("org.apache.ibatis.domain.blog.mappers.AuthorMapper.selectAuthor", new Author(101));
}
这种方式通过 namespace + sqlId 的方式直接指定 MappedStatement;这种方式因为直接编写字符串和强类型转换,既不安全也稍显麻烦,所以现在已经不推荐使用了;
@Override
public <T> Cursor<T> selectCursor(String statement, Object parameter, RowBounds rowBounds) {
  try {
    MappedStatement ms = configuration.getMappedStatement(statement);
    Cursor<T> cursor = executor.queryCursor(ms, wrapCollection(parameter), rowBounds);
    registerCursor(cursor);
    return cursor;
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}
2. 动态代理 Mapper 的方式
try (SqlSession session = sqlMapper.openSession()) {
  AuthorMapper mapper = session.getMapper(AuthorMapper.class);
  Author author = mapper.selectAuthor(500);
}
这种方式不经避免了以上的问题,同时也能够使用注解的方式编写 sql,而且可以使用 IDE 提示;现在一般都推荐使用这种方式;但是其最终也是调用了上面的接口;
首先在初始化的时候通过 bindMapperForNamespace,注册对应的 Mapper(要求namespace和Mapper的全限定名保持一致);
// XMLMapperBuilder
private void bindMapperForNamespace() {
  String namespace = builderAssistant.getCurrentNamespace();
  if (namespace != null) {
    Class<?> boundType = null;
    try {
      boundType = Resources.classForName(namespace);
    } catch (ClassNotFoundException e) { //ignore, bound type is not required }
    if (boundType != null) {
      if (!configuration.hasMapper(boundType)) {
        configuration.addLoadedResource("namespace:" + namespace);
        configuration.addMapper(boundType);
      }
    }
  }
}
// MapperRegistry
public <T> void addMapper(Class<T> type) {
  if (type.isInterface()) {
    if (hasMapper(type)) { throw new BindingException("Type " + type + " is already known to the MapperRegistry."); }
    boolean loadCompleted = false;
    try {
      knownMappers.put(type, new MapperProxyFactory<>(type));  // 添加代理工厂
      MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
      parser.parse();
      loadCompleted = true;
    } finally {
      if (!loadCompleted) {
        knownMappers.remove(type);
      }
    }
  }
}
使用的时候,通过 class 类名获取 MapperProxyFactory 代理工厂,制造一个新的 Mapper 代理(注意这里时每次都要生成一个代理类,因为其中包含了 SqlSession,而 SqlSession 是线程不安全的所以不能缓存,但是我觉得这里任然是可以优化的,有兴趣你可以自己尝试一下);
try (SqlSession session = sqlMapper.openSession()) {
  AuthorMapper mapper = session.getMapper(AuthorMapper.class);  // 代理类
}
// MapperRegistry
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);
  }
}
// MapperProxyFactory
public T newInstance(SqlSession sqlSession) {
  final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
  return newInstance(mapperProxy);
}
// MapperProxy
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
    if (Object.class.equals(method.getDeclaringClass())) {  // 从Object中继承的方法
      return method.invoke(this, args);
    } else if (method.isDefault()) {  // 有默认实现的接口方法
      return invokeDefaultMethod(proxy, method, args);
    }
  } catch (Throwable t) {
    throw ExceptionUtil.unwrapThrowable(t);
  }
  final MapperMethod mapperMethod = cachedMapperMethod(method);
  return mapperMethod.execute(sqlSession, args);  // 然后由 MapperMethod 执行,这里使用策略模式,后面还会详细讲解
}
总结
SqlSession 是线程不安全的,所以在示例代码中每次使用都会将其关闭?
在 mybatis 中还有一个类 SqlSessionManager 里面有一个 ThreadLocal 用来管理 SqlSession,在 Spring 中也同样是用 SqlSessionHolder 来管理的,所以并不会每次都创建一个新的 SqlSession;
以上内容只是大致将了 mybatis 的主要结构,后面的章节还会分模块进行讲解;
另外本文主要参考了《MyBatis技术内幕》,有兴趣的可以自行查看;
mybatis 源码分析(一)框架结构概览的更多相关文章
- MyBatis源码分析-MyBatis初始化流程
		
MyBatis 是支持定制化 SQL.存储过程以及高级映射的优秀的持久层框架.MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集.MyBatis 可以对配置和原生Map使用简 ...
 - MyBatis源码分析-SQL语句执行的完整流程
		
MyBatis 是支持定制化 SQL.存储过程以及高级映射的优秀的持久层框架.MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集.MyBatis 可以对配置和原生Map使用简 ...
 - MyBatis源码分析(5)——内置DataSource实现
		
@(MyBatis)[DataSource] MyBatis源码分析(5)--内置DataSource实现 MyBatis内置了两个DataSource的实现:UnpooledDataSource,该 ...
 - MyBatis源码分析(4)—— Cache构建以及应用
		
@(MyBatis)[Cache] MyBatis源码分析--Cache构建以及应用 SqlSession使用缓存流程 如果开启了二级缓存,而Executor会使用CachingExecutor来装饰 ...
 - MyBatis源码分析(3)—— Cache接口以及实现
		
@(MyBatis)[Cache] MyBatis源码分析--Cache接口以及实现 Cache接口 MyBatis中的Cache以SPI实现,给需要集成其它Cache或者自定义Cache提供了接口. ...
 - MyBatis源码分析(2)—— Plugin原理
		
@(MyBatis)[Plugin] MyBatis源码分析--Plugin原理 Plugin原理 Plugin的实现采用了Java的动态代理,应用了责任链设计模式 InterceptorChain ...
 - 【MyBatis源码分析】select源码分析及小结
		
示例代码 之前的文章说过,对于MyBatis来说insert.update.delete是一组的,因为对于MyBatis来说它们都是update:select是一组的,因为对于MyBatis来说它就是 ...
 - MyBatis源码分析之环境准备篇
		
前言 之前一段时间写了[Spring源码分析]系列的文章,感觉对Spring的原理及使用各方面都掌握了不少,趁热打铁,开始下一个系列的文章[MyBatis源码分析],在[MyBatis源码分析]文章的 ...
 - Mybatis源码分析-BaseExecutor
		
根据前文Mybatis源码分析-SqlSessionTemplate的简单分析,对于SqlSession的CURD操作都需要经过Executor接口的update/query方法,本文将分析下Base ...
 - Mybatis源码分析-StatementHandler
		
承接前文Mybatis源码分析-BaseExecutor,本文则对通过StatementHandler接口完成数据库的CRUD操作作简单的分析 StatementHandler#接口列表 //获取St ...
 
随机推荐
- Java学习笔记之---类和对象
			
Java学习笔记之---类和对象 (一)类 类是一个模板,它描述一类对象的行为和状态 例如:动物类是一个类,动物们都有属性:颜色,动物们都有行为:吃饭 public class Dog { Stri ...
 - WordPress教程之如何创建博客内容
			
上两篇教程的链接: Wordpress教程之初识WordPress Wordpress教程之如何入门WordPress Hostwinds共享主机vps限时五折优惠链接 现在,你的 WordPress ...
 - vue组件间通信六种方式(完整版)
			
本文总结了vue组件间通信的几种方式,如props. $emit/ $on.vuex. $parent / $children. $attrs/ $listeners和provide/inject,以 ...
 - JavaScript入门小案例
			
笔记: <!-- JavaScript的特点: 1.基于对象和事件驱动 JavaScript把HTML页面中的每一个元素都当做一个对象来处理,并且这些对象都具有层次关系, 像一颗倒立的树,这种关 ...
 - Mac上pycharm集成pyspark
			
前提: 1.已经安装好spark.我的是spark2.2.0. 2.已经有python环境,我这边使用的是python3.6. 一.安装py4j 使用pip,运行如下命令: pip install p ...
 - druid一步到位
			
版权声明:署名,允许他人基于本文进行创作,且必须基于与原先许可协议相同的许可协议分发本文 (Creative Commons) 在配置application.yml文件的时候,原本写的是MySQL的连 ...
 - web前端笔试篇(一)
			
[ 题外话 ]:本博主作为一名准毕业生,即将面临毕业就业问题,即将到大四了,不准备考研的我,那么该去干嘛呢?毫无疑问,那就是实习,那么即使是实习,那么在要想进入自己心仪的企业之前,笔试这一关终究是无法 ...
 - Android之无限轮播图源代码
			
Android轮播广告图是大家经常用到的一个控件今天便撸了一把代码 实现步骤 使用Viewpager进行实现图片滑动 设置ViewPager的数据,让其无限切换 Activity代码 public c ...
 - Linux目录文件
			
/binbin是binary的缩写.这个目录沿袭了UNIX系统的结构,存放着使用者最经常使用的命令.例如cp.ls.cat,等等. /boot这里存放的是启动Linux时使用的一些核心文件. /dev ...
 - Python基础之str常用方法、for循环
			
初学python,有些地方可能还不够明白,希望各位看官发现我的错误后留言指正! 一.字符串的索引与切片 注:字符串的第一位的索引值是0 1.索引案例 s = 'abcd' s1 = s[0] prin ...