mybatis源码解读(四):XMLStatementBuilder详解

功能

XMLMapperBuilder 中会将 mapper 映射文件中除 CRUD 外的标签解析验证,轮到CRUD标签的时候,是交给专门的类去做处理的,也就是XMLStatementBuilder。XMLStatementBuilder的解析工作:
第一步:解析一些较基本的属性,比如 id、databaseId(多数据库配置)、useCache(是否启动二级缓存)、flushCache(只要语句调用就刷新缓存)等。
第二步;替换sql中的include标签,将复用其它地方的sql整合处理成为一个完整的sql
第三步:处理主键生成策略,mybatis 默认使用的是 NoKeyGenerator,如果设置了 useGeneratedKeys 会使用 Jdbc3KeyGenerator
第四步:mybatis到了这一步,会将sql处理成为sqlSource存储起来并且替换类似#{}这样的特殊字符,SqlSource是mybatis中比较重要的一个部分,所以这一步sql处理会比较复杂。

UML

在这里插入图片描述

代码解析

parseStatementNode() 是 XMLStatementBuilder 解析的入口,首先,他会去找CURD标签上的唯一id,然后就是databaseId,databaseId是数据库厂商标识,MyBatis 会加载所有不带 databaseId 或匹配当前 databaseId 的语句,如果带和不带的语句都有,则不带的会被忽略,一般选择databaseId的时候是需要配置多数据库厂商的时候。在解析完databaseId 之后就是解析flushCache、useCache等标签,默认情况下,如果是select语句,则是开启缓存的

public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

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

    String nodeName = context.getNode().getNodeName();
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    // 如果是select语句,则默认是 useCache 启动缓存
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
    ...
  }

在CURD标签中,有时候为了方便是会配置一些 include标签的,复用其他sql,所以XMLStatementBuilder 在接下来的工作就是解析 include标签,解析 include标签的功能是由 XMLIncludeTransformer 这个类完成的,构造方法是将当前的configuration、builderAssistant传入进去,之所以传入这两个参数是为了在接下来的解析动作中可以加载其它的sqlSeqment。

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

在XMLIncludeTransformer 的 applyIncludes()方法中,会去解析include标签,这一块的逻辑比较复杂,不过条例还算清晰,在这之前需要介绍一下mybatis的几个概念,我们在CRUD标签中所写的sql最终都会解析成为一个个sqlnode,比如 select * from 这种纯文本的sql会对应为一个静态文本sql,foreach这种动态sql会对应为一个foreachnode,详细细节在解析动态sql中会介绍。
首先进入到applyIncludes()方法中,是几个 条件case, 我们的CURD标签,根节点是一个element标签,仔细看这段逻辑,空标签会走第二个条件case,也就是 source.getNodeType() == Node.ELEMENT_NODE 这个条件,在这块代码块中会获取到这个节点下的所有的孩子节点,进行递归处理,其实这里就可以拿到 include标签了。进入递归,如果是include标签,则会走第一个条件节点,会去configuration中找到这个include包括的sql加载进来,并且进行递归去处理,处理完成后则会将处理完成的sql拼接到当前node后并且移除当前include,至于如果是静态文本sql的话,会做一个非常重要的处理,就是替换 $ {}这种格式的占位符,也就是预先替换。

private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
    // 如果包括include标签则会走这里
    if (source.getNodeName().equals("include")) {
      Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
      Properties toIncludeContext = getVariablesContext(source, variablesContext);
      applyIncludes(toInclude, toIncludeContext, true);
      if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
        toInclude = source.getOwnerDocument().importNode(toInclude, true);
      }
      source.getParentNode().replaceChild(toInclude, source);
      // 会将include包含的sql文本拼接到这里并且将自己给移除掉
      while (toInclude.hasChildNodes()) {
        toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
      }
      toInclude.getParentNode().removeChild(toInclude);
    // 如果是element则是走一块逻辑,其实也就是 select insert等标签
    } else if (source.getNodeType() == Node.ELEMENT_NODE) {
      if (included && !variablesContext.isEmpty()) {
        // replace variables in attribute values
        NamedNodeMap attributes = source.getAttributes();
        for (int i = 0; i < attributes.getLength(); i++) {
          Node attr = attributes.item(i);
          attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext));
        }
      }
      //
      NodeList children = source.getChildNodes();
      for (int i = 0; i < children.getLength(); i++) {
        applyIncludes(children.item(i), variablesContext, included);
      }
    // 如果是 select * from 这种的sql走得这里,TEXT_NODE
    } else if (included && (source.getNodeType() == Node.TEXT_NODE || source.getNodeType() == Node.CDATA_SECTION_NODE)
        && !variablesContext.isEmpty()) {
      // replace variables in text node
      source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
    }
  }

替换 $ {}这种占位符是交给PropertyParser.parse()去完成,mybatis对于$ {}的处理是提前处理的,它会从环境变量中去取相对应key的值拿过来在这里替换掉,可以看他的处理逻辑,通过GenericTokenParser去匹配 $ {} ,如果匹配到的话就通过handler去环境变量中拿,variables就是configuration中配置的properties,至于parser.parse(string)的内容是一个替换算法,感兴趣的同学可以自己去看看。
其实从这段逻辑也可以看出,$ {} 是并不安全的,因为他会完全将符合key的值替换到sql中,会有sql注入的风险。

public static String parse(String string, Properties variables) {
    VariableTokenHandler handler = new VariableTokenHandler(variables);
    GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
    return parser.parse(string);
  }

经过incude标签的处理,sqlnode已经是粗略加工过的sql了,接下来就是解析一些标签,诸如selectKey 、根据selectKey去选择主键生成策略,通过LanguageDriver去加工处理前边初步加工过的sql为sqlsource,然后就是fetchSize、timeout、parameterMap、resultType、resultMap、resultSetType、keyProperty、keyColumn、resultSets标签。

    // Parse selectKey after includes and remove them.
    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;
    }
	// 通过 langDriver 去解析sqlSource
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    // mybatus默认是PrepareStatement处理
    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");
    Class<?> resultTypeClass = resolveClass(resultType);
    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");

在这一步中,其实有个很重要的东西,就是 SqlSource 的解析,mybatis会将 CRUD标签中的sql解析成为 sqlSource存放起来(主要是解析动态sql),解析sql是由 LanguageDriver 去处理的,LanguageDriver 是个接口专门负责动态sql的解析接口,有几个实现类,一般mybatis解析是由XMLLanguageDriver去做的,当然你如果有需求也可以自己去配置driver

 String lang = context.getStringAttribute("lang");
 LanguageDriver langDriver = getLanguageDriver(lang);
  SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

LanguageDriver 的实现类
在这里插入图片描述
在createSqlSource()中可以看到,解析动态sql是由 XMLScriptBuilder 去完成的

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

下一章介绍XMLScriptBuilder 的解析工作。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
MyBatis是一个开源的持久层框架,它的配置文件包含了会深深影响MyBatis行为的设置和属性信息。配置文件的顶层结构包括properties(属性)、settings(设置)、typeAliases(类型别名)、typeHandlers(类型处理器)、objectFactory(对象工厂)、plugins(插件)、environments(环境配置)、mappers(映射器)等。\[3\] 在MyBatis源码中,有一个方法build,该方法用于解析动态脚本并生成SqlSource对象。在该方法中,首先会调用parseDynamicTags方法解析动态标签,得到一个包含多个SqlNode的列表contents。然后,通过将contents传入MixedSqlNode的构造函数,创建一个MixedSqlNode对象rootSqlNode。接下来,根据是否为动态脚本,分别创建DynamicSqlSource或RawSqlSource对象,并将configuration、rootSqlNode和parameterType作为参数传入构造函数。最后,返回创建的SqlSource对象。\[2\] 另外,还有一个方法configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql),该方法用于创建StatementHandler对象。具体的解读需要查看该方法的实现代码。 #### 引用[.reference_title] - *1* [mybatis源码深度解析](https://blog.csdn.net/qq_31359923/article/details/126582701)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v4^insert_chatgpt"}} ] [.reference_item] - *2* [【MybatisMybatis源码解读](https://blog.csdn.net/keepfriend/article/details/124356649)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v4^insert_chatgpt"}} ] [.reference_item] - *3* [mybatis源码解析](https://blog.csdn.net/weixin_43189971/article/details/125418419)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v4^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值