美文网首页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