一、属性
PropertyParser
是一个包装工具类,没有自定义属性,依赖于VariableTokenHandler
和GenericTokenParser
来实现解析形如${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关系图(简化版)如下:
四、核心处理类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、存在问题
从测试结果中可以发现,对于转义起始占位符\\${
,处理之后变成了${
,但是对转义结束占位符\\}
,处理完了之后却没有变成}
,没有保持一致,个人觉得这里处理得让人不舒服,修改以下源码,有两处地方,如图所示:
修改后的代码如下:
// 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.java,GenericTokenParser.java
[修改源码] GenericTokenParserUpd.java
[测试案例] GenericTokenParserTest.java
网友评论