乐趣区

关于mybatis:MyBatis源码学习笔记一-初遇篇

这个系列的文章的开篇《当咱们说起看源码时,咱们是在看什么》在去年十月份就开始了,明天开始填这个系列的坑。MyBatis 是我接触的第一个 ORM 框架,也是我目前最为相熟的 ORM 框架,对它始终停留在用的阶段,明天尝试来看 MyBatis 的外部结构。如果还不会 MyBatis 的,能够先去看下《伪装是小白之重学 MyBatis(一)》。

那该如何看源码呢?

我是把 MyBatis 的源码下载下来, 茫无目标的看?这会不会迷失在源码中呢,我记得我刚到我以后这家公司的时候,看代码就是一个一个办法的看,而后感觉很头疼,也没看懂最初再做什么。前面反思了一下,其实应该关注宏观的流程,就是这个代码实现了什么性能,这些代码都是为了实现这个性能,不用每一个办法都一行一行的看,以办法为单位去看,这个办法从整体上来看做了什么样的事件,先不用过多的去关注外部的实现细节。这样去看对代码大略心里就无数了。同样的在 MyBatis 这里,这也是我第一个特地认真钻研的代码,所以 MyBatis 系列的第一篇,咱们先从宏观上看其实现,在前面的过程中缓缓补全其细节。本篇的主线是咱们在 xml 中写的增删改查语句到底是怎么被执行的。

参阅了很多 MyBatis 源码的材料,MyBatis 的整体架构能够分为三层:

  • 接口层:SqlSession 是咱们平时与 MyBatis 实现交互的外围接口 (包含后续整合 SpringFramework 用到的 SqlSessionTemplte)
  • 核心层: SqlSession 执行的办法,底层须要通过配置文件的解析、SQL 解析,以及执行 SQL 时的参数映射、SQL 执行、后果集映射,另外还有交叉其中的扩大插件。
  • 反对层: 核心层的性能实现,是基于底层的各个模块,独特协调实现的。

搭建 MyBatis 的环境

搭建 MyBatis 的环境在《伪装是小白之重学 MyBatis(一)》曾经讲过了,这里只简略在讲一下:

  • 引入 Maven 依赖
<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis</artifactId>
  <version>3.5.6</version>
 </dependency>
  <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.5</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.30</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.30</version>
        </dependency>
  • 而后来一张表
CREATE TABLE `student`  (`id` int(11) NOT NULL COMMENT '惟一标识',
  `name` varchar(255) ,
  `number` varchar(255) ,
  `money` int(255) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4;
  • 来个 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>
    <!-- 加载配置文件 -->
    <properties resource="jdbc.properties"/>
    <!-- 指定默认环境, 个别状况下, 咱们有三套环境,dev 开发 ,uat 测试 ,prod 生产 -->
    <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/studydatabase?characterEncoding=utf-8"/>
                <property name="username" value="root"/>
                <property name="password" value="root"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <!-- 设置扫描的 xml,org/example/mybatis 是包的全类名,StudentMapper.xml 会讲 -->

    <mappers>
        <!-- 设置扫描的 xml,org/example/mybatis 是包的全类名,这个 BlogMapper.xml 会讲
         <package name = "org.example.mybatis"/> <!-- 包下批量引入 单个注册 -->
          <mapper resource="org/example/mybatis/StudentMapper.xml"/> 
    </mappers>
    </mappers>
</configuration>
  • 来个 Student 类
public class Student {
    private Long id;
    private String name;
    private String number;
    private String money;
    // 省略 get set 函数
}
  • 来个 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 = "org.example.mybatis.StudentMapper">
    <select id = "selectStudent" resultType = "org.example.mybatis.Student">
        SELECT * FROM STUDENT
    </select>
</mapper>
  • 来个接口
public interface StudentMapper {List<Student> selectStudent();
}
  • 日志配置文件
log4j.rootCategory=debug, CONSOLE

# Set the enterprise logger category to FATAL and its only appender to CONSOLE.
log4j.logger.org.apache.axis.enterprise=FATAL, CONSOLE

# CONSOLE is set to be a ConsoleAppender using a PatternLayout.
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.Encoding=UTF-8
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} %-6r [%15.15t] %-5p %30.30
  • 开始查问之旅
public class MyBatisDemo {public static void main(String[] args) throws Exception {Reader reader = Resources.getResourceAsReader("conf.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
        List<Student> studentList = studentMapper.selectStudent();
        studentList.forEach(System.out::println);
    }
}

执行之后就能够在控制台看到如下输入了:

让咱们从 SQL 的执行之旅开始谈起

执行过程浅析

下面的执行过程大抵能够分成三步:

  • 解析配置文件,构建 SqlSessionFactory
  • 通过 SqlSessionFactory 拿到 SqlSession,进而取得代理类
  • 执行代理类的办法

解析配置文件

解析配置文件通过 SqlSessionFactoryBuilder 的 build 办法来执行, build 办法有几个重载:

Reader 指向了 conf 文件, environment 是环境,properties 用于 conf 向其余 properties 取值。咱们的配置文件是一个 xml,所以 XmlConfigBuilder 最终是对配置文件的封装。这里咱们不关注 XmlBuilder 是如何构建的,咱们接着往下看,构建 Xml 对象之后,调用 parse 办法,将其转换为 MyBatis 的 Configuration 对象:

// parseConfiguration 这个办法用于取 xml 标签的值并将其设置到 Configuration 上
public Configuration parse() {if (parsed) {throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }
// 取标签的过程,XML->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")); // 获取 mapper 办法,
    } catch (Exception e) {throw new BuilderException("Error parsing SQL Mapper Configuration. Cause:" + e, e);
    }
  }
  • Configuration 概览

  • mapperElement

​ 留神咱们本篇的主题是重点看咱们写在 xml 标签中的 sql 是如何被执行的,所以咱们这里重点看 parseConfiguration 的 mapperElement 的办法。从名字上咱们大抵推断,这个办法是加载 mapper.xml 文件的。咱们点进去看一下:

// parent 是 mappers 标签
private void mapperElement(XNode parent) throws Exception {if (parent != null) {for (XNode child : parent.getChildren()) { // 遍历 mappers 上面的结点
        if ("package".equals(child.getName())) {  // 如果是 package 标签则走批量引入
          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); // 加载指定文件夹下的 XML
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); 
            mapperParser.parse(); // 将 mapper 中的标签值映射成 MyBatis 的对象} 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(); // 咱们大抵看下 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 标签,咱们能够通过调试验证这一点:

public void parse() {if (!configuration.isResourceLoaded(resource)) {configurationElement(parser.evalNode("/mapper"));
    configuration.addLoadedResource(resource);
    bindMapperForNamespace();}

  parsePendingResultMaps();
  parsePendingCacheRefs();
  parsePendingStatements();}

在介绍的时候西安判断该 xml 是否曾经加载过了, 而后解析 mapper 标签下的增删改查等标签,咱们能够在 configurationElement 看到这一点。

  private void configurationElement(XNode context) {
    try {String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.isEmpty()) {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"));
      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) {if (configuration.getDatabaseId() != null) { // dataBaseId 用于指明该标签在哪个数据库下执行
    buildStatementFromContext(list, configuration.getDatabaseId());
  }
  buildStatementFromContext(list, null);
}

parseStatementNode 办法比拟长,最终还是在解析 Mapper.xml 的 select、insert、update、delete 的属性, 将解析的属性传递 builderAssistant.addMappedStatement() 办法中去,该办法参数略多,这来咱们上截图:

到此咱们根本完结看构建 configuration 的过程,咱们能够认为在这一步,Mybatis 的配置文件和 Mapper.xml 曾经根本解析结束。

获取 SqlSession 对象

SqlSession 是一个接口,有两个次要实现类:

咱们在第一步 build 进去的事实上是 DefaultSqlSessionFactory:

public SqlSessionFactory build(Configuration config) {return new DefaultSqlSessionFactory(config);
}

这里事实上 openSession 也是由 DefaultSqlSessionFactory 来执行的,咱们看下在 openSession 这个过程中大抵做了什么:

@Override
public SqlSession openSession() {return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}

留神这个 getDefaultExecutorType, 这个事实是 MyBatis 分层中核心层的 SQL 执行器,咱们接着往下看 openSessionFromDataSource:

//  level 隔离级别, autoCommit 是否主动提交
//  ExecutorType 是一个枚举值: SIMPLE、REUSE、BATCH
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);
      // 返回一个执行器, 咱们看下 newExecutor 这个办法
      final Executor executor = configuration.newExecutor(tx, execType);
      // 最初结构进去 SqlSession  
      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();}
  }
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);
  }
  // 下面是依据 executorType 生成对应的执行器
  // 如果开启缓存,则将其执行器包装为另一种模式的执行器
  if (cacheEnabled) {executor = new CachingExecutor(executor);
  }
  // interceptorChain 是一个拦截器链
  // 将该执行器退出到拦截器链中加强, 这事实上是 MyBatis 的插件开发。// 也是装璜器模式的利用, 前面会讲。executor = (Executor) interceptorChain.pluginAll(executor);
  return executor;
}

执行增删改查

接着咱们来看咱们的接口中的办法是如何执行的,

StudentMapper 执行 selectStudent 办法事实上进入的应该是对应代理的对象, 咱们进入下一步, 事实上是进入了 invoke 办法,这个 invoke 办法事实上重写的 InvocationHandler 的办法,InvocationHandler 是 JDK 提供的动静代理接口,调用被代理的的办法,事实上是会走到这个 invoke 办法中,实现如下:

  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {if (Object.class.equals(method.getDeclaringClass())) {return method.invoke(this, args);
      } else {
         // 该办法会缓存该办法, 如果该在缓存外面有,则无需再次产生, 外面的 methodCache 是 ConcurrentHashMap
         // 最终会返回 MapperMethod 对象调用 invoke 办法。// 我这里最终的 MethodInvoker 是 PlainMethodInvoker
        return cachedInvoker(method).invoke(proxy, method, args, sqlSession); 
      }
    } catch (Throwable t) {throw ExceptionUtil.unwrapThrowable(t);
    }
  }

最终的 invoke 办法如下图所示:

@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
  // 这个 execute 太长, 外面依据标签类型, 来做下一步的操纵, 这里咱们放截图 
  return mapperMethod.execute(sqlSession, args);
}

咱们接着来跟 executeForMany 这个办法的执行:

private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
  List<E> result;
  Object param = method.convertArgsToSqlCommandParam(args);
   // 默认的分页
    if (method.hasRowBounds()) {RowBounds rowBounds = method.extractRowBounds(args);
    result = sqlSession.selectList(command.getName(), param, rowBounds);
  } else {
    // 会走 DefaultSqlSession 的 selectList 上面
    result = sqlSession.selectList(command.getName(), param);
  }
  // issue #510 Collections & arrays support
  // 转换后果  
  if (!method.getReturnType().isAssignableFrom(result.getClass())) {if (method.getReturnType().isArray()) {return convertToArray(result);
    } else {return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
    }
  }
  return result;
}
  public <E> List<E> selectList(String statement, Object parameter) {return this.selectList(statement, parameter, RowBounds.DEFAULT);
  }
 // 
@Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      // 这个 statement 是办法援用:org.example.mybatis.StudentMapper.selectStudent
      // 通过这个 key 就能够从 configuration 获取构建的 MappedStatement
      MappedStatement ms = configuration.getMappedStatement(statement);
      // query 外面会判断后果是否在缓存里, 咱们没有引入缓存
      // 最终会走的 query 中的 queryFromDatabase 办法。//   queryFromDatabase 外面会调用 doQuery 办法
      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();}
  }

咱们这里重点来看 doQuery 办法:

 @Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    // 这里咱们其实曾经能够看到 MyBatis 曾经筹备在调用 JDBC 了
    // Statement 就位于 JDBC 中
    Statement stmt = null;
    try {Configuration configuration = ms.getConfiguration();
      // 依据参数解决标签中的 SQL
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      // 产生执行 SQL 的 Statement
      stmt = prepareStatement(handler, ms.getStatementLog());
      // 接着调 query 办法. 最终会走到 PreparedStatementHandler 的 query 办法上  
      return handler.query(stmt, resultHandler);
    } finally {closeStatement(stmt);
    }
  }
 // 最终执行 SQL
 public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    return resultSetHandler.handleResultSets(ps);
  }

PreparedStatement 是 JDBC,到当初就曾经开始调用 JDBC 执行 SQL 了。resultSetHandler 是对 JDBC 后果进行解决的处理器。

这里咱们把下面遇到的 Handler 大抵梳理一下:

  • StatementHandler: 语句处理器
  • ResultSetHandler:后果处理器,有后果处理器就有参数处理器
  • ParameterHandler: 参数处理器,

总结一下

在 MyBatis 中咱们写在 xml 文件中的 SQL 语句到底是怎么被执行这个问题到当初曾经有了答案:

  • xml 中的查问语句、属性会被事后的加载进入 Configuration 中,Configuration 中有 MappedStatements,这是一个 Map,key 是标签的 id。
  • 咱们在执行对应的 Mapper 的时候,首先要执行获取 Session,在这个过程中会通过 MyBatis 的拦截器,咱们能够抉择在这个过程对 MyBatis 进行加强
  • 调用接口对应的办法时, 事实上调用的时代理类的办法,代理类会先进行参数解决,依据办法签名获取 MappedStatement,再转换,交给 JDBC 来解决。

到当初咱们曾经对 MyBatis 曾经有的执行流程曾经有一个大抵的理解了,可能一些办法没有看太细,因为讲那些细节也对宏观执行流程没有太大的帮忙。

参考资料

  • MyBatis 视频教程(高级篇)视频 颜群 https://www.bilibili.com/vide…
  • 玩转 MyBatis:深度解析与定制 https://juejin.cn/book/694491…
退出移动版