Mybatis源码解析(三):映射配置文件的解析

有时候,不是因为你没有能力,也不是因为你缺少勇气,只是因为你付出的努力还太少,所以,成功便不会走向你。而你所需要做的,就是坚定你的梦想,你的目标,你的未来,然后以不达目的誓不罢休的那股劲,去付出你的努力,成功就会慢慢向你靠近。

导读:本篇文章讲解 Mybatis源码解析(三):映射配置文件的解析,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com,来源:原文

Mybatis源码系列文章

手写源码(了解源码整体流程及重要组件)

Mybatis源码解析(一):环境搭建

Mybatis源码解析(二):全局配置文件的解析

Mybatis源码解析(三):映射配置文件的解析

Mybatis源码解析(四):sql语句及#{}、${}的解析

Mybatis源码解析(五):SqlSession会话的创建

Mybatis源码解析(六):缓存执行器操作流程

Mybatis源码解析(七):查询数据库主流程

Mybatis源码解析(八):Mapper代理原理

Mybatis源码解析(九):插件机制

Mybatis源码解析(十):一级缓存和二级缓存



前言

  • Mybatis框架中有两种类型xml文件,核心配置文件以及实体类映射配置文件
  • 映射配置文件的路径在核心配置的<mappers>标签中配置(这样就可以只解析一个核心配置文件即可)
  • 从本系列Mybatis源码解析(二):全局配置文件的解析第四章节可知,解析<configuration>标签的子标签<mappers>即使解析映射配置文件

一、映射配置文件解析入口

  • 如下为核心配置类<configuration>标签下所有子标签的解析方法入口

XMLConfigBuilder.java

private void parseConfiguration(XNode root) {
  try {
    // issue #117 read properties first
    // 解析</properties>标签
    propertiesElement(root.evalNode("properties"));
    // 解析</settings>标签
    Properties settings = settingsAsProperties(root.evalNode("settings"));
    loadCustomVfs(settings);
    loadCustomLogImpl(settings);
    // 解析</typeAliases>标签
    typeAliasesElement(root.evalNode("typeAliases"));
    // 解析</plugins>标签
    pluginElement(root.evalNode("plugins"));
    // 解析</objectFactory>标签
    objectFactoryElement(root.evalNode("objectFactory"));
    // 解析</objectWrapperFactory>标签
    objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
    // 解析</reflectorFactory>标签
    reflectorFactoryElement(root.evalNode("reflectorFactory"));
    settingsElement(settings);
    // read it after objectFactory and objectWrapperFactory issue #631
    // 解析</environments>标签
    environmentsElement(root.evalNode("environments"));
    // 解析</databaseIdProvider>标签
    databaseIdProviderElement(root.evalNode("databaseIdProvider"));
    // 解析</typeHandlers>标签
    typeHandlerElement(root.evalNode("typeHandlers"));
    // 解析</mappers>标签 加载映射文件流程主入口
    mapperElement(root.evalNode("mappers"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
  }
}

进入mapperElement(root.evalNode(“mappers”));

  • root.evalNode(“mappers”)返回XNode对象,标签及其子标签都被封装到parent对象中

  • <package>标签为代理模式,引入包名,解析代理接口对应的映射配置文件(后续篇章单独讲)

  • <mapper>标签引入映射配置文件的方式,resource、url、class;前两种是加载资源构建XMLMapperBuilder解析,最后一种则是代理模式解析(后续篇章单独讲)
    在这里插入图片描述

  • 这里与解析核心配置文件方式一样,先创建XMLMapperBuilder解析类,再调用其解析方法parse()

private void mapperElement(XNode parent) throws Exception {
  if (parent != null) {
    // 获取<mappers>标签的子标签
    for (XNode child : parent.getChildren()) {
      // <package>子标签
      if ("package".equals(child.getName())) {
        // 获取mapper接口和mapper映射文件对应的package包名
        String mapperPackage = child.getStringAttribute("name");
        // 将包下所有的mapper接口以及它的代理工厂对象存储到一个Map集合中,key为mapper接口类型,value为代理对象工厂
        configuration.addMappers(mapperPackage);
      } else {// <mapper>子标签
        // 获取<mapper>子标签的resource属性
        String resource = child.getStringAttribute("resource");
        // 获取<mapper>子标签的url属性
        String url = child.getStringAttribute("url");
        // 获取<mapper>子标签的class属性
        String mapperClass = child.getStringAttribute("class");
        // 它们是互斥的
        if (resource != null && url == null && mapperClass == null) {
          ErrorContext.instance().resource(resource);
          try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
            // 专门用来解析mapper映射文件
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            // 通过XMLMapperBuilder解析mapper映射文件
            mapperParser.parse();
          }
        } else if (resource == null && url != null && mapperClass == null) {
          ErrorContext.instance().resource(url);
          try(InputStream inputStream = Resources.getUrlAsStream(url)){
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            // 通过XMLMapperBuilder解析mapper映射文件
            mapperParser.parse();
          }
        } else if (resource == null && url == null && mapperClass != null) {
          Class<?> mapperInterface = Resources.classForName(mapperClass);
          // 将指定mapper接口以及它的代理对象存储到一个Map集合中,key为mapper接口类型,value为代理对象工厂
          configuration.addMapper(mapperInterface);
        } else {
          throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
        }
      }
    }
  }
}

二、创建【映射配置文件解析类】

进入XMLMapperBuilder构建函数

  • inputStream:由映射配置文件xml加载而来的字节输入流
  • 将字节输入流转换为Document对象,存入XPathParser,而XPathParser对象引用放到XMLMapperBuilder对象parser属性中
  • 然后就是configuration赋值XMLMapperBuilder的父类BaseBuilder等一些其他属性
public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
  this(new XPathParser(inputStream, true, configuration.getVariables(), new XMLMapperEntityResolver()),
      configuration, resource, sqlFragments);
}

private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
  super(configuration);
  this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
  this.parser = parser;
  this.sqlFragments = sqlFragments;
  this.resource = resource;
}

三、调用【映射配置文件解析类】的解析方法

进入XMLMapperBuilder的parse()方法

  • 资源属性resource字符串即映射配置文件的resource或url,解析过则放入集合
  • addLoadedResource:添加到集合中;isResourceLoaded:判断集合中是否有解析的resource
public void parse() {
  // mapper映射文件是否已经加载过
  if (!configuration.isResourceLoaded(resource)) {
    // 从映射文件中的<mapper>根标签开始解析,直到完整的解析完毕
    configurationElement(parser.evalNode("/mapper"));
    // 标记已经解析
    configuration.addLoadedResource(resource);
    // 为命名空间绑定映射
    bindMapperForNamespace();
  }

  // 解析ResultMap
  parsePendingResultMaps();
  // 解析缓存
  parsePendingCacheRefs();
  // 解析statement
  parsePendingStatements();
}

1、<mapper>标签解析方法

  • <mapper>标签的namespace属性是必输值,空则报错
  • MappedStatement对象由sql语句、入参、返回值等标签内属性组成
  • MapperBuilderAssistant builderAssistant:构建MappedStatement对象的Builder类,因为其属性繁多,通过Builder类一点一点的的去构建对象
  • 缓存标签等后续单独讲一二级缓存时候讲
  • <parameterMap>标签和<resultMap>标签也就是将标签内属性解析到ParameterMap和ResultMap对象,然后再添加到builderAssistant中,为以后创建MappedStatement对象做准备
  • 主要部分看下增删改查sql语句的解析部分
private void configurationElement(XNode context) {
  try {
    // 获取<mapper>标签的namespace值,也就是命名空间
    String namespace = context.getStringAttribute("namespace");
    // 命名空间不能为空
    if (namespace == null || namespace.isEmpty()) {
      throw new BuilderException("Mapper's namespace cannot be empty");
    }
    // MapperBuilderAssistant:构建MappedStatement对象的构建助手,设置当前的命名空间为namespace的值
    builderAssistant.setCurrentNamespace(namespace);
    // 解析<cache-ref>子标签
    cacheRefElement(context.evalNode("cache-ref"));
    // 解析<cache>子标签
    cacheElement(context.evalNode("cache"));

    // 解析<parameterMap>子标签
    parameterMapElement(context.evalNodes("/mapper/parameterMap"));
    // 解析<resultMap>子标签
    resultMapElements(context.evalNodes("/mapper/resultMap"));
    // 解析<sql>子标签,也就是SQL片段
    sqlElement(context.evalNodes("/mapper/sql"));
    // 解析<select>\<insert>\<update>\<delete>子标签
    // 将cache对象封装到MappedStatement中
    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);
  }
}

2、<select><insert><update><delete>标签的解析

  • 四个标签都解析成XNode对象,组装成list,后续遍历解析
  • 一共有多少增删改查标签,最终就会构建多少MappedStatement
private void buildStatementFromContext(List<XNode> list) {
  if (configuration.getDatabaseId() != null) {
    buildStatementFromContext(list, configuration.getDatabaseId());
  }
  // 构建MappedStatement
  buildStatementFromContext(list, null);
}

进入buildStatementFromContext构建MappedStatement方法

  • 这里的操作和解析核心配置类和映射配置类一样,创建一个专门解析MappedStatement类的Builder对象
  • XMLStatementBuilder类构造函数则是将入参赋值对应属性,无逻辑代码
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
  for (XNode context : list) {
    // MappedStatement解析器
    final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, 
    context, requiredDatabaseId);
    try {
      //  // 解析select等4个标签,创建MappedStatement对象
      statementParser.parseStatementNode();
    } catch (IncompleteElementException e) {
      configuration.addIncompleteStatement(statementParser);
    }
  }
}

进入parseStatementNode解析select等4个标签,循环遍历创建MappedStatement对象

  • 通过遍历创建MappedStatement对象也证明了,有多少增删改查标签,就有多少该对象
  • 这个方法的作用就是将<select><insert><update><delete>标签内的属性全部解析出来封装成MappedStatement对象
  • 如:id,则是<select>标签的id值,一般这个值我们会和Mapper接口的方法名设置成一致
  • SqlSource对象:由sql语句(带?号)、替换?的#{}属性值、入参的对象或Map、动态sql标签等组成;创建过程复杂,后续系列单独讲
  • 最后一步,通过builderAssistant构建助手创建MappedStatement对象
public void parseStatementNode() {
    // 获取statement的id属性(特别关键的值)
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }

    String nodeName = context.getNode().getNodeName();
    // 解析SQL命令类型是什么?确定操作是CRUD中的哪一种 后续执行器操作时会用到
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    //是否查询语句
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    // Include Fragments before parsing
    // <include>标签解析
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    // 获取入参类型
    String parameterType = context.getStringAttribute("parameterType");
    // 别名处理,获取入参对应的Java类型
    Class<?> parameterTypeClass = resolveClass(parameterType);

    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    // Parse selectKey after includes and remove them.
    // 解析<selectKey>标签
    processSelectKeyNodes(id, parameterTypeClass, langDriver);

    // Parse the SQL (pre: <selectKey> and <include> 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 {
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }

    // *******创建SqlSource,解析SQL,封装SQL语句(未参数绑定)和入参信息
    // 问题:sql占位符如何进行的替换?动态sql如何进行的解析?
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

    // 设置默认StatementType为Prepared,该参数指定了后面的JDBC处理时,采用哪种Statement
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    // 获取结果映射类型
    String resultType = context.getStringAttribute("resultType");
    // 别名处理,获取返回值对应的Java类型
    Class<?> resultTypeClass = resolveClass(resultType);
    // 获取ResultMap
    String resultMap = context.getStringAttribute("resultMap");
    String resultSetType = context.getStringAttribute("resultSetType");
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    if (resultSetTypeEnum == null) {
      resultSetTypeEnum = configuration.getDefaultResultSetType();
    }
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    String resultSets = context.getStringAttribute("resultSets");

    // 通过构建者助手,创建MappedStatement对象
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

进入addMappedStatement构建MappedStatement对象的方法

  • applyCurrentNamespace方法:创建的id即statementId = namespace.id
  • statementBuilder通过构建者模式一个属性一个属性的构建对象,最后.build()方法返回构建的MappedStatement对象本身
  • addMappedStatement方法:将MappedStatement对象添加到全局对象Configuration对象中
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 = applyCurrentNamespace(id, false);
  boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

  //利用构建者模式,去创建MappedStatement.Builder,用于创建MappedStatement对象
  MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
      .resource(resource)
      .fetchSize(fetchSize)
      .timeout(timeout)
      .statementType(statementType)
      .keyGenerator(keyGenerator)
      .keyProperty(keyProperty)
      .keyColumn(keyColumn)
      .databaseId(databaseId)
      .lang(lang)
      .resultOrdered(resultOrdered)
      .resultSets(resultSets)
      .resultMaps(getStatementResultMaps(resultMap, resultType, id))
      .resultSetType(resultSetType)
      .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
      .useCache(valueOrDefault(useCache, isSelect))
      .cache(currentCache);// 将cache对象存入到MappedStatement中

  ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
  if (statementParameterMap != null) {
    statementBuilder.parameterMap(statementParameterMap);
  }

  // 通过MappedStatement.Builder,构建一个MappedStatement
  MappedStatement statement = statementBuilder.build();
  // 将MappedStatement对象存储到Configuration中的Map集合中,key为statement的id,value为MappedStatement对象
  configuration.addMappedStatement(statement);
  return statement;
}

1)applyCurrentNamespace方法:构建statementId

  • 中间是一些属性判断,省略,核心代码就最后一句
  • currentNamespace是<mapper>标签解析方法里构建助手builderAssistant给其属性设置namesapce
  • 所有返回的id即statementid=命名空间(一般设置全限定类名)+“.”+增删改查标签id(一般设置为Mapper接口方法名)
  public String applyCurrentNamespace(String base, boolean isReference) {
	...
	//属性判断
	...
    return currentNamespace + "." + base;
  }

2)addMappedStatement方法:添加对象到Configuration

  • MappedStatement对象以Map形式存在,key则是其id:statementid=namespace.id,value是MappedStatement对象本身
  • Configuration对象与MappedStatement对象是一对多的关系,因为全局对象下有多个映射配置文件,而每个映射配置文件下又有多个增删改查标签即MappedStatement对象
  protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
      .conflictMessageProducer((savedValue, targetValue) ->
          ". please check " + savedValue.getResource() + " and " + targetValue.getResource());
  ...

  public void addMappedStatement(MappedStatement ms) {
    mappedStatements.put(ms.getId(), ms);
  }

至此,<mapper>标签增删改查标签解析成多个MappedStatement对象,映射配置文件解析完毕


总结

  • 映射配置文件的解析是全局配置文件解析的一部分,本篇内容只是将其单独拎出来了
  • 映射配置文件的解析就是将<select><insert><update><delete>每个标签内容封装成一个MappedStatement对象
  • 所有MappedStatement对象以key为statementId=namespaceId.id的map形式挂在全局配置类Configuration下

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/148598.html

(0)
飞熊的头像飞熊bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!