Mybatis源码学习(3)-解析器模块之PropertyParser、GenericTokenParser

一、模块简介

  解析器模块的主要提供了两个功能:

  1. 为解析mybatis配置文件、mapper映射文件等提供支持。
  2. 为处理动态SQL语句中的占位符提供支持。

二、包结构

在这里插入图片描述
  解析器模块的包结构如上图所示,其中,XPathParser 和XNode 主要用来解析 XML,PropertyParser、GenericTokenParser 、TokenHandler 主要用于标记处理(占位符),ParsingException是异常处理类。

  1. PropertyParser (Property解析器)
  2. GenericTokenParser (通用标记解析器)
  3. TokenHandler (标记处理器接口)
  4. XNode (XML节点)
  5. XPathParser (XML的XPath解析器)
  6. ParsingException (异常类)
1、TokenHandler 标记处理器

  TokenHandler是一个接口,定义了一个handleToken方法,该方法主要用于标记处理,具体实现在各自实现类中完成。主要是配合通用标记解析器GenericTokenParser类完成对配置文件等的解析工作,其中TokenHandler主要完成处理,而解析器主要实现前序工作-解析,具体使用场景在解析器章节详细介绍。接口代码:

package org.apache.ibatis.parsing;

public interface TokenHandler {
  String handleToken(String content);
}
2、GenericTokenParser 通用标记解析器

  GenericTokenParser类是通用的标记解析器,主要完成了配置文件、脚本等文件或代码片段中的占位符的解析工作,然后再根据给定的标记处理器(TokenHandler)来进行表达式的处理工作。

  • 属性字段和构造器
	private final String openToken;
	private final String closeToken;
	private final TokenHandler handler;

	public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
		this.openToken = openToken;
		this.closeToken = closeToken;
		this.handler = handler;
	}

  其中,定义了三个字段,分别为openToken(开始标记)、closeToken(结束标记)、handler(标记处理器)。定义了一个带参构造函数,实现对象的初始化和赋值,只提供了一个构造函数,所以每次创建实例对象的时候,都需要为定义的三个字段进行赋值。

  • 解析方法parse()
	public String parse(String text) {
		if (text == null || text.isEmpty()) {
			return "";
		}
		// search open token
		int start = text.indexOf(openToken, 0);
		if (start == -1) {// 不存在开始标记,直接返回
			return text;
		}
		// 转换成字符数组
		char[] src = text.toCharArray();
		// 偏移量,即已经解析到的位置,默认从0开始
		int offset = 0;
		// 需要返回的字符串变量
		final StringBuilder builder = new StringBuilder();
		StringBuilder expression = null;
		while (start > -1) {// 如果存在开始标记
			// 存在开始标记,且开始标记前有转义字符,反斜杠"\",当有转义字符反斜杠时,默认该标记字符不做特殊处理
			// 即,不把该标记字符作为参数进行处理
			if (start > 0 && src[start - 1] == '\\') {
				// this open token is escaped. remove the backslash and continue.
				builder.append(src, offset, start - offset - 1).append(openToken);
				offset = start + openToken.length();
			} else {//存在开始标记,且需要特殊处理,即作为参数进行处理
				// found open token. let's search close token.
				if (expression == null) {
					expression = new StringBuilder();
				} else {
					expression.setLength(0);
				}
				builder.append(src, offset, start - offset);
				offset = start + openToken.length();
				int end = text.indexOf(closeToken, offset);
				while (end > -1) {//存在结束标记时
					if (end > offset && src[end - 1] == '\\') {//如果结束标记前面有转义字符时
						// this close token is escaped. remove the backslash and continue.
						expression.append(src, offset, end - offset - 1).append(closeToken);
						offset = end + closeToken.length();
						end = text.indexOf(closeToken, offset);
					} else {//不存在转义字符,即需要作为参数进行处理
						expression.append(src, offset, end - offset);
						offset = end + closeToken.length();
						break;
					}
				}
				if (end == -1) {
					// close token was not found.
					builder.append(src, start, src.length - start);
					offset = src.length;
				} else {
					//首先根据参数的key(即expression)进行参数处理,返回?作为占位符
					builder.append(handler.handleToken(expression.toString()));
					offset = end + closeToken.length();
				}
			}
			start = text.indexOf(openToken, offset);
		}
		if (offset < src.length) {
			builder.append(src, offset, src.length - offset);
		}
		return builder.toString();
	}

  该方法主要实现了配置文件、脚本等片段中占位符的解析、处理工作,并返回最终需要的数据。其中,解析工作由该方法完成,处理工作是在第52行处由处理器handler的handleToken()方法来实现。该功能主要实现的效果是,通过解析获取到代码片段中占位符对应的变量,然后通过真正的处理器TokenHandler获取该变量对应的值,然后把代码片段中的占位符替换成需要的值,比如:如果是Mybatis配置文件中的${xxx},这个时候就会直接使用xxx变量值进行直接替换;如果是SQL代码片段中的占位符#{aaa},这个时候就会把占位符整体替换成“?”,然后构成一个需要传递参数的SQL语句。下面具体分析一下parse()方法:
  第一步:其中,方法的参数text其实一般是SQL脚本字符串或者Mybatis配置文件中的占位符。上面代码从第2行到第9行,首先验证参数问题,如果是null,就返回空字符串。否则继续执行,下面继续验证是否包含开始标签,如果不包含,默认不是占位符,直接原样返回即可,否则继续执行。
  第二步:把text转成字符数组src,并且定义默认偏移量offset=0、存储最终需要返回字符串的变量builder,text变量中占位符对应的变量名expression。判断start是否大于-1(即text中是否存在openToken),如果存在就执行下面代码。
  第三步:从20行到24行代码,是判断如果开始标记前如果有转义字符,就不作为openToken进行处理,否则继续处理。
  第四步:当存在openToken且前面不包含转义字符,这个时候就可以认为这是一个占位符的开始标记。从26到30行代码,主要是重置expression变量,避免空指针或者老数据干扰。
  第五步:第31行代码主要是把考试标记前的字符串存储到builder中,第32行diamante主要是重新修正偏移量offset,即偏移量为上一次的偏移量+开始标签的长度。第33行主要是判断结束标签在片段中从偏移位置起第一次出现的位置。
  第六步:判断end是否大于-1,即是否存在结束标记,如果存在(大于-1),则继续判断结束标记前是否有转义字符,如果存在转义字符即认为该结束标记字符串不作为结束标记使用,就需要把从偏移量到当前位置的字符串作为表达式的一部分进行存储,并重新定义偏移量,然后继续寻找结束标记,即37、38、39行所实现效果,如果不存在转义字符就认为该结束标记字符串就是结束标记,就需要把偏移量到这个字符串中间的子串作为表达式进行处理,并直接跳出循环进行下一步处理,即41、42、43行代码。
  第七步:从46到53行代码,主要是构建需要返回的数据,并重新赋值偏移量offset,为解析下一个占位符做准备。其中else中需要根据具体处理器,进行返回指定的数据。
  第七步:第56行代码是重置start开始位置,进行下个参数的解析。
  第八步:从58到61行,主要是处理需要返回的结果字符串。

3、PropertyParser Property解析器

  PropertyParser主要是用于解析配置文件中<properties>元素。所以学习PropertyParser源码之前,首先需要保证对mybatis配置文件中的<properties>元素有足够的了解,如果需要了解可以去官方文档学习-传送门。其中,从 MyBatis 3.4.2 开始,新增了为占位符添加默认值的功能,详情可以参考官方文档,下面截图是官方文档的介绍:
在这里插入图片描述

  • 字段和构造函数
  private static final String KEY_PREFIX = "org.apache.ibatis.parsing.PropertyParser.";

  public static final String KEY_ENABLE_DEFAULT_VALUE = KEY_PREFIX + "enable-default-value";

  public static final String KEY_DEFAULT_VALUE_SEPARATOR = KEY_PREFIX + "default-value-separator";

  private static final String ENABLE_DEFAULT_VALUE = "false";
  private static final String DEFAULT_VALUE_SEPARATOR = ":";

  private PropertyParser() {
    // Prevent Instantiation
  }

  上面定义的几个常量主要是为占位符添加默认值的功能服务,KEY_PREFIX(定义属性时的默认前缀)、KEY_ENABLE_DEFAULT_VALUE (是否开启默认值功能,默认不开启)、KEY_DEFAULT_VALUE_SEPARATOR (是否修改默认值的分隔符,默认是“:”)、ENABLE_DEFAULT_VALUE (是否开启默认值功能的默认值,false)、DEFAULT_VALUE_SEPARATOR (默认分隔符,“:”)。构造函数是一个私有方法,说明该类不可以进行实例化,该类主要是一个工具类,所有方法都是静态方法,所以也不需要进行实例化。

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

  实现代码片段的解析处理工作,其中参数string是需要解析的代码片段,variables是可能需要填充的参数集合。该方法主要是定义了一个处理代码片段的处理器,然后交由GenericTokenParser实例对象,完成最终的解析工作。其中第2行是初始化一个标记处理器实例VariableTokenHandler(内部类),该实例就是用来完成真正的处理工作。第3行,初始化了一个GenericTokenParser实例,并设置开始标记为"${", 结束标记为"}",处理器为VariableTokenHandler实例。第4行,就是通过调用GenericTokenParser 的parse()方法实现代码片段的解析工作。

  • 内部类(VariableTokenHandler)
      该内部类实现了TokenHandler接口,是一个标记处理器的实现类。PropertyParser类中的解析处理工作,就是由该内部类来完成的。

  • 字段和构造器

    private final Properties variables;
    private final boolean enableDefaultValue;
    private final String defaultValueSeparator;

    private VariableTokenHandler(Properties variables) {
      this.variables = variables;
      this.enableDefaultValue = Boolean.parseBoolean(getPropertyValue(KEY_ENABLE_DEFAULT_VALUE, ENABLE_DEFAULT_VALUE));
      this.defaultValueSeparator = getPropertyValue(KEY_DEFAULT_VALUE_SEPARATOR, DEFAULT_VALUE_SEPARATOR);
    }

    private String getPropertyValue(String key, String defaultValue) {
      return (variables == null) ? defaultValue : variables.getProperty(key, defaultValue);
    }

  包括了三个字段属性,其中,variables定义的是存储参数集合的变量,enableDefaultValue定义是否开启占位符默认值,defaultValueSeparator定义占位符默认值的分隔符。getPropertyValue()方法主要是从variables变量中获取指定参数的方法。构造函数,提供了初始化三个字段属性的方法,enableDefaultValue和defaultValueSeparator如果没有被配置,就使用默认值。

  • handleToken方法实现
   @Override
    public String handleToken(String content) {
      if (variables != null) {
        String key = content;
        if (enableDefaultValue) {
          final int separatorIndex = content.indexOf(defaultValueSeparator);
          String defaultValue = null;
          if (separatorIndex >= 0) {
            key = content.substring(0, separatorIndex);
            defaultValue = content.substring(separatorIndex + defaultValueSeparator.length());
          }
          if (defaultValue != null) {
            return variables.getProperty(key, defaultValue);
          }
        }
        if (variables.containsKey(key)) {
          return variables.getProperty(key);
        }
      }
      return "${" + content + "}";
    }

  该方法主要用于占位符的参数处理。首先,判断variables变量是否有值,如果为null,则返回 “${” + content + "}"字符串,否则,继续执行;然后判断是否开启了占位符默认值的功能,如果开启,就执行4到14行的代码,判断是否有默认值,如果有默认值就解析对应的key和默认值defaultValue,然后返回key对应的参数值;如果不开启默认值功能,就直接从variables获取key对应的参数值并返回。

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

姠惢荇者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值