带你一步一步手撕 Mybatis 源码加手绘流程图——构建部分

了解 Mybatis

MyBatis 的前身是 Apache 的开源项目 iBatis。MyBatis 消除了几乎全部的 JDBC 代码和参数的手工设置以及对结果集的检索封装,是一个支持普通 SQL 查询,存储过程和高级映射的基于 Java 的优秀持久层框架。java

MyBatis 配置

当阅读源码的时候咱们不能深陷一些细节,咱们应该先鸟瞰全貌,这样可以帮助你从高维度理解框架。node

由于这篇文章主要涉及配置文件对应的配置对象的初始化和构建,因此执行部分先不作介绍。咱们首先放一张平时咱们使用 Mybatis 的时候会编写的两个重要配置文件——mybatis-config.xmlxxxMapper.xmlsql

这里咱们默认 mybatis 的配置文件为 mybatis-config.xml 数据库

在 mybatis-config.xml 配置文件中,咱们会有一个专门的 <mappers>标签映射了相关的 mapper 映射文件。缓存

其实,Mybatis的构建流程就是:对配置文件解析成配置对象装入内存中mybatis

Mybatis是如何将配置文件解析成配置对象的

首先咱们来思考一个问题:这个配置对象何时会被使用到?app

咱们知道在 mybatis-config.xml 中配置了一些类型处理器,类型别名,mappers,数据库链接信息等等,而这些东西在每次数据库链接进行 CRUD 操做的时候都须要用到,也就是在每次 SQL会话 中咱们须要用到。框架

而在 Mybatis 中使用了一个 SqlSession 接口来表示和规范了 Sql会话,咱们须要经过专门的 SqlSessionFactory 去建立,这里面是一种工厂模式。这里我简单画一下 UML 图,你能够回顾一下 工厂模式,但这不是这篇文章的重点。ide

Mybatis使用了工厂模式还不止,在构造 SqlSessionFactory 的时候还使用了 SqlSessionFactoryBuilder 去构建 SqlSessionFactory 也就是使用了构建者模式。而又由于在建立 SqlSession 的时候咱们须要传入咱们的配置对象 Configuration,而咱们知道 mybatis-config.xml 配置文件中有许多标签,也就意味着当咱们构造一个 Configuration 对象的时候会带有不少字段的解析,那么整个 Configuration 对象的构建是很是复杂的。在 Mybatis 中使用了 构建者模式 来解决这个问题。咱们能够看一下源码。源码分析

// 这是在SqlSessionFactoryBuilder类中
// 在 SqlSessionFactoryBuilder 中会有不少build构造工厂的方法
// 其中这里是主线,由于其余build方法都会调用此方法
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
  // 经过配置文件解析成的流去建立 
  // 构建Configuration对象的 builder类 XmlConfigBuilder
  // 以后会调用parse方法构建 Configuration 对象
  XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
  // 最终会调用参数为Configuration的build方法
  // 进行最终的SqlSessionFactory的构建
  return build(parser.parse());
} catch (Exception e) {
  throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
  ErrorContext.instance().reset();
  try {
    inputStream.close();
  } catch (IOException e) {
    // Intentionally ignore. Prefer previous error.
  }
}
}
复制代码

这样咱们就能够画出一个简单的流程图了。

沿着主线走看 Mybatis 如何解析配置文件的

由上面的分析咱们能够知道:XMLConfigBuilder 类中的 parse() 方法进行了 Configuration 对象的解析和构建

咱们来沿着这条路线进去看看底层原理是什么样的。

public Configuration parse() {
// 若是已经解析过了,报错
if (parsed) {
  throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
// 根节点是configuration
// 解析还在这里
// 我须要在这里解释一下
// "/configuartion" 这个实际上是xpath语法
// mybatis封装了 xpath解析器去解析 xml
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}

//解析配置
private void parseConfiguration(XNode root) {
try {
  //分步骤解析
  //issue #117 read properties first
  //1.properties
  propertiesElement(root.evalNode("properties"));
  //2.类型别名
  typeAliasesElement(root.evalNode("typeAliases"));
  //3.插件
  pluginElement(root.evalNode("plugins"));
  //4.对象工厂
  objectFactoryElement(root.evalNode("objectFactory"));
  //5.对象包装工厂
  objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
  //6.设置
  settingsElement(root.evalNode("settings"));
  // read it after objectFactory and objectWrapperFactory issue #631
  //7.环境
  environmentsElement(root.evalNode("environments"));
  //8.databaseIdProvider
  databaseIdProviderElement(root.evalNode("databaseIdProvider"));
  //9.类型处理器
  typeHandlerElement(root.evalNode("typeHandlers"));
  //10.映射器
  mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
  throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
复制代码

看到这些有没有以为很熟悉?

其实就是 配置文件中的一些标签配置。咱们画张图来对应一下就一目了然了。

比较重要的 mappers

如上图所示,在整个 Configuration 配置对象的构建过程当中须要涉及到不少标签的解析,因此 Mybatis 巧妙地利用了 构建者模式,而这么多配置信息在这篇文章中我不能一一去进行源码分析(有不少都是细枝末节的东西,咱们只须要大概知道干什么就好了),因此我挑了最重要的 <mappers> 标签的解析去进行源码分析。

咱们再次进入源码查看,这回是 XmlConfigBuilder 中的 mapperElement(XNode parent) 方法。固然咱们最好对照着配置信息格式去看

<mappers>
  <!-- 这几种配置方式 结合下面源码看 -->
  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
  <mapper class="org.mybatis.builder.BlogMapper"/>
  <package name="org.mybatis.builder"/>
</mappers>
复制代码
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
  for (XNode child : parent.getChildren()) {
    if ("package".equals(child.getName())) {
      // 自动扫描包下全部映射器
      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);
        // 映射器比较复杂,调用XMLMapperBuilder
        // 注意在for循环里每一个mapper都从新new一个XMLMapperBuilder,来解析
        // 注意构建者里面还传入了 configuration
        // 也就是说 mapper映射文件 对应的配置对象也须要封装在 configuration中
        XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
        mapperParser.parse();
      } else if (resource == null && url != null && mapperClass == null) {
        // 使用绝对url路径
        ErrorContext.instance().resource(url);
        InputStream inputStream = Resources.getUrlAsStream(url);
        // 映射器比较复杂,调用XMLMapperBuilder
        XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
        mapperParser.parse();
      } else if (resource == null && url == null && mapperClass != null) {
        // 使用java类名
        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.");
      }
    }
  }
}
}
复制代码

咱们会对 mappers 标签里面的子标签进行遍历,对于除了 package 的三种资源标识(resource,url,class)来讲,每一个 mapper 子标签都会构建一个 XMLMapperBuilder 去构建解析对应的 mapper 映射配置文件。其实这些 资源标志 就是让程序去寻找到对应的 xxxMapper.xml 映射文件,而后一样适用构建者模式去构建 xxxMapper.xml 对应的配置对象

咱们来看一下 XmlMapperBuilder 构建者是如何构建相应的 “Mapper” 配置对象的。

public void parse() {
// 若是没有加载过再加载,防止重复加载
if (!configuration.isResourceLoaded(resource)) {
  //主线在这里 配置 mapper
  configurationElement(parser.evalNode("/mapper"));
  // 标记一下,已经加载过了
  configuration.addLoadedResource(resource);
  // 绑定映射器到namespace
  bindMapperForNamespace();
}
// 能够忽略 
parsePendingResultMaps();
parsePendingChacheRefs();
parsePendingStatements();
}

private void configurationElement(XNode context) {
try {
  //1.配置namespace
  // 这步骤也是挺关键 先记住 namespace这个东西
  String namespace = context.getStringAttribute("namespace");
  if (namespace.equals("")) {
    throw new BuilderException("Mapper's namespace cannot be empty");
  }
  builderAssistant.setCurrentNamespace(namespace);
  // 缓存 能够先无论
  //2.配置cache-ref
  cacheRefElement(context.evalNode("cache-ref"));
  //3.配置cache
  cacheElement(context.evalNode("cache"));
  //4.配置parameterMap(已经废弃,老式风格的参数映射)
  parameterMapElement(context.evalNodes("/mapper/parameterMap"));
  // mybatis 很是很是重要的功能
  //5.配置resultMap(高级功能)
  resultMapElements(context.evalNodes("/mapper/resultMap"));
  //6.配置sql(定义可重用的 SQL 代码段)
  sqlElement(context.evalNodes("/mapper/sql"));
  //7.配置select|insert|update|delete
  // 这里是真正的主线
  // 这里会根据前面的sql片断建立在Mapper中真正的配置对象 MappedStatement
  buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
  throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
}
}

// 传入 select|insert|update|delete 标签的 节点列表进行构建 Statement 
private void buildStatementFromContext(List<XNode> list) {
  // 判断DatabaseId
  if (configuration.getDatabaseId() != null) {
    buildStatementFromContext(list, configuration.getDatabaseId());
  }
  // 都是调用这个方法
  buildStatementFromContext(list, null);
}

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
  // 构建全部语句,一个mapper下能够有不少select
  // 这里又使用了构造者模式
  // 语句比较复杂,核心都在这里面,因此调用XMLStatementBuilder
  final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
  try {
    // 主线 
    // 核心XMLStatementBuilder.parseStatementNode
    statementParser.parseStatementNode();
  } catch (IncompleteElementException e) {
      // 若是出现SQL语句不完整,把它记下来,塞到configuration去
    configuration.addIncompleteStatement(statementParser);
  }
}
}
复制代码

上面那么长的一大串代码其实就是一个链式调用。咱们画一下流程便于你理解。

接下来就到了 XMLStatementBuilder 这个类中去构建 MappedStatement 对象了。

// 紧接着上面的解析构建方法
public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

    // 若是databaseId不匹配,退出
    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }

    //暗示驱动程序每次批量返回的结果行数
    Integer fetchSize = context.getIntAttribute("fetchSize");
    //超时时间
    Integer timeout = context.getIntAttribute("timeout");
    //引用外部 parameterMap,已废弃
    String parameterMap = context.getStringAttribute("parameterMap");
    // 前面三个不过重要
    //参数类型 这个在参数映射的时候挺重要
    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);
    //引用外部的 resultMap(高级功能) 很是重要了
    //算是 Mybatis 中核心功能了
    String resultMap = context.getStringAttribute("resultMap");
    //结果类型
    String resultType = context.getStringAttribute("resultType");
    //脚本语言,mybatis3.2的新功能 不重要
    String lang = context.getStringAttribute("lang");
    //获得语言驱动 不重要
    LanguageDriver langDriver = getLanguageDriver(lang);

    Class<?> resultTypeClass = resolveClass(resultType);
    //结果集类型,FORWARD_ONLY|SCROLL_SENSITIVE|SCROLL_INSENSITIVE 中的一种
    String resultSetType = context.getStringAttribute("resultSetType");
    //语句类型, STATEMENT|PREPARED|CALLABLE 的一种
    // 获取 Statement类型 这个是须要和 JDBC作映射的
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);

    //获取命令类型(select|insert|update|delete)
    String nodeName = context.getNode().getNodeName();
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    //是否要缓存select结果
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    //仅针对嵌套结果 select 语句适用:若是为 true,就是假设包含了嵌套结果集或是分组了,这样的话当返回一个主结果行的时候,就不会发生有对前面结果集的引用的状况。
    //这就使得在获取嵌套的结果集的时候不至于致使内存不够用。默认值:false。 
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    // Include Fragments before parsing
    // 解析以前先解析<include>SQL片断 这个在前面 XMLMapperBuilder 
    // 中已经构建完成,这里须要调用并解析
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    // Parse selectKey after includes and remove them.
    //解析以前先解析<selectKey> selectKey主要涉及须要某些特殊关系来设置主键的值
    processSelectKeyNodes(id, parameterTypeClass, langDriver);
    
    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    // 作了解
    //解析成SqlSource,通常是DynamicSqlSource
    // 其实无论 Dynamic 仍是 Raw 最终都会解析成 static
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    String resultSets = context.getStringAttribute("resultSets");
    //(仅对 insert 有用) 标记一个属性, MyBatis 会经过 getGeneratedKeys 或者经过 insert 语句的 selectKey 子元素设置它的值
    String keyProperty = context.getStringAttribute("keyProperty");
    //(仅对 insert 有用) 标记一个属性, MyBatis 会经过 getGeneratedKeys 或者经过 insert 语句的 selectKey 子元素设置它的值
    String keyColumn = context.getStringAttribute("keyColumn");
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    if (configuration.hasKeyGenerator(keyStatementId)) {
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? new Jdbc3KeyGenerator() : new NoKeyGenerator();
    }
    // 真正的主线在这里
	//调用助手类去真正建立MappedStatement而后加入配置Configuration中
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered, 
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
复制代码

这个 parseStatementNode 方法比较长,但其实你能够发现这里无非就是对 每一个 CRUD(这里指 select delete update insert标签) 标签作了具体的解析,其中比较重要的就几种,好比 ParameterType,ResultMap,解析成SqlSource(Sql的封装),sql片断的解析。。。 其余的其实都是支线了,你能够自行去了解。

在作完属性的一些解析后,XMLStatementBuilder 会将这些属性再 委托 给助手对象 MapperBuilderAssistant 去进行构建 MappedStatement

// 好多参数。。
public MappedStatement addMappedStatement( String id, SqlSource sqlSource, StatementType statementType, SqlCommandType sqlCommandType, Integer fetchSize, Integer timeout, String parameterMap, Class<?> parameterType, String resultMap, Class<?> resultType, ResultSetType resultSetType, boolean flushCache, boolean useCache, boolean resultOrdered, KeyGenerator keyGenerator, String keyProperty, String keyColumn, String databaseId, LanguageDriver lang, String resultSets) {
    
    if (unresolvedCacheRef) {
      throw new IncompleteElementException("Cache-ref not yet resolved");
    }
    
    //为id加上namespace前缀
    // 这里就有意思了 
    // 还记的上面说的 namespace 吗? 
    // 这里会使用 CRUD自己标签的id 加上namespace构建独一无二的id
    // 主要是由于全部mapper文件中的 crud 标签配置对象都是直接存储
    // 在 configuration 中的 ,为了防止有些 标签id会重复
    id = applyCurrentNamespace(id, false);
    // 是不是select语句
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

    // 很经典的构造者模式了,返回须要被构建的对象就能够链式调用
    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType);
    statementBuilder.resource(resource);
    statementBuilder.fetchSize(fetchSize);
    statementBuilder.statementType(statementType);
    statementBuilder.keyGenerator(keyGenerator);
    statementBuilder.keyProperty(keyProperty);
    statementBuilder.keyColumn(keyColumn);
    statementBuilder.databaseId(databaseId);
    statementBuilder.lang(lang);
    statementBuilder.resultOrdered(resultOrdered);
    statementBuilder.resulSets(resultSets);
    setStatementTimeout(timeout, statementBuilder);

    //1.参数映射 这里很重要 
    // 由于 parameterMap 被弃用 因此这里通常为空
    // 而真正传入的其实应该是 parameterType 这个 Class
    setStatementParameterMap(parameterMap, parameterType, statementBuilder);
    //2.结果映射 也很重要
    setStatementResultMap(resultMap, resultType, resultSetType, statementBuilder);
    setStatementCache(isSelect, flushCache, useCache, currentCache, statementBuilder);

    MappedStatement statement = statementBuilder.build();
    //建造好调用configuration.addMappedStatement
    // 加入configuration 这里整个构建流程就算基本结束了。。
    configuration.addMappedStatement(statement);
    return statement;
}
复制代码

咱们发现最终是经过 XMLMapperBuilder 的助手类去构建 MappedStatement 并传入 Configuration 中的。咱们这时候能够将上面一张流程图更加细化一些。

参数映射和结果映射

参数映射流程

咱们来看一下刚刚的还未分析完的参数映射代码

private void setStatementParameterMap( String parameterMap, Class<?> parameterTypeClass, MappedStatement.Builder statementBuilder) {
    // 给parameterMap加上namespace 可是由于parameterMap被弃用 因此通常返回null
    parameterMap = applyCurrentNamespace(parameterMap, true);

    if (parameterMap != null) {
      try {
        statementBuilder.parameterMap(configuration.getParameterMap(parameterMap));
      } catch (IllegalArgumentException e) {
        throw new IncompleteElementException("Could not find parameter map " + parameterMap, e);
      }
    } else if (parameterTypeClass != null) {
      // 解析 parameterType生成的类对象
      List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();
      // 构造ParameterMap类内部的构建类
      // 这里主要是 parameterTypeClass 的赋值 而parameterMapping仅做为一个空列表传入
      ParameterMap.Builder inlineParameterMapBuilder = new ParameterMap.Builder(
          configuration,
          statementBuilder.id() + "-Inline",
          parameterTypeClass,
          parameterMappings);
      // 经过内部构建类构建ParameterMap并传入配置对象中
      statementBuilder.parameterMap(inlineParameterMapBuilder.build());
    }
}
复制代码

由于 parameterMap 弃用,因此设置参数大部分是围绕着 parameterType 走的,总结来讲就是经过 parameterType 去构建一个 ParameterMap 对象(这里是使用的ParameterMap中的内部构建者构建的)。而后将这个 ParameterMap 对象存储在 MappedStatement 中。

其实这个 ParameterMap 对象也就三个字段,甚至咱们仅仅须要两个。我这里简单写一个 ParameterMap 类。

public class ParameterMap {
  private String id;
  private Class<?> type;
  // 其实若是 parameterMapping 弃用了这个字段也没什么用了
  // 估计后面会进行重构
  private List<ParameterMapping> parameterMappings;
}
复制代码

官方文档已经要删除这个元素了。

结果映射流程

说完了参数映射,其实结果映射也大同小异。

private void setStatementResultMap( String resultMap, Class<?> resultType, ResultSetType resultSetType, MappedStatement.Builder statementBuilder) {
    // 应用 namespace
    resultMap = applyCurrentNamespace(resultMap, true);

    List<ResultMap> resultMaps = new ArrayList<ResultMap>();
    if (resultMap != null) {
      // 进行ResultMap的解析
      // 这里经过,分割 你能够写成 xxxResultMap,xxxResultMap 但我还没发现有人使用过
      String[] resultMapNames = resultMap.split(",");
      for (String resultMapName : resultMapNames) {
        try {
          // 这里其实就是经过 resultMapName 
          // 去原来已经在 configuration解析完成的 <resultMap> 标签
          // 配置中获取相应的 resultMap而后加入 resultMaps中
          resultMaps.add(configuration.getResultMap(resultMapName.trim()));
        } catch (IllegalArgumentException e) {
          throw new IncompleteElementException("Could not find result map " + resultMapName, e);
        }
      }
    } else if (resultType != null) {
      // resultType解析
      //<select id="selectUsers" resultType="User">
      //这种状况下,MyBatis 会在幕后自动建立一个 ResultMap,基于属性名来映射列到 JavaBean 的属性上。
      //若是列名没有精确匹配,你能够在列名上使用 select 字句的别名来匹配标签。
      //建立一个inline result map, 把resultType设上就OK了,
      //而后后面被DefaultResultSetHandler.createResultObject()使用
      //DefaultResultSetHandler.getRowValue()使用
      ResultMap.Builder inlineResultMapBuilder = new ResultMap.Builder(
          configuration,
          statementBuilder.id() + "-Inline",
          resultType,
          new ArrayList<ResultMapping>(),
          null);
      // 最后仍是封装成了 resultMap 集合
      resultMaps.add(inlineResultMapBuilder.build());
    }
    // 将 resultMap 集合加入配置
    statementBuilder.resultMaps(resultMaps);
    // 这个直接加入配置
    statementBuilder.resultSetType(resultSetType);
}
复制代码

最新版本的结果映射写在了构建流程中。

总的说来也就是 获取 resultMap或者resultType中的值 而后经过这个值构建ResultMap传入 MappedStatement配置中去

这个时候咱们就能够画出大概的 MappedStatement 对象的构建流程图了。

总结

其实整个 Mybatis 初始化流程就是 对配置文件进行解析成配置对象装入内存以便在后面执行的过程当中使用。而 mybatis 的配置文件会存储到对应的 Configuration 对象中,而映射配置文件会专门解析 CRUD 标签存入 MappedStatement 对象中,最终这个 MappedStatement 对象也会加入集合并存入到 Configuration 中去。这其中主要用到了 工厂模式构建者模式

Mybatis主要有两个执行模块,第一就是这篇文章咱们所讲的 构建部分,还有一部分就是 Sql的执行部分,关于执行部分我会在下一篇文章中分享。

相关文章
相关标签/搜索