美文网首页MyBatis源码剖析
[MyBatis源码详解 - 解析器模块 - 组件三] Prop

[MyBatis源码详解 - 解析器模块 - 组件三] Prop

作者: 小胡_鸭 | 来源:发表于2019-07-25 16:59 被阅读0次
一、属性

  PropertyParser是一个包装工具类,没有自定义属性,依赖于VariableTokenHandlerGenericTokenParser来实现解析形如${database.driver}的带占位符变量的字符串的变量值。

二、构造函数
private PropertyParser() {}

// 解析表达式调用本方法
public static String parse(String string, Properties variables) {
    VariableTokenHandler handler = new VariableTokenHandler(variables);
    GenericTokenParser parser = new GenericTokenParser("${", "}", handler);     
    return parser.parse(string);
}

  空实现且声明为私有,在源码中这样的例子非常常见,用来防止工具类被创建对象实例,一般工具类中的方法都是声明为static,使用类型即可使用方法。

三、静态内部类

  PropertyParser中定义了一个静态内部类VariableTokenHandler,源码如下:

    private static class VariableTokenHandler implements TokenHandler {
        private Properties variables;
        
        public VariableTokenHandler(Properties variables) {
            this.variables = variables;
        }
        
        @Override
        public String handleToken(String context) {
            if (variables != null && variables.containsKey(context)) {
                return variables.getProperty(context);
            }
            
            return "${" + context + "}";
        }
        
    }

  功能从字面上就可以理解变量占位符处理器,它做的事情很简单,比如${database.driver},去掉占位符之后拿到里面的变量名database,driver,然后从variables中拿到对应的变量值比如com.mysql.jdbc.Driver,如果没找到把占位符重新套上去。
  VariableTokenHandler实现了TokenHandler接口,TokenHandler有四个实现类,UML关系图(简化版)如下:

TokenHandler_implements.png
四、核心处理类GenericTokenParser
public class GenericTokenParser {
    
    private final String openToken;      // 占位符的开始标记,eg: ${、#{
    private final String closeToken;     // 占位符的结束标记,eg: }
    private final TokenHandler handler;  // 占位符处理器
    
    public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
        this.openToken = openToken;     
        this.closeToken = closeToken;
        this.handler = handler;
    }
      
    // code
}

  openToken:占位符的开始标记
  closeToken:占位符的结束标记
  handler:占位符处理器
  PropertyParser处理的是形如${database.driver}的占位变量,从PropertyParser.parse()可以看出,openToken${closeToken}handler是一个VariableTokenHandler对象。
  核心方法parse()源码如下:

    public String parse(String text) {
        final StringBuilder builder = new StringBuilder();
        final StringBuilder expression = new StringBuilder();
        if (text != null & text.length() > 0) {
            char[] src = text.toCharArray();               // 将字符串转化成一个字符数组,方便解析占位符位置
            int offset = 0;                                // 初始占位符偏移量,随着对数组的遍历和查找会不断变化,偏移量之前的字符为已经处理过的数据
            int start = text.indexOf(openToken, offset);   // 查找占位符开始标记位置
            while (start > -1) {                           // 大于-1证明找到了占位符标记,当对文本查找到最后一个占位符后再次搜索时也返回-1
                // 有时候节点文本值可能有带转义占位符,表示虽然我是个占位符,但我不是用来占位用的,eg: \\${ 表示普通的"${"
                // PS. Java中'\\'实际上只代表一个反斜杠,为了对其进行转义,所以又加了个反斜杠
                if (start > 0 && src[start - 1] == '\\') {
                    // eg: text = "123\\${var}"  => "123${var}"
                    // offset = 0, start = 4, builder.append("123").append("${")
                    builder.append(src, offset, start - offset - 1).append(openToken);
                    // 偏移量往前移,如上面的例子,处理完之后偏移量就变成4 + 2 = 6,第6个字符是'v'
                    offset = start + openToken.length();
                } 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] == '\\') {
                            expression.append(src, offset, end - offset - 1).append(closeToken);
                            offset = end + closeToken.length();
                            end = text.indexOf(closeToken, offset);
                        } else {
                            // 将开始标记和结束标记之间的字符串追加到expression中保存
                            expression.append(src, offset, end - offset);
                            offset = end + closeToken.length();
                            break;
                        }
                    }
                    // 如果没有找到结束标记,默认是这种情况"${database.user"
                    if (end == -1) {
                        builder.append(src, start, src.length - start);
                        offset = src.length;
                    } else {
                        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();
    }

  本来只要找到开始标记和结束标记,取到中间的变量名就可以了,但是由于还要处理转义符,多层占位符嵌套的情况,逻辑变得比较复杂。
【源码分析】
  首先将带占位符变量字符串转化为char[],然后找到占位符开始标记${的位置,假如text中压根没有${,则不进入while循环,直接将text原封不动返回。
  假如找到了${,则进入while循环,先看if分支

if (start > 0 && src[start - 1] == '\\') {
    builder.append(src, offset, start - offset - 1).append(openToken);
    offset = start + openToken.length();
}

  通常我们会用反斜杠\转义某个特殊字符串,表示说这个字符你就把他当成普通字符,不要当成特殊字符,有可能输入的text"123\\${var}",对$加了转义,意味着不能将其当成占位符开始标记的一部分,builder存储的是返回的变量名,这里代码去掉了转义字符\,而且后面又没有非转义的${,所以"123\\${var}"最终会以123${var}的形式返回。
  如果顺利找到非转义的${,则进入下面的else分支

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] == '\\') {
            expression.append(src, offset, end - offset - 1).append(closeToken);
            offset = end + closeToken.length();
            end = text.indexOf(closeToken, offset);
        } else {
            // 将开始标记和结束标记之间的字符串追加到expression中保存
            expression.append(src, offset, end - offset);
            offset = end + closeToken.length();
            break;
        }
    }
}

  有可能我们的变量刚好是带占位符的,比如${test\\}},这样变量名会是test}(当然实际情况不会这么奇葩),解析占位结束符的流程跟起始符类似,接着解析}的循环会继续找,直到找到不带占位跟${配对匹配的};如果是常规的非转义},则直接进入while循环中的else分支,解析到的${和匹配的}中间的字符串即为变量名,进入下一段代码。

    // 如果没有找到结束标记,默认是这种情况"${database.user"
    if (end == -1) {
        builder.append(src, start, src.length - start);
        offset = src.length;
    } else {
        builder.append(handler.handleToken(expression.toString()));
        offset = end + closeToken.length();
    }

  如果从头到尾一直没有找到非转义的},则将${之后的字符串都附加到返回的builder中,正常情况下会拿到变量名,再由handler去从它的Properties属性中找到对应的变量值,一次占位符解析的流程到此结束,但是有时候可能一个字符串中包含了多对匹配的占位符,比如username:${database.username},password:${database.password},所以有了外面的解析起始占位符${while循环,流程如上面分析所示。

while (start > -1) { 
    // 一次解析匹配占位符的代码
    start = text.indexOf(openToken, offset);

}
if (offset < src.length) {
    builder.append(src, offset, src.length - offset);
}

  在开始占位符前可能有正常字符,同样在结束占位符前可能也有正常字符,比如aaa${test}bbb所以要将匹配到的最后一个}后的常规字符bbb也附加到builder中去。

五、测试案例
1、jdbc.properties配置文件
database.driver=com.mysql.jdbc.Driver
database.url=jdbc:mysql://localhost:3306/ssm?useSSL=false
database.username=root
database.password=root


2、测试案例
public class GenericTokenParserTest {
    public static void main(String[] args) throws Exception {
        // 1. 加载Properties文件生成Properties对象
        InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("jdbc.properties");
        Properties variables = new Properties();
        variables.load(inputStream);
        
        // 2. 解析占位符
        VariableTokenHandler handler = new VariableTokenHandler(variables);
        GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
        // 常规的情况,通常我们也都是这么用的
        System.out.println(parser.parse("${database.username}"));
        // 起始占位符前有常规字符串,结束占位符后又常规字符串
        System.out.println(parser.parse("haha${database.username}hehe"));
        // 演示对转义起始占位符的处理
        System.out.println(parser.parse("haha\\${database.username}hehe"));
        // 演示对转义结束占位符的处理
        System.out.println(parser.parse("haha${database.username\\}hehe"));
        // 演示对起始结束占位符都转义的处理
        System.out.println(parser.parse("haha\\${database.username\\}hehe"));
        // 演示有多对匹配的占位符的处理
        System.out.println(parser.parse("username:${database.username},password:${database.password}"));
    }
    
    // 为了方便测试,将私有静态内部类定义在测试案例中
    private static class VariableTokenHandler implements TokenHandler {
        private Properties variables;
        
        public VariableTokenHandler(Properties variables) {
            this.variables = variables;
        }
        
        @Override
        public String handleToken(String context) {
            if (variables != null && variables.containsKey(context)) {
                return variables.getProperty(context);
            }
            
            return "${" + context + "}";
        }
        
    }
}


3、测试结果
root
haharoothehe
haha${database.username}hehe
haha${database.username\}hehe
haha${database.username\}hehe
username:root,password:root


4、存在问题

  从测试结果中可以发现,对于转义起始占位符\\${,处理之后变成了${,但是对转义结束占位符\\},处理完了之后却没有变成},没有保持一致,个人觉得这里处理得让人不舒服,修改以下源码,有两处地方,如图所示:

GenericTokenParser_upd.png
  修改后的代码如下:
  // code
                    // 如果没有找到结束标记,默认是这种情况"${database.user"
                    if (end == -1) {
                        /**
                         *  TODO: 针对"${database.user\\}"这种情况
                         *  解析为"${database.user}"
                         */
                        if (expression.length() != 0) {
                            builder.append(openToken).append(expression);
                        } else {
                            builder.append(src, start, src.length - start);
                            offset = src.length;
                        }
                    } else {
                        builder.append(handler.handleToken(expression.toString()));
                        offset = end + closeToken.length();
                    }
                }
                start = text.indexOf(openToken, offset);
            }
            /**
             * TODO: 针对"aaa\\${xx\\}"这种情况
             * 解析为"${xx}"
             */
            while (true) {
                int end = text.indexOf(closeToken, offset);
                if (end > -1) {
                    if (end > offset && src[end - 1] == '\\') {
                        builder.append(src, offset, end - offset - 1).append(closeToken);
                        offset = end + closeToken.length();
                    } else {
                        builder.append(src, offset, end - offset).append(closeToken);
                        offset = end + closeToken.length();
                    }
                } else {
                    break;
                }
            }
            if (offset < src.length) {
                builder.append(src, offset, src.length - offset);
            }
        }

  修改后代码新建一个类GenericTokenParserUpd,测试代码如下:

        // 3. 测试修改后的代码
        GenericTokenParserUpd parser2 = new GenericTokenParserUpd("${", "}", handler);
        // 常规的情况,通常我们也都是这么用的
        System.out.println(parser2.parse("${database.username}"));
        // 起始占位符前有常规字符串,结束占位符后又常规字符串
        System.out.println(parser2.parse("haha${database.username}hehe"));
        // 演示对转义起始占位符的处理
        System.out.println(parser2.parse("haha\\${database.username}hehe"));
        // 演示对转义结束占位符的处理
        System.out.println(parser2.parse("haha${database.username\\}hehe"));
        // 演示对起始结束占位符都转义的处理
        System.out.println(parser2.parse("haha\\${database.username\\}hehe"));
        // 演示有多对匹配的占位符的处理
        System.out.println(parser2.parse("username:${database.username},password:${database.password}"));

  试下测试结果,达到想要的目的。

root
haharoothehe
haha${database.username}hehe
haha${database.username}hehe
haha${database.username}hehe
username:root,password:root


六、我的github(注释源码、测试案例)

   [仓库地址] huyihao/mybatis-source-analysis
   [注释源码] PropertyParser.javaGenericTokenParser.java
   [修改源码] GenericTokenParserUpd.java
   [测试案例] GenericTokenParserTest.java

相关文章

网友评论

    本文标题:[MyBatis源码详解 - 解析器模块 - 组件三] Prop

    本文链接:https://www.haomeiwen.com/subject/hswcrctx.html