MyBatis源码解析


MyBatis源码解析

如何编译源码

仓库地址

1、idea中打开项目(推荐在导入idea之前,在命令行先将项目编译通过,命令mvn clean compile)

2、等待解析依赖

备注:解析依赖的过程中有可能某些jar包的版本找不到,可自行在https://mvnrepository.com/ 找寻替代版本

运行流程

XML解析

mybatis3支持注解方式声明SQL语句,但是最终经过mybatis内部解析,作用和使用XML定义是一样的。

只要你理解了XML的整个解析流程,其实就已经吃透了mybatis。

在阅读源码的过程中,mybatis的中文官网必须时刻去翻阅。特别是如下几章内容:

  • 入门
  • XML配置
  • XML映射器
  • 动态SQL

XML配置讲解的是mybatis-config.xml (配置数据库地址、插件、类型别名)

XML映射器讲解的是我们自定义业务SQL语句的***Mapper.xml

如何开始阅读源码呢?当然不可能一个一个包一个一个类去翻阅,这样是没有意义的。我们必须找到一个入口,也就是写一个最简单的例子跑完整个流程。然后使用idea跟踪这个流程的每一步。

可以参照入门搭建一个最小的示例。

入口示例

我参照入门构造了一个示例,目录结构如下:

mybatis-config.xml中,配置了数据库地址、以及业务mapper xml文件 (projectInfo.xml)

Mybatis-config.xml

<?xml version="1.0" encoding="UTF-8" ?>


  
    
  

  
    
      
      
        
        
        
        
      
    
  

  
    
  


projectInfo.xml

<?xml version="1.0" encoding="UTF-8"?>




    
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
    

    
        p.id, p.project_name,p.url,p.description,p.tags,
        p.username,p.`password`,p.thumbnail_url,p.second_category_id,
        p.project_sort,p.maintainer,p.`status`,p.back_service,p.create_time
    

    

    
    
        
        
        
        
        
        
        
        
        
        
        
        
        
    

    
        p.project_name,p.url,p.description,p.tags,
        p.username,p.`password`,p.thumbnail_url,
        pc.name as category,pc.sort as category_sort,
        sc.name as second_category,sc.sort as second_category_sort,
        p.project_sort,p.maintainer,p.create_time
    

    


mybatis解析xml文件的入口是 mybatis-config.xml

依次解析properties->settings->typeAliases->plugins->objectFactory->objectWrapperFactory->reflectorFactory->environments->databaseIdProvider->typeHandlers->mappers

最后的mapperElement(root.evalNode("mappers")); 解析的就是我们的业务mapper:


  

我们先来看mybatis-config.xml的解析

mybatis-config.xml

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

我们的入口程序中指定了mybatis-config.xml的路径,传递给了SqlSessionFactoryBuilderbuild方法。

XMLConfigBuilder1

我们进入看看:

注意XMLConfigBuilder对象,它负责解析mybatis-config.xml中除了mappers标签以外的元素。

我们进入XMLConfigBuilder的构造方法中看看,同时也要注意一下XMLConfigBuilder继承了一个BaseBuilder类:

public class XMLConfigBuilder extends BaseBuilder {

  public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
    //XMLMapperEntityResolver实现了org.xml.EntityResolver接口
    this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
  }
}

注意:构造方法中新建了一个new XPathParser对象,我们不深入这个类,它的作用是可以根据标签方便的获取xml内容。例如parser.evalNode("/configuration")就能获取到configuration中的内容。

<?xml version="1.0" encoding="UTF-8" ?>


  
    
  
  
  
  
    
  


刚刚看了XMLConfigBuilder的构造方法,我们继续:

仍然还是调用构造方法,注意在构造方法中调用了父类的构造方法,传进了一个新的Configuration对象。

特别说明,Configuration类在mybatis中可是非常非常重要,几乎无处不在。它保存着mybatis-config.xml中的配置信息以及我们业务mapper中的类信息。真的超级重要

Configuration1

既然这么重要,我们看看new Configuration的时候做了些什么操作吧。

/**
 * 初始化了内置别名、LanguageDriver
 */
public Configuration() {
  typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
  typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);

  typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
  typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
  typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
  ...
    省略了其他
}

因为configuration的内容真的太多了,它有很多成员变量,这些成员变量都是new出来的,所以新建一个configuration对象是个耗时的工作。

TypeAliasRegistry

我们先看看构造器中用到的这个typeAliasRegistry对象吧,它在成员变量中的定义如下:

protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();

我们再来看看它:

public class TypeAliasRegistry {

  /**
   * 例子:
   */
  private final Map> typeAliases = new HashMap<>();

  public TypeAliasRegistry() {
    registerAlias("string", String.class);

    registerAlias("byte", Byte.class);
    registerAlias("long", Long.class);
    registerAlias("short", Short.class);
    registerAlias("int", Integer.class);
    ...
      省略
  }
}

TypeAliasRegistry从它的名称就能知道它的作用:别名注册器,啥意思呢?

比如我在mybaits-config中定义了一个别名 ,最终就会调用TypeAliasRegistryregisterAlias方法

registerAlias('Author',Author.class)

如何使用呢?遇到的时候再告诉你吧。

注意一下:在TypeAliasRegistry构造器中我们初始化了基本类型的别名


Configuration2

现在我们回到Configuration1一节的最后,继续。

在Configuration的构造器中我们初始化了

typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);

typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);

再来看mybatis-config.xml中的环境变量:


  
    
    

    
  

这里提一下,后面我们在解析environments标签的时候会用到我们的别名JDBC、POOLED


现在我们回到XMLConfigBuilder1中的构造器:

private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
  //注意:在此new Configuration实例
  super(new Configuration());
  ErrorContext.instance().resource("SQL Mapper Configuration");
  this.configuration.setVariables(props);
  this.parsed = false;
  this.environment = environment;
  this.parser = parser;
}

构造器后续的几条语句就没什么好说的了。

我们又回到了XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);

紧接着调用XMLConfigBuilder的parse方法,正式开始解析mybatis-config.xml

XMLConfigBuilder.parse

因为我们在mybatis-config.xml中只定义了plugins--environments--mappers这几个节点,所以我们就只看这几个节点的解析。(其中settings标签设置了很多默认值,比如下划线转驼峰,可以去看看,这里不讲解)

解析plugins

plugin在mybatis中可是非常出名的,像我们最经常使用的分页插件,入口就在这。

因为这一节只是说xml的解析,所以不能说得太详细。

先贴一下xml中的配置:


  

private void pluginElement(XNode parent) throws Exception {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      //获取plugin节点的interceptor属性
      String interceptor = child.getStringAttribute("interceptor");

      Properties properties = child.getChildrenAsProperties();
      //反射new
      Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
      interceptorInstance.setProperties(properties);
      //添加到过滤器链interceptorChain中
      configuration.addInterceptor(interceptorInstance);
    }
  }
}
  • 获取plugin节点的interceptor属性
  • 通过反射生成我们自定义的拦截器(比如分页插件)
  • 最后添加到过滤器链中

过滤器链设计得非常巧妙,很精彩。以后再给你们分享

解析environments

环境变量主要是设置数据库连接地址、用户名、密码等

再贴xml配置:


    
      
      
        
        
        
        
      
    
  

配合着代码看:

我们来看dataSourceElement这个方法,正好把上面提到的TypeAliasRegistry用上:

private DataSourceFactory dataSourceElement(XNode context) throws Exception {
    if (context != null) {
      //这里的type=POOLED
      String type = context.getStringAttribute("type");
      //这里存储用户名、密码、数据库地址
      Properties props = context.getChildrenAsProperties();
      //如果type=POOLED => PooledDataSourceFactory
      //别名注册地:typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
      DataSourceFactory factory = (DataSourceFactory) resolveClass(type).getDeclaredConstructor().newInstance();
      factory.setProperties(props);
      return factory;
    }
    throw new BuilderException("Environment declaration requires a DataSourceFactory.");
  }

请看代码里的注释。

其中resolveClass(type)在这里是resolveClass("POOLED") ,跟踪一下代码就很容易发现,resolveClass(type)最终调用的是typeAliasRegistry.resolveAlias(alias);

通过resolveClass(type)拿到Class类型后,就能通过反射生成PooledDataSourceFactory实例了。

接下来就是最最复杂的mappers解析了,我们的select、update、delete都定义在此。


projectInfoMapper.xml

//映射器--这里面非常复杂--转交给XMLMapperBuilder实现
mapperElement(root.evalNode("mappers"));

配合着xml,来看看mapperElement方法吧:


  

由于我的演示示例中没有定义package标签,代码不会进入。

此次我会进入红色箭头指示的地方,因为我只定义了resource属性

来看硬核的地方:XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());

XMLMapperBuilder

进入构造方法看看,同时注意它也继承了父类BaseBuilder(之前已经提过了)

同样也new了个XPathParser解析器,跳过。看上图我红框标记的地方:

this.builderAssistant = new MapperBuilderAssistant(configuration, resource);

这也是个非常重要的类,看名字就知道它是个助手类,负责组装SQL用的。很重要很重要

参数:

  • configuration: 之前我们在XMLConfigBuilder构造器中new出来的,还有印象吗
  • resource: mapper文件路径,这里是com/twx/mybatis/mapper/xml/projectInfo.xml

我们来看看它吧。

MapperBuilderAssistant

public class MapperBuilderAssistant extends BaseBuilder {
  /**
   * xml文件中的命名空间 
* 例如: */ private String currentNamespace; /** * mapper xml 文件的全路径 */ private final String resource; private Cache currentCache; private boolean unresolvedCacheRef; // issue #676 public MapperBuilderAssistant(Configuration configuration, String resource) { super(configuration); ErrorContext.instance().resource(resource); this.resource = resource; } }

它也继承了BaseBuilder,所以它也有一个Configuration对象的引用(还记得我说过的,Configuration对象很重要,后面非常多类都要用到它。

注意它的几个成员变量:

  • currentNamespace:业务xml文件中的命名空间
  • resource:业务xml 文件的全路径,设置在mybatis-config.xml mapper标签的resource属性中
  • currentCache:一级缓存,暂时不讲解

它还有很多重要的方法,这里先列举几个常用的:

  • applyCurrentNamespace-返回mapper接口方法的完全限定名称
  • useNewCache--创建缓存对象
  • addResultMap--生成ResultMap,add到configure对象的resultMaps中
  • addMappedStatement--生成MappedStatement对象,set到config对象的mappedStatements中
  • getStatementResultMaps--根据resultMap参数从config对象中获取ResultMap对象
  • buildResultMapping--构建ResultMapping对象
  • resolveResultJavaType-返回java bean属性对应的Java类型

XMLMapperBuilder解析

parse

从XMLMapperBuilder构造器出来,紧接着就调用parse方法

XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();

String namespace = context.getStringAttribute("namespace");拿到的是

我们再来看看更具体的configurationElement(XNode context)方法

接下来会着重讲解三个方法:

  • resultMapElements
  • sqlElement
  • buildStatementFromContext

这三个方法对应下图xml文件中的三个标签

resultMapElements1

先来看如何解析resultMap标签的。

真正解析的方法很长。我们分段看。


省略

//type = com.twx.mybatis.entity.ProjectInfo
String type = resultMapNode.getStringAttribute("type",
    resultMapNode.getStringAttribute("ofType",
        resultMapNode.getStringAttribute("resultType",
            resultMapNode.getStringAttribute("javaType"))));

由于只传了一个type属性,所以String type=com.twx.mybatis.entity.ProjectInfo

Class<?> typeClass = resolveClass(type);
if (typeClass == null) {
  typeClass = inheritEnclosingType(resultMapNode, enclosingType);
}

typeClass理所应当是com.twx.mybatis.entity.ProjectInfo


再来看上面这段代码,先解释一下ResultMapping.

ResultMapping

一个ResultMapping对应entity中的一个属性,亦即

上图xml中的每一行最后都被解析成一个一个的ResultMapping 对象。

理解了这点,我们回到java代码中的for循环。for循环就是解析这些id,result。由于我们没有定义constructor,discriminator,就跳过这几行代码。直接看idresult标签的解析。

我们来看这行代码:

//解析 标签
resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));

来看buildResultMappingFromContext方法,buildResultMappingFromContext 用于生成一个ResultMapping对象。

上图这段java代码对照着下图看就很清晰了,不做过多解析。(注意,我们没对resultMap的每个属性声明typeHandler)

代码最后一行我特别标注,最终ResultMapping对象的生成是转交给builderAssistant助手实现的(前面我们提到过,还有印象吗,当时只是提到了方法名称,并没有具体去分析,现在我们去看看)。


我们来看builderAssistantbuildResultMapping方法:

public ResultMapping buildResultMapping(Class<?> resultType, String property, String column, Class<?> javaType, JdbcType jdbcType,
      String nestedSelect, String nestedResultMap, String notNullColumn, String columnPrefix,
      Class<? extends TypeHandler<?>> typeHandler, List flags,
      String resultSet, String foreignColumn, boolean lazy) {
    //根据属性property名称例如username获取对应的java类型String
    Class<?> javaTypeClass = resolveResultJavaType(resultType, property, javaType);
    //typeHandler在我们的示例中是null,返回null
    TypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler);

    //是否有组合对象
    List composites;
    //假如
    // 有配置select属性,那么这里的nestedSelect!=null
    //有配置foreignColumn属性,这里的foreignColumn!=null
    //在我们的示例中,显然两者都为空,所以composites=null
    if ((nestedSelect == null || nestedSelect.isEmpty()) && (foreignColumn == null || foreignColumn.isEmpty())) {
      composites = Collections.emptyList();
    } else {
      composites = parseCompositeColumnName(column);
    }

    return new ResultMapping.Builder(configuration, property, column, javaTypeClass)
        .jdbcType(jdbcType)
        .nestedQueryId(applyCurrentNamespace(nestedSelect, true))
        .nestedResultMapId(applyCurrentNamespace(nestedResultMap, true))
        .resultSet(resultSet)
        .typeHandler(typeHandlerInstance)
        .flags(flags == null ? new ArrayList<>() : flags)
        .composites(composites)
        .notNullColumns(parseMultipleColumnNames(notNullColumn))
        .columnPrefix(columnPrefix)
        .foreignColumn(foreignColumn)
        .lazy(lazy)
        .build();
  }

这个方法的作用是为了:

  • 确定java bean属性对应的Class类型
  • 确定属性交给什么TypeHandler处理
  • 以及属性是否有嵌套查询

最后的return语句其实就是new ResultMapping(),只不过用的是建造者模式而已。


resultMapElements2

我们回到resultMapElements1一节的最后。

//每一个ResultMapping对应entity中的一个属性
 List resultMappings = new ArrayList<>(additionalResultMappings);
for (XNode resultChild : resultChildren) {
  ....
    省略
   //解析 标签
		resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags)); 
}

经过我们上面对ResultMapping的分析,执行完上面这个for循环后,我们继续。

再次贴上xml:

 
   ...
   省略了
  

看代码注释就好。

最后调用ResultMapResolver.resolve()生成ResultMap对象。

ResultMapResolver.resolve() 最终还是调用我一再强调的助手类MapperBuilderAssistant的addResultMap

public ResultMap resolve() {
  return assistant.addResultMap(this.id, this.type, this.extend, this.discriminator, this.resultMappings, this.autoMapping);
}

我们来看MapperBuilderAssistant.addResultMap()

/**
 * 生成ResultMap,add到configure对象的resultMaps中
 * @param id  resultMapId
 * @param type  mapper返回类型
 * @param extend
 * @param discriminator
 * @param resultMappings
 * @param autoMapping
 * @return
 */
public ResultMap addResultMap(String id, Class<?> type, String extend,
    Discriminator discriminator, List resultMappings, Boolean autoMapping) {

  //resultMap的完全限定名称
  //例如:com.twx.mybatis.mapper.ProjectInfoMapper.projectInfoMap
  id = applyCurrentNamespace(id, false);
  
  //TODO: 构造示例研究
  extend = applyCurrentNamespace(extend, true);
  if (extend != null) {
    ...
      省略
  }
  
  //build方法内部做了很多操作,主要是提取ResultMapping中的属性
  //ResultMapping存储了很多@Result的属性
  ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping)
      .discriminator(discriminator)
      .build();
  configuration.addResultMap(resultMap);
  return resultMap;
}

不需要关心太多,直接看最后new ResultMap.Builder().build()

这个方法不详细讲了,大概的内容就是遍历之前生成的List resultMappings 给自己的成员变量赋值。下面列举几个关键的成员变量。

/**
 * resultMap的id
 */
private String id;
/**
 * 返回类型,例如com.twx.mybatis.entity.ProjectInfo
 */
private Class<?> type;
private List resultMappings;
/**
 * id标签对应的ResultMapping
 */
private List idResultMappings;
/**
 * 构造器对应的constructorResultMappings
 */
private List constructorResultMappings;
/**
 * id+result标签对应的ResultMapping
 */
private List propertyResultMappings;

至此,终于完成了 标签的解析了。


sqlElement

这是我们的第二大标签。我们先看看xml中的定义与使用。你一定很熟悉。


  p.id, p.project_name,p.url,p.description,p.tags,
  p.username,p.`password`,p.thumbnail_url,p.second_category_id,
  p.project_sort,p.maintainer,p.`status`,p.back_service,p.create_time


  

代码实现很简单,只是把 标签的id和内容存储到sqlFragments

sqlFragments是Configuration中的sqlFragments引用

sqlFragments的定义:

/**
 * 存储标签语句,key指代id
 */
protected final Map sqlFragments = new StrictMap<>("XML fragments parsed from previous mappers");

注意 sqlFragments.put(id, context); 中的context是一个Xnode节点,后续会拿出来解析的。这里不做处理


buildStatementFromContext

最重磅的一个方法了。非常复杂!

做好准备。这是xml解析最后一步了。

那来看buildStatementFromContext

由于解析select delete insert 等语句比较复杂,转交给单独的类XMLStatementBuilder解析

parseStatementNode()

同样继承了BaseBuilder,构造方法没什么好讲的了。之前都说过了。

注意一下context代表的是xml node节点内容。例如:



我们直接来看parseStatementNode(),这方法也是超级的长。慢慢分析。

String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");

id在此是getProjectInfo,databaseId=null


String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));

nodeName是select insert update delete其中一种,在此是select

sqlCommandType=SqlCommandType.SELECT


boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
//将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:false。
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
//将其设置为 true 后,将会导致本条语句的结果被二级缓存缓存起来,默认值:对 select 元素为 true。
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
//这个设置仅针对嵌套结果 select 语句:如果为 true,将会假设包含了嵌套结果集或是分组,当返回一个主结果行时,就不会产生对前面结果集的引用。
//这就使得在获取嵌套结果集的时候不至于内存不够用。默认值:false。
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

看代码中的注释。针对select查询语句,useCache默认是true


//解析include语句
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());

上面两行用于解析标签,例如示例中的:

select

from project_info p

上面替换标签的方法使用到了递归,最核心的是第一个红色框里的代码,找到include对应的标签,然后替换内容。因为递归的原因,不太好通过文字讲解。(可以自己调试代码感受)


我们继续回到parseStatementNode()

String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);

String lang = context.getStringAttribute("lang");
//如果未指定,默认是XMLLanguageDriver
//初始化new Configuration时
//languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
LanguageDriver langDriver = getLanguageDriver(lang);

parameterType我们没传,所以parameterType=null

LanguageDriver langDriver 默认是XMLLanguageDriver,看注释


//
//    select CAST(RANDOM()*1000000 as INTEGER) a from SYSIBM.SYSDUMMY1
//  
processSelectKeyNodes(id, parameterTypeClass, langDriver);

用得不多,跳过


// Parse the SQL (pre:  and  were parsed and removed)
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
  keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
  //(仅适用于 insert 和 update)这会令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键
  // (比如:像 MySQL 和 SQL Server 这样的关系型数据库管理系统的自动递增字段),默认值:false。
  keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
      configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
      ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}

过程就不讲了。不重要。

keyStatementId="com.twx.mybatis.mapper.ProjectInfoMapper.getProjectInfo!selectKey"

KeyGenerator keyGenerator = NoKeyGenerator


//如果是动态的,返回DynamicSqlSource
//如果是静态的,返回RawSqlSource
//SqlSource接口有一个方法BoundSql getBoundSql(Object parameterObject);
//能拿到BoundSql
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

这里面也是非常复杂的。如果你的select语句中有if | foreach | trim| when (...) 或者 ${} 那就是动态的,否则是静态的。

如果是动态的,返回DynamicSqlSource

如果是静态的,返回RawSqlSource

我们来看看XMLLanguageDrivercreateSqlSource

@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
  XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
  return builder.parseScriptNode();
}

内部又借助XMLScriptBuilderparseScriptNode()实现。

那我们转战战场。

XMLScriptBuilder

  • 同样继承了BaseBuilder
  • 构造方法中调用了initNodeHandlerMap()
  • initNodeHandlerMap内部有些TrimHandler之类的东西,这些是内部类

我们来看parseScriptNode()方法:

public SqlSource parseScriptNode() {
  //解析标签中的动态变量。

返回的MixedSqlNode维护着一个列表List contents,contents可能包含 StaticTextSqlNodeIfSqlNode或者WhereSqlNode或者其他。

我们粗略地看看parseDynamicTags(context)就行,内部的textSqlNode.isDynamic()非常复杂的。主要是数据处理。


解析完parseDynamicTags,必然是动态的(我们的示例),所以返回DynamicSqlSource


parseStatementNode()2

那我们继续回到parseStatementNode()的这个地方:

SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

继续往下看。

如果未指定,默认是StatementType.PREPARED

这非常重要,在真正取数据的时候,会根据这个枚举类型new不同的statementHandler对象

详情参照RoutingStatementHandler构造器

switch (ms.getStatementType()) {
  case STATEMENT:
  delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
  break;
  case PREPARED:
  delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
  break;
  case CALLABLE:
  delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
  break;
  default:
  throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
  }

在此我们只要知道StatementType statementType=StatementType.PREPARED,用到的时候再细聊。


接下来的这些代码没什么好说的,都是从xml配置取值。


parseStatementNode()方法的最后就是生产MappedStatement对象。

一个MappedStatement对象就代表着你定义的一个语句的id,即getProjectInfo 生成了MappedStatement后,xml解析工作基本已经完结了,剩下的都是收尾工作。 顺着堆栈一步步弹出。我们回到了下图这里,即XMLMapperBuilder.parse(): 所有xml文件都已经解析完毕了。 然后标记一下我们已经解析过的xml文件,避免重复加载,毕竟很耗时。 configuration.addLoadedResource(resource); 然后就是这行代码bindMapperForNamespace(); 它的作用是将我们声明的***Mapper.java接口添加到Configuration对象的mapperRegistry成员变量中。 我们示例声明的接口是ProjectInfoMapper: public interface ProjectInfoMapper { List getProjectFront(); List getProjectInfo(@Param("fId") Integer fId, @Param("sId") Integer sID); } 调用bindMapperForNamespace 后,相当于 protected final MapperRegistry mapperRegistry = new MapperRegistry(this); mapperRegistry.addMapper(ProjectInfoMapper.class); parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements(); 这几个方法就不说了。 至此,xml解析流程全部完毕。 接下来,我们看看mybatis是如何查询的。 如何查询 我们回到一开始的测试代码 SqlSession 我们已经完成了XML文件的解析工作了。现在我们需要打开一个SqlSession: SqlSession session = sqlSessionFactory.openSession() 跟踪代码来到DefaultSqlSessionFactory.openSessionFromDataSource(): 最重要的就是这一行代码:final Executor executor = configuration.newExecutor(tx, execType); 先说一下,Executor是个很重要的东西,后面是用来真正发起查询的入口。 我们进入configuration.newExecutor(tx, execType);看看: 看看上图我标记出的几点就好。后面再精讲。 mapper动态代理 接下来就精彩了。这一句ProjectInfoMapper mapper = session.getMapper(ProjectInfoMapper.class); 总所周知,我们定义的ProjectInfoMapper是一个接口,但是接口是必须有实现类,才能真正发挥作用的。 所以,我们猜测session.getMapper(ProjectInfoMapper.class)返回的肯定是ProjectInfoMapper的一个实现类。先通过idea看看这个mapper究竟是什么吧? mapper = org.apache.ibatis.binding.MapperProxy@6f96c77 为什么是一个MapperProxy对象?这个MapperProxy会是ProjectInfoMapper的实现类吗? session.getMapper的调用过程如上图所示: DefaultSqlSession#getMapper Configuration#getMapper MapperRegistry#getMapper 我们来到最后,分析MapperRegistry#getMapper。 public T getMapper(Class type, SqlSession sqlSession) { final MapperProxyFactory mapperProxyFactory = (MapperProxyFactory) 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); } } knownMappers.get(type); get是取出的意思,那肯定有个地方放进去。那是在哪放进去的呢? 前面我已经说过了,在xml解析最后一步。 当时我们没仔细分析mapperRegistry.addMapper(Class type) 现在我们来看看。 可以看到,knownMappersput的是一个MapperProxyFactory(mapper代理工厂) 所以final MapperProxyFactory mapperProxyFactory = (MapperProxyFactory) knownMappers.get(type);拿到的是一个MapperProxyFactory 对象。(这里的T针对我们示例是ProjectInfo) 接下来执行这句语句: return mapperProxyFactory.newInstance(sqlSession); 进去看看。 从标记2就能看出,这里使用的是JDK动态代理技术。 那标记1中的MapperProxy必然是一个实现了InvocationHandler接口的类。(不要问为什么,就得这样) 其实解释到这就能解释为什么 ProjectInfoMapper mapper = org.apache.ibatis.binding.MapperProxy@6f96c77 会是一个MapperProxy实例了。 熟悉JDK动态代理的同学肯定知道,当执行ProjectInfoMapper mapper中的任意方法时,都会转交到MapperProxy的invoke()方法中执行。 MapperProxy的内部代码我们稍候再看,不对,很快就要去分析了。 我们先回到ProjectInfoTest。 我们刚刚分析了上图红框标记的代码,知道了ProjectInfoMapper mapper其实是个MapperProxy对象。 现在我们要调用ProjectInfoMapper 的getProjectInfo方法了,依照我刚刚分析的。程序必须会进入MapperProxy的invoke方法。 我们debug走起!在List projects = mapper.getProjectInfo(2, 1); 处打个断点,然后通过Step Into进入,就到了下图这个地方: 我们继续调试,程序进入else分支,我稍微改了下源码风格,变成下面这样了: MapperMethodInvoker methodInvoker = cachedInvoker(method); return methodInvoker.invoke(proxy, method, args, sqlSession); 接下来用图片的方式演示调用过程: ![](https://soyuan-yuque.oss-cn-shanghai.aliyuncs.com/picgo/截屏2020-06-07 14.49.31.png) ![](https://soyuan-yuque.oss-cn-shanghai.aliyuncs.com/picgo/截屏2020-06-07 14.51.03.png) ![](https://soyuan-yuque.oss-cn-shanghai.aliyuncs.com/picgo/截屏2020-06-07 14.52.01.png) ![](https://soyuan-yuque.oss-cn-shanghai.aliyuncs.com/picgo/截屏2020-06-07 14.52.44.png) 上面4张图中最重要的是MapperMethod,运行流程最终会进入到MapperMethod的execute方法。 execute方法又会根据查询、插入、删除、更新等操作导向到不同的方法中处理,如果是select方法,又会根据方法的返回类型进一步导向。由于List getProjectInfo(@Param("fId") Integer fId, @Param("sId") Integer sID);返回的是List,所以进入了executeForMany 再来看executeForMany 转向会DefaultSqlSession.selectList处理 还记得MappedStatement是什么时候生成的吗?在XML解析最后一步,生成MappedStatement后我们把它放进了Configuration中,这里要取出来用了。 executor 还记得是什么时候生成的吗?在构建SqlSession时,在DefaultSqlSessionFactory.openSessionFromDataSource()中,忘记了回去看看。 但是当时我们没有很细致的分析executor,现在是时候了。 还记得在我们创建executor时,我提到过这个executor被装饰器包装了好几次吗? 现在要使用它了?你觉得调用顺序会是怎么样的呢? 回顾一下,创建时的顺序: 调用时的顺序正好相反。一层层脱壳。 这里调用executor.query的顺序是,拦截器-->CachingExecutor.query-->SimpleExecutor.query 由于mybatis 插件(拦截器)的设计还挺优雅的,内部又是运用动态代理技术,如果在这讲解,会把你们绕进去,下次单独讲。所以,现在我们把mybatis-config.xml的插件注释掉 现在我们来调试。 第二张图也就是CachingExecutor.query中的BoundSql boundSql = ms.getBoundSql(parameterObject);用于绑定动态参数。 内部从MappedStatement中取出SqlSource(DynamicSqlSource),然后结合parameterObject进行赋值。 说明:为什么最后一步调用的是BaseExecutor.query而不是SimpleExecutor.query,因为SimpleExecutor继承了BaseExecutor。 以上步骤说明了,我刚刚讲解的装饰器模式是一层层剥壳是没有问题的。 我们忽略CachingExecutor.query中的代码,因为它只是判断内存中是否已有缓存的查询结果,并不会真正执行查询。(别忘了我们的目的,是为了理解查询流程) 所以我们直接来到BaseExecutor.query中 直接来到这里list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); 进入queryFromDatabase private List queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { ... try { list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); } finally { ... } ... return list; } BaseExecutor.doQuery方法是个抽象方法,转交给子类实现。所以来到了SimpleExecutor#doQuery @Override public List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { Statement stmt = null; try { Configuration configuration = ms.getConfiguration(); //一般返回的是PreparedStatementHandler StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); stmt = prepareStatement(handler, ms.getStatementLog()); return handler.query(stmt, resultHandler); } finally { closeStatement(stmt); } } 我们先来看StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); newStatementHandler RoutingStatementHandler的作用是根据statement的类型(STATEMENT、PREPARED、CALLABLE)返回不同的StatementHandler。(还记得statement的类型是在哪设置的吗?在构建MappedStatement的最后阶段,看parseStatementNode()2这一节)。默认就是PREPARED。 我们进RoutingStatementHandler看看: public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { switch (ms.getStatementType()) { case STATEMENT: delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); break; case PREPARED: delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); break; case CALLABLE: delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); break; default: throw new ExecutorException("Unknown statement type: " + ms.getStatementType()); } } 由于默认是PREPARED。所以代码执行会进入delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); public class PreparedStatementHandler extends BaseStatementHandler { public PreparedStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { super(executor, mappedStatement, parameter, rowBounds, resultHandler, boundSql); } 注意PreparedStatementHandler继承了父类BaseStatementHandler,在构造方法中调用了父类的构造方法。 好,我们回到StatementHandler statementHandler = new RoutingStatementHandler(....),现在我们知道statementHandler返回的是一个RoutingStatementHandler对象,但是RoutingStatementHandler对象内容又维持着一个StatementHandler statementHandler引用,这个引用可能是(PreparedStatementHandler、SimpleStatementHandler等等)的实例。 所以调用RoutingStatementHandler.query()实际上调用的是PreparedStatementHandler.query(),后面我们会讲到。(想想,这有点像哪种设计模式?) prepareStatement 紧接着我们开始下一句stmt = prepareStatement(handler, ms.getStatementLog()); 首先,参数handler,是PreparedStatementHandler,上面我们已经分析过了。 变量stmt,是jdbc的Statement(很重要,注意区别) 然后是getConnection()方法,内部细节我们就不进去看了。大概意思就是: mybatis自己建立了一个数据库连接池,还记得我们在mybatis-config.xml配置的环境变量中指定了dataSource type="POOLED".我们这里取连接就是从POOL中取的.(在openSession 新建executor的时候,就已经实例化出事务和datasource了) 紧接着是stmt = handler.prepare(connection, transaction.getTimeout()); 刚刚说过了RoutingStatementHandler 使用的是代理模式,所以调用prepare实际调用的是PreparedStatementHandler的prepare(),PreparedStatementHandler继承了BaseStatementHandler,所以实际调用的是BaseStatementHandler.prepare 上图是BaseStatementHandler.prepare的实现。注意最重要的一句,statement = instantiateStatement(connection); instantiateStatement是一个抽象方法,叫给子类实现,也就是PreparedStatementHandler.instantiateStatement 上图是PreparedStatementHandler.instantiateStatement的实现,红框里的代码是不是非常眼熟。这里就是最底层的jdbc获取Statement的地方。 我们debug看看String sql = boundSql.getSql();的sql是什么? sql结果 select p.id, p.project_name,p.url,p.description,p.tags, p.username,p.`password`,p.thumbnail_url,p.second_category_id, p.project_sort,p.maintainer,p.`status`,p.back_service,p.create_time from project_info p LEFT JOIN second_category sc on sc.id = p.second_category_id INNER JOIN primary_category pc on pc.id = sc.p_id where p.logic_delete = 0 and pc.id = 2 and sc.id = 1 ORDER BY pc.sort,sc.sort,p.project_sort stmt = handler.prepare(connection, transaction.getTimeout()); 这句话结束后,紧接着是 handler.parameterize(stmt); 从字面含义来看,此方法是对参数进行处理的。由于我们的示例无法对这个方法进行测试,跳过解析。留待以后遇到再研究。 我们回到SimpleExecutor#doQuery的最后一句handler.query(stmt, resultHandler); 同样,handler还是那个RoutingStatementHandler,调用query方法实际还是PreparedStatementHandler#query。 来看看PreparedStatementHandler#query: 可以看到,查询最终是调用jdbc的PreparedStatement.execute,没什么好说的了。 红色箭头所指示的代码是对结果集进行处理的。我们放在下一大节讲解。 如何映射结果集 resultSetHandler.handleResultSets(ps) 就是对结果集进行处理的语句。 上图是对结果集进行处理的一个概览堆栈图。从图中可以看出结果集的处理都在DefaultResultSetHandler类中进行 进入DefaultResultSetHandler.handleResultSets 1、上图中的ResultSetWrapper保留着数据库查询的原始结果ResultSet 、列名、jdbc类型等信息 public ResultSetWrapper(ResultSet rs, Configuration configuration) throws SQLException { super(); this.typeHandlerRegistry = configuration.getTypeHandlerRegistry(); this.resultSet = rs; //结果集元数据 final ResultSetMetaData metaData = rs.getMetaData(); //多少列 final int columnCount = metaData.getColumnCount(); for (int i = 1; i <= columnCount; i++) { //使用列标签代替列名 columnNames.add(configuration.isUseColumnLabel() ? metaData.getColumnLabel(i) : metaData.getColumnName(i)); jdbcTypes.add(JdbcType.forCode(metaData.getColumnType(i))); classNames.add(metaData.getColumnClassName(i)); } } 2、List resultMaps = mappedStatement.getResultMaps(); 就不用多说了。在我们生成MappedStatement的时候绑定了ResultMap 3、handleResultSet(rsw, resultMap, multipleResults, null);根据resultMap中的定义对结果集进行处理(resultMap定义中映射了属性名和列名、或者还有其他的,比如typeHandler)。最终结果集会存储在multipleResults列表中 我们进入handleResultSet方法: handleResultSet 1、DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory); DefaultResultHandler用于保存最终的查询结果,它内部有个List list成员变量 2、handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);这一句是真正解析结果集的地方。结果存储于defaultResultHandler中的List list中。然后下一句multipleResults.add(defaultResultHandler.getResultList()); multipleResults取出defaultResultHandler中的List list 所以我们深入看handleRowValues方法 handleRowValues 大多数情况我们都只有一个resultMap 所以继续看handleRowValuesForSimpleResultMap handleRowValuesForSimpleResultMap 1、DefaultResultContext resultContext 起到一个中转的作用,它的成员变量T resultObject临时存储当前的rowValue 2、while循环的作用和我们使用jdbc处理resultSet一样。一行一行处理 3、resolveDiscriminatedResultMap这个方法可以不看 4、Object rowValue = getRowValue(rsw, discriminatedResultMap, null);这一行代码是整个一节最核心的地方,我们去看他 5、storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);等解析完上一条语句再来看。 getRowValue 最硬核的地方是上面标注的两个方法。 一个是生成对象,一个是给对象的属性赋值。 来看对象生成的代码 ![image-20200610083816635](/Users/twx/Library/Application Support/typora-user-images/image-20200610083816635.png) 继续 com.twx.mybatis.entity.ProjectInfo不是接口但是有默认的构造函数(当然也没有上图注释中的其他情况)所以进入箭头所指处 objectFactory是如何生成对象的呢?看下图 反射而已! 看完了对象生成,现在看属性赋值。 applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) 不是说要给对象赋值的吗?刚刚生成的对象在哪?看了一眼参数,也没有刚刚反射生成的rowValue啊 rowValue其实被包裹在metaObject中(MetaObject详情请进其构造函数查看) 我们进入applyPropertyMappings: for循环:一个一个属性解析。 如何获取某个属性(比如id、name)对应的值呢? Object value = getPropertyMappingValue(rsw.getResultSet(), metaObject, propertyMapping, lazyLoader, columnPrefix); 这个方法是关键,之前一直介绍的typeHander在此终于发挥作用了。 的每一个都有一个默认的typeHandler属性 再看最后一句,typeHandler.getResult(rs, column); 其实很简单,对应于jdbc接口的ResultSet.ge****(),随便找一个实现类,比如Int: 至此,以无法再深入。顺着堆栈往回走,之后的一些方法就不做介绍了。后面的工作基本都是做一些清理性的工作。