这个系列的文章的开篇《当咱们说起看源码时,咱们是在看什么》在去年十月份就开始了,明天开始填这个系列的坑。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.ConsoleAppenderlog4j.appender.CONSOLE.Encoding=UTF-8log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayoutlog4j.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->Configurationprivate 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这个过程中大抵做了什么:

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

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

//  level 隔离级别, autoCommit 是否主动提交//  ExecutorType 是一个枚举值: SIMPLE、REUSE、BATCHprivate 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办法如下图所示:

@Overridepublic 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...