Java字符串模板化/格式化器,支持命名参数

11

是否有类似于String.format但具有命名参数的标准或至少广泛实现的东西?

我想以以下方式格式化模板字符串:

Map<String, Object> args = new HashMap<String, Object>();
args.put("PATH", "/usr/bin");
args.put("file", "foo");
String s = someHypotheticalMethod("#{PATH}/ls #{file}");
// "/usr/bin/ls foo"

从技术上讲,它几乎与以下内容相同:

String[] args = new String[] { "/usr/bin", "foo" };
String s = String.format("%1$s/ls %2$s", args);
// "/usr/bin/ls foo"

但是使用命名参数。

我知道:

但它们都使用有序或至少编号的参数,而不是命名参数。我知道实现一个是微不足道的,但是否有我正在标准Java库中寻找的机制,或者至少在Apache Commons / Guava / 类似的东西中,而不需要引入高调的模板引擎?

注意:我对具有某些命令式/函数逻辑、流程控制、修饰符、子模板/包含、迭代器等功能的完整模板引擎并不感兴趣。通常以下方法是一个可行的4行实现 - 这就是我所需要的全部:

public static String interpolate(String format, Map<String, ? extends Object> args) {
    String out = format;
    for (String arg : args.keySet()) {
        out = Pattern.compile(Pattern.quote("#{" + arg + "}")).
                matcher(out).
                replaceAll(args.get(arg).toString());
    }
    return out;
}

你为什么不直接使用 "#" + args.get("PATH") + "/ls #" + args.get("file") 呢? - Charles
我有一堆模板文件,我有一个参数映射表,我需要从每个模板文件中获取填充字符串。 - GreyCat
8个回答

10

如果 Java 7 并不可行,你也可以尝试使用org.apache.commons.lang3.text.StrSubstitutor。它能够准确地实现你想要的功能。它是否轻量级可能取决于你是否同时使用了 commons-lang 中的其他内容。


4

我总是很高兴看到Java 7的参考资料。 - Michael Piefel

4
我最近发现了JUEL,它完美地符合描述。这是从JSP中提取出来的表达式语言。它声称非常快。
我将在自己的项目中尝试使用它。
但是如果要使用一个更轻量级的变体,请尝试以下内容(包含在单元测试中):
public class TestInterpolation {

    public static class NamedFormatter {
        public final static Pattern pattern = Pattern.compile("#\\{(?<key>.*)}");
        public static String format(final String format, Map<String, ? extends Object> kvs) {
            final StringBuffer buffer = new StringBuffer();
            final Matcher match = pattern.matcher(format);
            while (match.find()) {
                final String key = match.group("key");
                final Object value = kvs.get(key);
                if (value != null)
                    match.appendReplacement(buffer, value.toString());
                else if (kvs.containsKey(key))
                    match.appendReplacement(buffer, "null");
                else
                    match.appendReplacement(buffer, "");
            }
            match.appendTail(buffer);
            return buffer.toString();
        }
    }

    @Test
    public void test() {
        assertEquals("hello world", NamedFormatter.format("hello #{name}", map("name", "world")));
        assertEquals("hello null", NamedFormatter.format("hello #{name}", map("name", null)));
        assertEquals("hello ", NamedFormatter.format("hello #{name}", new HashMap<String, Object>()));
    }

    private Map<String, Object> map(final String key, final Object value) {
        final Map<String, Object> kvs = new HashMap<>();
        kvs.put(key, value);
        return kvs;
    }
}

我会扩展它以添加便利方法,用于快速的键值对。

format(format, key1, value1)
format(format, key1, value1, key2, value2)
format(format, key1, value1, key2, value2, key3, value3)
...

把Java 7+转换成Java 6-并不难。


我想提一下,它其实并不是很轻量级:它基本上只允许在某些字符串中插入执行Java子集。它压缩后大约有138K的类,里面包含114个类。 - GreyCat
是的,我用手机回答了那个问题,一旦我到了键盘旁边,我会添加另一个实现。 - Michael Deardeuff
现在已经添加了我的实现。 - Michael Deardeuff

2

1
啊嗯... "属性引用","模板引用(例如#include或宏扩展)","子模板的条件包含","将模板应用于属性列表"... 221K的压缩jar类,内部有124个类 - 这绝对不是轻量级的。不过我会检查其他的,谢谢! - GreyCat
是的,我确认FreeMarker、Mustache和Velocity都比我需要的更加重量级。不管怎样,感谢您的建议! - GreyCat

1
您可以使用Java的字符串模板功能。 它在JEP 430中有详细描述,并且作为JDK 21的预览功能出现。这是一个使用示例:
String name = "Joan";
String info = STR."My name is \{name}";
assert info.equals("My name is Joan");   // true

Java的字符串模板比其他语言(如Python的f-strings)中的插值更加灵活且更安全。例如,字符串拼接或插值可能导致SQL注入攻击的发生。
String query = "SELECT * FROM Person p WHERE p.last_name = '" + name + "'";
ResultSet rs = conn.createStatement().executeQuery(query);

但是这个变体(来自JEP 430)可以防止SQL注入。
PreparedStatement ps = DB."SELECT * FROM Person p WHERE p.last_name = \{name}";
ResultSet rs = ps.executeQuery();

0

我知道我的回答有点晚,但如果你仍然需要这个功能,而不需要下载一个完整的模板引擎,你可以看一下aleph-formatter(我是其中之一的作者):

Student student = new Student("Andrei", 30, "Male");

String studStr = template("#{id}\tName: #{st.getName}, Age: #{st.getAge}, Gender: #{st.getGender}")
                    .arg("id", 10)
                    .arg("st", student)
                    .format();
System.out.println(studStr);

或者您可以链接参数:

String result = template("#{x} + #{y} = #{z}")
                    .args("x", 5, "y", 10, "z", 15)
                    .format();
System.out.println(result);

// Output: "5 + 10 = 15"

在内部,它使用 StringBuilder 通过“解析”表达式来创建结果,不执行字符串连接或正则表达式/替换。


0

这是我的解决方案:

public class Template
{

    private Pattern pattern;
    protected Map<CharSequence, String> tokens;
    private String template;

    public Template(String template)
    {
        pattern = Pattern.compile("\\$\\{\\w+\\}");
        tokens = new HashMap<CharSequence, String>();
        this.template = template;
    }

    public void clearAllTokens()
    {
        tokens.clear();
    }

    public void setToken(String token, String replacement)
    {
        if(token == null)
        {
            throw new NullPointerException("Token can't be null");
        }

        if(replacement == null)
        {
            throw new NullPointerException("Replacement string can't be null");
        }

        tokens.put(token, Matcher.quoteReplacement(replacement));
    }

    public String getText()
    {
        final Matcher matcher = pattern.matcher(template);
        final StringBuffer sb = new StringBuffer();

        while(matcher.find()) 
        {
            final String entry = matcher.group();
            final CharSequence key = entry.subSequence(2, entry.length() - 1);
            if(tokens.containsKey(key))
            {
                matcher.appendReplacement(sb, tokens.get(key));
            }
        }
        matcher.appendTail(sb);
        return sb.toString();
    }


    public static void main(String[] args) {
        Template template = new Template("Hello, ${name}.");
        template.setToken("name", "Eldar");

        System.out.println(template.getText());
    }
}

0

我也在我的字符串工具中做了一个(未经测试)string.MapFormat("abcd {var}",map)

//util
public static String mapFormat(String template, HashMap<String, String> mapSet) {
    String res = template;
    for (String key : mapSet.keySet()) {
        res = template.replace(String.format("{%s}", key), mapSet.get(key));
    }
    return res;
}

//use

public static void main(String[] args) {
    boolean isOn=false;
    HashMap<String, String> kvMap=new HashMap<String, String>();
    kvMap.put("isOn", isOn+"");
    String exp=StringUtils.mapFormat("http://localhost/api/go?isOn={isOn}", kvMap);
    System.out.println(exp);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接