Java 一步一步实现高逼格的字符串替换工具(二)
上一篇实现了一个用于字符串替换的方法,主要是利用 正则 + jdk的字符串替换,本篇则会再之前的基础上走一个扩展
1. 之前的方法存在的问题
先把上一篇的两个方法贴下,研究下有什么问题,然后再看下可以怎么去改进
// 获取patter的过程较为负责,这里初始化时,做一次即可
private static Pattern pattern;
static {
pattern = Pattern.compile("((?<=\\{)([a-zA-Z_]{1,})(?=\\}))");
}
/**
* 字符串替换, 将 {} 中的内容, 用给定的参数进行替换
*
* @param text
* @param params
* @return
*/
public static String format(String text, Map<String, Object> params) {
// 把文本中的所有需要替换的变量捞出来, 丢进keys
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
String key = matcher.group();
// text = StringUtils.replace(text, "{" + key + "}", params.get(key) + "");
text = text.replaceAll("\\{" + key + "\\}", params.get(key) + "");
}
return text;
}
public static List<String> batchFormat(String text, List<Map<String, Object>> params) {
List<String> keys = new ArrayList<>();
// 把文本中的所有需要替换的变量捞出来, 丢进keys
Matcher matcher = pattern.matcher(text);
int tempIndex = 0;
while (matcher.find()) {
String key = matcher.group();
if (keys.contains(key)) {
continue;
}
text = StringUtils.replace(text, key, tempIndex + "");
tempIndex++;
keys.add(key);
}
List<String> result = new ArrayList<>(params.size());
String[] tempParamAry = new String[keys.size()];
for (Map<String, Object> param : params) {
for (int i = 0; i < keys.size(); i++) {
tempParamAry[i] = param.get(keys.get(i)) + "";
}
result.add(MessageFormat.format(text, tempParamAry));
}
return result;
}
一个单个替换,一个批量替换,我们一个一个分析,首先看
1. public static String format(String text, Map<String, Object> params)
- 正则替换效率问题
-
String.replaceAll()
这个也是走的正则替换, 从我们的业务场景来看,有更好的替换
apache的 commons-lang 有个 StringUtils
工具类, 我们可以用里面的 replace
方法进行代替, 上面注释的就是我们推荐的使用方式
2. public static List<String> batchFormat(String text, List<Map<String, Object>> params)
这个的实现原理比较简单
- 先用正则把所有需要替换的捞出来, 放在列表中, 并将坑位用数字来替换
- 然后使用
MessageFormat.format
进行替换
这个流程比较清晰简单,对于 MessageFormat.format
却发现一个诡异的问题,当text中包含单引号时,后面的不会被替换, 测试case如下
public String replace(String text, Object... args) {
return MessageFormat.format(text, args);
}
@Test
public void testReplace2() {
String text = "hello {0}, welcome to {1}!";
String user = "Lucy";
String place = "China";
String ans = replace(text, user, place);
System.out.println(ans);
text = "hello {0}, welcome to {2} ! what's a good day! today is {1}!";
ans = replace(text, "Lucy", new Date(), "HangZhou");
System.out.println(ans);
}
输出如下:
debug到源码去看下,然后发现在生成 MessageFormat
对象的实现中,单引号内部有特殊用途,认为两个单引号之间的为一个整体,不做替换
String text = "hello {0}, welcome to {2} ! what's {0}' a good day! today is {1}!";
String ans = MessageFormat.format(text, "Lucy", new Date(), "HangZhou");
System.out.println(ans); // 输出 hello Lucy, welcome to HangZhou ! whats {0} a good day! today is 17-3-28 下午5:54!
2. 改进++
对上面的正则获取key,然后再调用
MessageFormat.format()
的方式不满意,特别是后者的潜规则还不少,我们要实现一个纯粹的,高效的,可扩展的替换工具,应该这么玩?
既然已经深入了MessageFormat
的源码,那么就简单了,把他的实现逻辑抠出来,砍掉各种潜规则,我们自己来实现即可
新版的设计思路:
- 首先将文本进行拆分
- 以`{}`作为分割, 大括号前后的各自作为新的`Word`; 大括号内的也作为独立的`Word`
- 将拆分的`Word` 塞入一个数组中
- 遍历上面的数组,替换变量
- 返回想要的结果
实现如下:
public static String formatV2(String text, Map<String, Object> params) {
StringBuilder stringBuilder = new StringBuilder();
int startIndex = 0;
for (int i = 0; i < text.length(); i++) {
if (text.charAt(i) == '{') {
if (startIndex > 0) {
stringBuilder.append(text.substring(startIndex, i));
}
startIndex = i + 1;
continue;
}
if (text.charAt(i) == '}') {
stringBuilder.append(params.get(text.substring(startIndex, i)));
startIndex = i + 1;
}
}
if (startIndex < text.length()) {
stringBuilder.append(text.substring(startIndex));
}
return stringBuilder.toString();
}
/**
* 规定大括号中不能再次出现大括号, 即不允许迭代替换
*
* @param text
* @param paramsList
* @return
*/
public static List<String> batchFormatV2(String text, List<Map<String, Object>> paramsList) {
List<Word> textList = splitText2words(text);
List<String> result = new ArrayList<>();
StringBuilder stringBuilder;
for (Map<String, Object> params: paramsList) {
stringBuilder = new StringBuilder();
for (Word word: textList) {
stringBuilder.append(replaceWord(word, params));
}
result.add(stringBuilder.toString());
}
return result;
}
private static String replaceWord(Word word, Map<String, Object> params) {
if (word.getIsReplaceKey()) {
return params.get(word.getWord()) + "";
} else {
return word.getWord();
}
}
/**
* 将文本根据{}进行分割
* <p/>
* 如: {place} is a good place, what do you think {user}?
* 分割:
* - Word("place", true)
* - Word(" is a good place, what do you think ", false)
* - Word("user", true)
* - Word("?", false)
*
* @param text
* @return
*/
private static List<Word> splitText2words(String text) {
List<Word> textList = new ArrayList<>();
int startIndex = 0;
for (int i = 0; i < text.length(); i++) {
if (text.charAt(i) == '{') {
if (startIndex > 0) {
textList.add(new Word(text.substring(startIndex, i), false));
}
startIndex = i + 1;
continue;
}
if (text.charAt(i) == '}') {
textList.add(new Word(text.substring(startIndex, i), true));
startIndex = i + 1;
}
}
if (startIndex < text.length()) {
textList.add(new Word(text.substring(startIndex), false));
}
return textList;
}
private static class Word {
private String word;
/**
* true 则表示保存的是需要被替换的值
*/
private Boolean isReplaceKey;
public Word(String word, Boolean replaceKey) {
this.word = word;
this.isReplaceKey = replaceKey;
}
public String getWord() {
return word;
}
public Boolean getIsReplaceKey() {
return isReplaceKey;
}
@Override
public String toString() {
return "Word{" +
"word='" + word + '\'' +
", isReplaceKey=" + isReplaceKey +
'}';
}
}
至此,一个算是不错的文本替换工具类出来了,想想还有什么可以改进的地方么?
简单的字符串进行替换有点low,如果我想在 {}
中执行一些表达式可以怎么玩 ?
下一篇则将精力主要集中在 {}
中value替换的玩法上