在项目中有用格式化数据(json,xml)来存储规则配置,存储数据库为mysql。且规则项目中有复制功能(导出sql到文件,上传sql文件写到数据库);出现配置好的数据复制后json解析失败。
origin db:
[{"name":"nA","id":"idA","evals":"[{\"name\":\"xa\",\"type\":\"tA\",\"operation\":\"idAdd\",\"domainArea\":[\\\"20181010\\\",\\\"20201010\\\"]}]"]
复制后:
[{"name":"nA","id":"idA","evals":"[{"name":"xa","type":"tA","operation":"idAdd","domainArea":[\"20181010\",\"20201010\"]}]"]
复制后数据查出后Json转换失败。
经测试:
1、jvm 导出到.sql文件,转义字符未变。
2、.sql文件加载到 jvm,转义字符未变。
3、mysql:
3.1使用jdbc mybatis prepareStatement setString,插入的数据转义字符未变。应是prepareStatement 在setString 对字符串加了转义。
3.2 使用jdbc statement.execute(sql),插入的数据转义字符减少。
CREATE TABLE TEST_STR(
ID INT(10) NOT NULL AUTO_INCREMENT,
STR VARCHAR(32) DEFAULT NULL,
PRIMARY KEY (ID)
) engine=Innodb DEFAULT CHARSET=UTF-8;
insert into test_str(str)values('\"');
insert into test_str(str)values('\\"');
insert into test_str(str)values('\\\"'); --\"
insert into test_str(str)values('\\\\"');
insert into test_str(str)values('\\\\\"'); -- \\"
insert into test_str(str)values('\\\\\\"');
insert into test_str(str)values('\\\\\\\"'); -- \\\"
select * from test_str;
结果为
1 ”
2 \"
3 \"
4 \\"
5 \\"
6 \\\"
7 \\\"
经测试,因此方案是生成了完整的sql 语句,使用jdbcTemplate,prepareStatement execute(sql) 都没有找到API设置转义字符不转义的。
所以考虑在java中先对insert sql 加转义,达到statement.execute(insertSql) 后db内的转义字符与复制前一致。
本来以为是replaceAll就行,但经测试发现结果挺有意思的,
public static void testEscape(){
String s = "\\";
/*
java 中字符串未区别正则与字符串,所以写\到字符串时需转义,"\\\\" => \\ , replaceAll 参数为正则,需转义,所以\\=>\。
即replaceAll("\\\\","\\\\\\\\")此处替换的实际字符为\.
*/
String str1 = s.replaceAll("\\\\","\\\\\\\\");
System.out.println(String.format("str1:%s",str1));
String str2 = s.replaceAll("\\\\","$0$0");
System.out.println(String.format("str2:%s",str2));
}
public static void testEscape2(){
String s = "\\\""; // => \"
System.out.println(String.format("s:%s",s));
String str1 = s.replaceAll("\\\\","\\\\\\\\");
System.out.println(String.format("str1:%s",str1));
String str2 = s.replaceAll("\\\\","$0$0");
System.out.println(String.format("str2:%s",str2));
}
public static void testEscape3(){
String s = "AA\\BB\\\\CC\\\"aa\\\\\"bb\\\\\\\\\"";
System.out.println(String.format("s:%s",s));
String str1 = s.replaceAll("(\\\\+)(\")","$1$1$2");
System.out.println(String.format("str1:%s",str1));
}
public static void testEscape4(){
String s = "AA\\BB|CC\\\\|"; // 在字符串中| 不是特殊字符,但在正则表达式中是特殊字符。
System.out.println(String.format("s:%s",s));
String str1 = s.replaceAll("(\\\\+)(\\|)","$1$1$2");
System.out.println(String.format("str1:%s",str1));
}
查看源码replaceAll方法的实现如下:
public String replaceAll(String regex, String replacement) {
return Pattern.compile(regex).matcher(this).replaceAll(replacement);
}
其中Pattern.compile(regex).matcher(this), 返回的是一个Matcher对象。
先简单介绍java.util.regex.Matcher类,是通过解释 Pattern 对 字符序列执行匹配操作的引擎,其中持有对当前Pattern对象和当前String对象的引用。
执行一次调用其find方法,即对字符串执行一次从左向右的以Pattern为正则的匹配,并记录下匹配结果字符串的开始和结束位置索引,以及更新一个记录当前匹配结果的分组groups。
进入Matcher类的replaceAll方法,继续查看源码,
public String replaceAll(String replacement) {
// 对当前Matcher类进行重置,即对其中记录匹配结果的开始和结束位置索引,以及分组信息重置
reset();
// 执行第一次搜索
boolean result = find();
if (result) {
// 第一次搜索匹配成功
// 用于记录最终的替换结果字符串
StringBuffer sb = new StringBuffer();
// 循环搜索
do {
// *重点在此方法内:用于将从上一次匹配子字符串的下一个索引位置开始,到当前匹配的子字符串的结束索引位置的所有字符 append到字符串sb中
// 有点绕,可以暂时跳过,下面会对该方法进一步分析
appendReplacement(sb, replacement);
result = find();
} while (result);
// 将从最后一次匹配子字符串的下一个索引位置,到字符串的结尾的所有字符append到字符串sb中
appendTail(sb);
return sb.toString();
}
return text.toString();
}
继续南下,进入Matcher类的appendReplacement方法,
public Matcher appendReplacement(StringBuffer sb, String replacement) {
...省略部分代码
// 用于跟踪replacement字符串的索引
int cursor = 0;
String s = replacement;// Java api源码也有垃圾代码啊,呵呵 (s局部变量并未在后续代码中被使用)
// 对当前匹配到子字符串替换后的结果字符串
StringBuffer result = new StringBuffer();
// 遍历replacement字符串
while (cursor < replacement.length()) {
char nextChar = replacement.charAt(cursor);
if (nextChar == '\\') {
// 重点1:当字符为\时,跳过,并获取其后面的字符,追加到result
cursor++;
nextChar = replacement.charAt(cursor);
result.append(nextChar);
cursor++;
} else if (nextChar == '$') {
// 重点2:当字符为$时,跳过,并获取其后面的数值,并以此如果$后面第一个不为数字则抛异常,
// Skip past $
cursor++;
// The first number is always a group
int refNum = (int)replacement.charAt(cursor) - '0';
// 此处代码用于计算$符号后的数值,数值结果赋予refNum
...省略部分代码
// group(refNum) 用于获取正则表达式第refNum个分组表示的字符串,不详说了
if (group(refNum) != null)
result.append(group(refNum)); // 追加到result
} else {
// 当前字符不为\ 或 $ 则直接追加到result
result.append(nextChar);
cursor++;
}
}
// 将从上一次匹配的子字符串的结尾索引,到当前匹配的第一个字符串索引的字符串追加到sb
// lastAppendPosition参数为上一次执行appendReplacement方法最后追加的字符在原始字符串中的索引位置。
// first 参数为当前待替换的子字符串的首个字符在原始字符串中的索引位置
sb.append(getSubSequence(lastAppendPosition, first));
// 将当前配置子字符串替换后的结果字符串追加到sb
sb.append(result.toString());
// 更新lastAppendPosition,供下一个匹配执行appendReplacement方法使用
lastAppendPosition = last;
/*
到此, sb中追加了当前匹配的子字符串与前一次匹配子字符串中间的字符,以及当前匹配子字符串被替换后的字符串
*/
return this;
}