1. Overview
1.概述
Java standard library provides the String.format() method to format a template-based string, such as: String.format(“%s is awesome”, “Java”).
Java标准库提供了String.format()方法来格式化一个基于模板的字符串,例如。String.format(“%s is awesome”, “Java”)。
In this tutorial, we’ll explore how to make string formatting support named parameters.
在本教程中,我们将探讨如何使字符串格式化支持命名参数。
2. Introduction to the Problem
2.对问题的介绍
The String.format() method is pretty straightforward to use. However, when the format() call has many arguments, it gets difficult to understand which value will come to which format specifier, for example:
String.format()方法的使用相当直接。然而,当format()调用有许多参数时,就很难理解哪个值会到哪个格式指定器,比如说。
Employee e = ...; // get an employee instance
String template = "Firstname: %s, Lastname: %s, Id: %s, Company: %s, Role: %s, Department: %s, Address: %s ...";
String.format(template, e.firstName, e.lastName, e.Id, e.company, e.department, e.role ... )
Further, it’s error-prone when we pass those arguments to the method. For instance, in the example above, we put e.department ahead of e.role by mistake.
此外,当我们把这些参数传递给方法时,是很容易出错的。例如,在上面的例子中,我们错误地把e.department放在了e.role前面。
So, it would be great if we could use something like named parameters in the template and then apply formatting through a Map that holds all parameter name->value mappings:
因此,如果我们能在模板中使用类似命名参数的东西,然后通过持有所有参数name->value映射的Map应用格式化,那就太好了。
String template = "Firstname: ${firstname}, Lastname: ${lastname}, Id: ${id} ...";
ourFormatMethod.format(template, parameterMap);
In this tutorial, we’ll first look at a solution using a popular external library, which can solve most cases of this problem. Then, we’ll discuss an edge case that breaks the solution.
在本教程中,我们将首先看看使用一个流行的外部库的解决方案,它可以解决这个问题的大多数情况。然后,我们将讨论一个打破该解决方案的边缘案例。
Finally, we’ll create our own format() method to cover all cases.
最后,我们将创建我们自己的format()方法来涵盖所有情况。
For simplicity, we’ll use unit test assertions to verify if a method returns the expected string.
为了简单起见,我们将使用单元测试断言来验证一个方法是否返回预期的字符串。
It’s also worth mentioning that we’ll only focus on simple string formats (%s) in this tutorial. Other format types, such as date, number, or a format with defined width and precision, are not supported.
还值得一提的是,我们在本教程中只关注简单的字符串格式(%s)。其他格式类型,如日期、数字或具有定义宽度和精度的格式,都不被支持。
3. Using StrSubstitutor From Apache Commons Text
3.使用StrSubstitutor来自Apache Commons Text
Apache Commons Text library contains many handy utilities for working with strings. It ships with StrSubstitutor, which allows us to do string substitution based on named parameters.
Apache Commons Text库包含许多用于处理字符串的便捷工具。它带有StrSubstitutor,它允许我们根据命名参数进行字符串替换。
First, let’s add the library as a new dependency to our Maven configuration file:
首先,让我们把该库作为一个新的依赖项添加到我们的Maven配置文件中。
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.9</version>
</dependency>
Of course, we can always find the latest version at the Maven Central repository.
当然,我们可以随时在Maven Central仓库找到最新版本。
Before we see how to use the StrSubstitutor class, let’s create a template as an example:
在我们看到如何使用StrSubstitutor类之前,让我们创建一个模板作为例子。
String TEMPLATE = "Text: [${text}] Number: [${number}] Text again: [${text}]";
Next, let’s create a test to build a string based on the template above using StrSubstitutor:
接下来,让我们创建一个测试,使用StrSubstitutor在上面的模板基础上构建一个字符串。
Map<String, Object> params = new HashMap<>();
params.put("text", "It's awesome!");
params.put("number", 42);
String result = StrSubstitutor.replace(TEMPLATE, params, "${", "}");
assertThat(result).isEqualTo("Text: [It's awesome!] Number: [42] Text again: [It's awesome!]");
As the test code shows, we let params hold all name -> value mappings. When we call the StrSubstitutor.replace() method, apart from template and params, we also pass the prefix and the suffix to inform StrSubstitutor what a parameter consists of in the template. StrSubstitutor will search prefix + map.entry.key + suffix for parameter names.
正如测试代码所示,我们让params持有所有name -> value的映射。当我们调用StrSubstitutor.replace()方法时,除了template和params之外,我们还传递前缀和后缀来告知StrSubstitutor一个参数在模板中的组成。StrSubstitutor将搜索prefix + map.entry.key + suffix的参数名称。
When we run the test, it passes. So, it seems that StrSubstitutor solves the problem.
当我们运行测试时,它通过了。所以,似乎StrSubstitutor解决了这个问题。
4. An Edge Case: When Replacements Contain Placeholders
4.一个边缘案例 当替代物包含占位符时
We’ve seen that the StrSubstitutor.replace() test passes for our basic use case. However, some special cases are not covered by the test. For example, the parameter value may contain the parameter name pattern “${ … }“.
我们已经看到,StrSubstitutor.replace()测试通过了我们的基本用例。然而,一些特殊情况并没有被测试所覆盖。例如,参数值可能包含参数名称模式”${ … }“。
Now, let’s test this case:
现在,让我们测试一下这个案例。
Map<String, Object> params = new HashMap<>();
params.put("text", "'${number}' is a placeholder.");
params.put("number", 42);
String result = StrSubstitutor.replace(TEMPLATE, params, "${", "}");
assertThat(result).isEqualTo("Text: ['${number}' is a placeholder.] Number: [42] Text again: ['${number}' is a placeholder.]");
In the test above, the value of the parameter “${text}” contains the text “${number}“. So, we’re expecting that “${text}” is replaced by the text “${number}” literally.
在上面的测试中,参数”${text}“的值包含文本”${number}“。因此,我们期望”${text}“被”${number}“的字面意思取代。
However, the test fails if we execute it:
然而,如果我们执行该测试,就会失败。
org.opentest4j.AssertionFailedError:
expected: "Text: ['${number}' is a placeholder.] Number: [42] Text again: ['${number}' is a placeholder.]"
but was: "Text: ['42' is a placeholder.] Number: [42] Text again: ['42' is a placeholder.]"
So, StrSubstitutor treats the literal ${number} as a parameter placeholder, too.
所以,StrSubstitutor将字面意思${number}也视为参数占位符。
In fact, StrSubstitutor‘s Javadoc has stated this case:
事实上,StrSubstitutor的Javadoc已经说明了这种情况。
Variable replacement works in a recursive way. Thus, if a variable value contains a variable then that variable will also be replaced.
变量替换是以递归的方式工作的。因此,如果一个变量值包含一个变量,那么该变量也将被替换。
This happens because, in each recursion step, StrSubstitutor takes the last replacement result as the new template to proceed with further replacements.
这是因为,在每个递归步骤中,StrSubstitutor将最后一个替换结果作为新的模板来进行进一步的替换。
To bypass this problem, we can choose different prefixes and suffixes so that they don’t get interfered with:
为了绕过这个问题,我们可以选择不同的前缀和后缀,这样它们就不会受到干扰了。
String TEMPLATE = "Text: [%{text}] Number: [%{number}] Text again: [%{text}]";
Map<String, Object> params = new HashMap<>();
params.put("text", "'${number}' is a placeholder.");
params.put("number", 42);
String result = StrSubstitutor.replace(TEMPLATE, params, "%{", "}");
assertThat(result).isEqualTo("Text: ['${number}' is a placeholder.] Number: [42] Text again: ['${number}' is a placeholder.]");
However, theoretically speaking, as we cannot predict the values, it’s always possible that a value contains the parameter name pattern and interferes with the replacement.
然而,从理论上讲,由于我们无法预测这些值,总是有可能某个值包含参数名称模式,并干扰了替换。
Next, let’s create our own format() method to solve the problem.
接下来,让我们创建自己的format()方法来解决这个问题。
5. Building the Formatter on Our Own
5.构建我们自己的格式化程序
We’ve discussed why StrSubstitutor cannot handle the edge case well. So, if we create a method, the difficulty is that we shouldn’t use a loop or recursion to take the last step’s result as new input in the current step.
我们已经讨论过为什么StrSubstitutor不能很好地处理边缘情况。所以,如果我们创建一个方法,困难在于我们不应该使用循环或递归来把上一步的结果作为当前步骤的新输入。
5.1. The Idea to Solve the Problem
5.1.解决问题的想法
The idea is that we search for the parameter name patterns in the template. However, when we find one, we don’t replace it with the value from the map immediately. Instead, we build a new template that can be used for the standard String.format() method. If we take our example, we will try to convert:
这个想法是,我们在模板中搜索参数名称模式。然而,当我们找到一个时,我们不会立即用地图上的值来替换它。相反,我们建立一个新的模板,可以用于标准的String.format()方法。如果我们以我们的例子为例,我们将尝试进行转换。
String TEMPLATE = "Text: [${text}] Number: [${number}] Text again: [${text}]";
Map<String, Object> params ...
into:
成。
String NEW_TEMPLATE = "Text: [%s] Number: [%s] Text again: [%s]";
List<Object> valueList = List.of("'${number}' is a placeholder.", 42, "'${number}' is a placeholder.");
Then, we can call String.format(NEW_TEMPLATE, valueList.toArray()); to finish the job.
然后,我们可以调用String.format(NEW_TEMPLATE, valueList.toArray());来完成工作。
5.2. Creating the Method
5.2.创建方法
Next, let’s create a method to implement the idea:
接下来,让我们创建一个方法来实现这个想法。
public static String format(String template, Map<String, Object> parameters) {
StringBuilder newTemplate = new StringBuilder(template);
List<Object> valueList = new ArrayList<>();
Matcher matcher = Pattern.compile("[$][{](\\w+)}").matcher(template);
while (matcher.find()) {
String key = matcher.group(1);
String paramName = "${" + key + "}";
int index = newTemplate.indexOf(paramName);
if (index != -1) {
newTemplate.replace(index, index + paramName.length(), "%s");
valueList.add(parameters.get(key));
}
}
return String.format(newTemplate.toString(), valueList.toArray());
}
The code above is pretty straightforward. Let’s walk through it quickly to understand how it works.
上面的代码是非常直接的。让我们快速浏览一下,了解它是如何工作的。
First, we’ve declared two new variables to save the new template (newTemplate) and the value list (valueList). We’ll need them when we call String.format() later.
首先,我们声明了两个新的变量来保存新模板(newTemplate)和值列表(valueList)。当我们以后调用String.format() 时,我们将需要它们。
We use Regex to locate parameter name patterns in the template. Then, we replace the parameter name pattern with “%s” and add the corresponding value to the valueList variable.
我们使用Regex来定位模板中的参数名称模式。然后,我们用“%s”替换参数名称模式,并将相应的值添加到valueList变量。
Finally, we call String.format() with the newly converted template and values from valueList.
最后,我们用新转换的模板和valueList.中的值调用String.format()。
For simplicity, we’ve hard-coded the prefix “${” and suffixes “}” in the method. Also, if the value for a parameter “${unknown}” isn’t provided, we’ll simply replace the “${unknown}” parameter with “null“.
为了简单起见,我们在方法中硬编码了前缀”${“和后缀”}“。另外,如果没有提供参数”${unknown}“的值,我们将简单地用”${unknown}“替换参数”null“。
5.3. Testing Our format() Method
5.3.测试我们的format()方法
Next, let’s test if the method works for the regular case:
接下来,让我们测试一下该方法是否对常规情况有效。
Map<String, Object> params = new HashMap<>();
params.put("text", "It's awesome!");
params.put("number", 42);
String result = NamedFormatter.format(TEMPLATE, params);
assertThat(result).isEqualTo("Text: [It's awesome!] Number: [42] Text again: [It's awesome!]");
Again, the test passes if we give it a run.
同样,如果我们让它运行一下,测试就会通过。
Of course, we want to see if it works for the edge case, too:
当然,我们想看看它是否对边缘情况也有效。
params.put("text", "'${number}' is a placeholder.");
result = NamedFormatter.format(TEMPLATE, params);
assertThat(result).isEqualTo("Text: ['${number}' is a placeholder.] Number: [42] Text again: ['${number}' is a placeholder.]");
If we execute this test, it passes, too! We’ve solved the problem.
如果我们执行这个测试,它也会通过!我们已经解决了这个问题。
6. Conclusion
6.结语
In this article, we’ve explored how to replace parameters in template-based strings from a set of values. Basically, Apache Commons Text’s StrSubstitutor.replace() method is pretty straightforward to use and can solve most cases. However, when values contain the parameter name patterns, StrSubstitutor may produce an unexpected result.
在这篇文章中,我们已经探讨了如何从一组值中替换基于模板的字符串中的参数。基本上,Apache Commons Text的StrSubstitutor.replace()方法使用起来相当直接,可以解决大多数情况。然而,当值包含参数名称模式时,StrSubstitutor可能会产生一个意外的结果。
Therefore, we’ve implemented a format() method to solve this edge case.
因此,我们实现了一个format()方法来解决这个边缘情况。
As always, the full source code of the examples is available over on GitHub.
一如既往,这些示例的完整源代码可在GitHub上获得over。