解决使用MyBatis处理含有$的变量时报IllegalArgumentException Illegal group reference

现象

在一个GET请求中,输入参数包含$符号,然后后台报错。

复现

1
2
3
4
5
6
7
8
9
curl 'http://localhost:9999/xxxx/drivers?page=1&size=20&realname=$' \
-H 'Accept-Encoding: gzip, deflate' \
-H 'Accept-Language: zh,en;q=0.9,ja;q=0.8,zh-TW;q=0.7,fr;q=0.6,zh-CN;q=0.5' \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36' \
-H 'Accept: application/json, text/plain, */*' \
-H 'userId: 453' \
-H 'Connection: keep-alive' \
-H 'token: 1e6966ec2fafdbbd53ef53124cc3e5ae' \
--compressed

报错截图

在这里插入图片描述

原因

这个现象与mybatis无关,因为mybatis对$符号的操作主要集中在GenericTokenParser.java中,而这些操作都是以\${出现,因此可以认为mybatis不处理单独的$符号。如下:
在这里插入图片描述
在interceptor中,有一段打印将要执行的sql语句相关信息,根据调试结果,锁定了这个方法出问题所在,如下:

1
2
Object obj = boundSql.getAdditionalParameter(propertyName);
sql = sql.replaceFirst("\\?", getParameterValue(obj));

因此问题转化成了下面的问题:

1
2
3
4
5
6
// 有问题的语句
String sql = "er WHERE realname like ? AND deleted = ?";
sql = sql.replaceFirst("\\?", "\\%$%");
System.out.println(sql);
// 修改
sql = sql.replaceFirst("\\?", Matcher.quoteReplacement("\\%$%"));

$在replaceFirst(String regex, String replacement)中的使用

网上关于这个方法的使用,普遍停留在regex,对replacement的解释少之又少,基本上没有。因为$符号在replacement中导致操作报错,因此,这个$肯定是有某种特殊的意义的。打开源代码,一直跟踪到抛出错误的地方。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
/**
* Processes replacement string to replace group references with
* groups.
*/
private StringBuilder appendExpandedReplacement(String replacement, StringBuilder result) {
int cursor = 0;
while (cursor < replacement.length()) {
char nextChar = replacement.charAt(cursor);
// 可以看出,转义符号使用起来也需要小心。如果在最后面,又会报错。
if (nextChar == '\\') {
cursor++;
if (cursor == replacement.length())
throw new IllegalArgumentException(
"character to be escaped is missing");
// 对转义符的操作比较简单,只是先跳过转义符,然后将下一个字符拷贝
nextChar = replacement.charAt(cursor);
result.append(nextChar);
cursor++;
// 如果是$,那么后面要么是数字,要么是{
} else if (nextChar == '$') {
// Skip past $
cursor++;
// Throw IAE if this "$" is the last character in replacement
if (cursor == replacement.length())
throw new IllegalArgumentException(
"Illegal group reference: group index is missing");
nextChar = replacement.charAt(cursor);
int refNum = -1;
if (nextChar == '{') {
cursor++;
StringBuilder gsb = new StringBuilder();
// 先读出{}中的内容
while (cursor < replacement.length()) {
nextChar = replacement.charAt(cursor);
if (ASCII.isLower(nextChar) ||
ASCII.isUpper(nextChar) ||
ASCII.isDigit(nextChar)) {
gsb.append(nextChar);
cursor++;
} else {
break;
}
}
if (gsb.length() == 0)
throw new IllegalArgumentException(
"named capturing group has 0 length name");
if (nextChar != '}')
throw new IllegalArgumentException(
"named capturing group is missing trailing '}'");
String gname = gsb.toString();
if (ASCII.isDigit(gname.charAt(0)))
throw new IllegalArgumentException(
"capturing group name {" + gname +
"} starts with digit character");
//{}中包含的内容,是分组的名字,分组存在,直接拿来分组的值,不存在则报错
if (!parentPattern.namedGroups().containsKey(gname))
throw new IllegalArgumentException(
"No group with name {" + gname + "}");
refNum = parentPattern.namedGroups().get(gname);
cursor++;
} else {
// The first number is always a group
refNum = nextChar - '0';
// 只接受0-9的数字,也就是分组的序号
if ((refNum < 0) || (refNum > 9))
throw new IllegalArgumentException(
"Illegal group reference");
cursor++;
// Capture the largest legal group string
boolean done = false;
while (!done) {
if (cursor >= replacement.length()) {
break;
}
int nextDigit = replacement.charAt(cursor) - '0';
if ((nextDigit < 0) || (nextDigit > 9)) { // not a number
break;
}
int newRefNum = (refNum * 10) + nextDigit;
if (groupCount() < newRefNum) {
done = true;
} else {
refNum = newRefNum;
cursor++;
}
}
}
// Append group
if (start(refNum) != -1 && end(refNum) != -1)
result.append(text, start(refNum), end(refNum));
} else {
result.append(nextChar);
cursor++;
}
}
return result;
}

可以看出,转义符号使用起来也需要小心。如果在最后面,又会报错,对转义符的操作比较简单,只是先跳过转义符,然后将下一个字符拷贝;如果是$,那么后面要么是数字,要么是{。对$符号的处理,如果后面是{,则先读出{}中的内容,也就是分组的名字,然后查询分组是否存在,存在则直接拿来分组的值,不存在则报错,如果后面是数字,就去拿第n个分组的值。

过程如上,但是网上基本上没有上述内容的示例,根据上面的简要分析,做出了下面简要的demo:

1
2
3
4
5
String Str = new String("Welcome to Tutoririalspoint.combb");
System.out.print("Return Value :" );
System.out.println(Str.replaceFirst("Tuto(.*)als(?<hi>.*)(bb)", "$1AMROOD}$2--$3--${hi}--xx"));
// 输出:
// Return Value :Welcome to ririAMROOD}point.com--bb--point.com--xx

给分组命名确实以前没尝试过,参考:https://stackoverflow.com/questions/415580/regex-named-groups-in-java。

解决办法

替换后的方法如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private String showSql(Configuration configuration, BoundSql boundSql) {
Object parameterObject = boundSql.getParameterObject();
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
if (parameterMappings.size() > 0 && parameterObject != null) {
TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(getParameterValue(parameterObject)));

} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
for (ParameterMapping parameterMapping : parameterMappings) {
String propertyName = parameterMapping.getProperty();
if (metaObject.hasGetter(propertyName)) {
Object obj = metaObject.getValue(propertyName);
sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(getParameterValue(obj)));
} else if (boundSql.hasAdditionalParameter(propertyName)) {
Object obj = boundSql.getAdditionalParameter(propertyName);
sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(getParameterValue(obj)));
}
}
}
}
return sql;
}

参考:
https://github.com/abel533/Mapper/issues/30

解决使用MyBatis处理含有$的变量时报IllegalArgumentException Illegal group reference

https://eucham.me/2019/03/26/945fdb518121.html

作者

遇寻

发布于

2019-03-26

更新于

2021-02-09

许可协议

评论