MyBatis源码学习笔记(一) 初遇篇

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

那该如何看源码呢?

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

参阅了很多MyBatis源码的资料,MyBatis的整体架构可以分为三层:

MyBatis源码学习笔记(一) 初遇篇_第1张图片

  • 接口层: SqlSession 是我们平时与MyBatis完成交互的核心接口(包括后续整合SpringFramework用到的SqlSessionTemplte)
  • 核心层: SqlSession执行的方法,底层需要经过配置文件的解析、SQL解析,以及执行SQL时的参数映射、SQL执行、结果集映射,另外还有穿插其中的扩展插件。
  • 支持层: 核心层的功能实现,是基于底层的各个模块,共同协调完成的。

搭建MyBatis的环境

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

  • 引入Maven依赖

  org.mybatis
  mybatis
  3.5.6
 
  
            mysql
            mysql-connector-java
            5.1.47
        
        
            com.alibaba
            druid
            1.2.5
        
        
            org.slf4j
            slf4j-api
            1.7.30
        
        
            org.slf4j
            slf4j-log4j12
            1.7.30
        
  • 然后来一张表
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的配置文件



    
    
    
    
        
            
            
            
            
                
                
                
                
            
        
    
    
        

    
        
           
    
    
  • 来个Student类
public class Student {
    private Long id;
    private String name;
    private String number;
    private String money;
    // 省略get set 函数
}
  • 来个Mapper.xml




    
  • 来个接口
public interface StudentMapper {
    List 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 studentList = studentMapper.selectStudent();
        studentList.forEach(System.out::println);
    }
}

执行之后就可以在控制台看到如下输出了:

MyBatis结果输出

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

执行过程浅析

上面的执行过程大致可以分成三步:

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

解析配置文件

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

MyBatis源码学习笔记(一) 初遇篇_第2张图片

MyBatis源码学习笔记(一) 初遇篇_第3张图片

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概览

MyBatis源码学习笔记(一) 初遇篇_第4张图片

  • 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标签,我们可以通过调试验证这一点:

MyBatis源码学习笔记(一) 初遇篇_第5张图片

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 list) {
  if (configuration.getDatabaseId() != null) { // dataBaseId 用于指明该标签在哪个数据库下执行
    buildStatementFromContext(list, configuration.getDatabaseId());
  }
  buildStatementFromContext(list, null);
}

解析标签

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

MyBatis源码学习笔记(一) 初遇篇_第6张图片

到此我们基本结束看构建configuration的过程,我们可以认为在这一步,Mybatis的配置文件和Mapper.xml已经基本解析完毕。

获取SqlSession对象

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

MyBatis源码学习笔记(一) 初遇篇_第7张图片

我们在第一步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;
}

执行增删改查

接着我们来看我们的接口中的方法是如何执行的,

MyBatis源码学习笔记(一) 初遇篇_第8张图片

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);
}

MyBatis源码学习笔记(一) 初遇篇_第9张图片

我们接着来跟executeForMany这个方法的执行:

private  Object executeForMany(SqlSession sqlSession, Object[] args) {
  List 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  List selectList(String statement, Object parameter) {
    return this.selectList(statement, parameter, RowBounds.DEFAULT);
  }
 // 
@Override
  public  List 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  List 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  List 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)