Use StringSubstitutor from Apache Commons Text.

Dependency import

Import the Apache commons text dependency using maven as bellow:

Copy<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-text</artifactId>
    <version>1.10.0</version>
</dependency>

Example

CopyMap<String, String> valuesMap = new HashMap<String, String>();
valuesMap.put("animal", "quick brown fox");
valuesMap.put("target", "lazy dog");
String templateString = "The ${animal} jumped over the ${target}.";
StringSubstitutor sub = new StringSubstitutor(valuesMap);
String resolvedString = sub.replace(templateString);
Answer from JH. on Stack Overflow
🌐
Baeldung
baeldung.com › home › java › java string › named placeholders in string formatting
Named Placeholders in String Formatting | Baeldung
August 27, 2025 - As the test code shows, we let params hold all name -> value mappings. When we call the StringSubstitutor.replace() method, apart from template and params, we also pass the prefix and the suffix to inform StringSubstitutor what a parameter consists of in the template.
Discussions

java - Fastest possible text template for repeated use? - Code Review Stack Exchange
While reviewing Sending templatized e-mail to a million contacts, I wrote this implementation to illustrate an alternate approach. It is designed to be the fastest possible way to generate templated More on codereview.stackexchange.com
🌐 codereview.stackexchange.com
August 31, 2015
Why did String Templates choose '\{' instead of '{' ?
In the JEP this reasoning is given For the syntax of embedded expressions we considered using ${...}, but that would require a tag on string templates (either a prefix or a delimiter other than ") to avoid conflicts with legacy code. We also considered [...] and (...), but [ ] and ( ) are likely to appear in embedded expressions; { } is less likely to appear, so visually determining the start and end of embedded expressions will be easier. You might be able to find a more in depth reasoning if you go through the mailing list. https://mail.openjdk.org/pipermail/amber-dev/ More on reddit.com
🌐 r/java
74
6
March 14, 2023
JEP: String Templates (Final) for Java 22
I have read the explanation by various members of the OpenJDK team as to why that syntax was chosen and I still don't understand it. Even Brian's explanation, which otherwise gets right through to me even on more complex topics, did not really clarify it for me. He says choosing ${} would have made all the existing Strings a template if they contain ${} with a valid variable in the same scope, which breaks compatibility. But I don't know what I am missing to not see the breakage. For an existing String literal having a ${...} does not make it into a template that will be processed. You have to explicitly invoke STR. on that to process it as a template. Else if will be just like any other String with no interpolation. STR. is a completely new feature and is illegal in prior versions so I don't see how this breaks compatibility. Even if you had a variable named STR in existing code, it is illegal to do STR."some str literal". I guess it is too late now. More on reddit.com
🌐 r/java
133
72
October 1, 2023
What Happened to Java's String Templates? Inside Java Newscast
Honestly, this whole thing with string templates in java feels like a paranoia. Security? Validation? The hell are they smokin there? Why are they trying to solve world hunger with it? Just give people the damn interpolation like all normal human beings have other languages that's all we want. More on reddit.com
🌐 r/java
122
66
June 20, 2024
🌐
Simplesolution
simplesolution.dev › java-substitute-a-string-in-java-by-replace-variables-map-to-template-string
Substitute a String in Java by replace variables map to Template String using Apache Commons Text
import org.apache.commons.text.StringSubstitutor; import java.util.HashMap; import java.util.Map; public class StringSubstitutorExample3 { public static void main(String[] args) { // Template String String templateString = "The account ${accountNumber} balance is ${balance} dollars."; // Prepare value map of variables to replace to template String // This value map does not provide "balance" key value pair Map<String, String> valuesMap = new HashMap<>(); valuesMap.put("accountNumber", "SS12345"); // Initialize StringSubstitutor instance with value map StringSubstitutor stringSubstitutor = new StringSubstitutor(valuesMap); // replace value map to template string String result = stringSubstitutor.replace(templateString); System.out.println(result); } } Output:
🌐
Baeldung
baeldung.com › home › java › java string › string templates in java
String Templates in Java | Baeldung
July 7, 2025 - Java provides some out-of-the-box template processors. The STR Template Processor performs string interpolation by iteratively replacing each embedded expression of the provided template with the stringified value of that expression.
🌐
JetBrains
blog.jetbrains.com › idea › 2023 › 11 › string-templates-in-java-why-should-you-care
String Templates in Java - why should you care? | The IntelliJ IDEA Blog
November 27, 2023 - To get around it, I created a custom String Template, which would decipher the down arrow in the question text, which was read in the memory as a record instance from a json file. However, while generating the corresponding PDF file, the String template would replace the precipitation symbol, that is, ↓, with the text ‘(Precipitation)’. Here’s the code for this custom String template:
🌐
OpenJDK
openjdk.org › jeps › 430
JEP 430: String Templates (Preview)
September 17, 2021 - STR is a template processor defined in the Java Platform. It performs string interpolation by replacing each embedded expression in the template with the (stringified) value of that expression.
🌐
Oracle
docs.oracle.com › en › java › javase › 22 › language › string-templates.html
4 String Templates - Java
March 13, 2024 - It automatically performs string interpolation by replacing each embedded expression in the template with its value, converted to a string. The JDK includes two other template processors: The FMT Template Processor: It's like the STR template processor except that it accepts format specifiers as defined in java...
Find elsewhere
🌐
javaspring
javaspring.net › blog › how-to-replace-a-set-of-tokens-in-a-java-string
How to Replace Tokens in a Java String Template Without Replacing Tokens in Variables — javaspring.net
Scanning the template to find these tokens. Replacing them with variable values while leaving other content intact. import java.util.HashMap; import java.util.Map; public class CustomParserExample { public static void main(String[] args) { String template = "Hello {{user}}, your session ID is {{sessionId}}. Support: {{supportEmail}}"; Map<String, String> tokensToReplace = new HashMap<>(); tokensToReplace.put("{{user}}", "jane{{456}}"); tokensToReplace.put("{{sessionId}}", "abc123"); tokensToReplace.put("{{supportEmail}}", "[email protected]"); // Replace only explicitly defined tokens String result = template; for (Map.Entry<String, String> entry : tokensToReplace.entrySet()) { result = result.replace(entry.getKey(), entry.getValue()); } System.out.println(result); // Output: "Hello jane{{456}}, your session ID is abc123.
🌐
Medium
medium.com › naukri-engineering › can-java-21-string-templates-replace-the-third-party-java-templating-engines-9bcfe2d4f28c
Can Java 21 String templates replace the third-party Java templating engines? | by Anurag Rana | Naukri Engineering | Medium
January 12, 2024 - STR (used in the 2nd line of code above) is a template processor defined in the Java Platform. It performs string interpolation by replacing each embedded expression in the template with the (stringified) value of that expression.
🌐
Apache Commons
commons.apache.org › proper › commons-text › apidocs › org › apache › commons › text › StringSubstitutor.html
StringSubstitutor (Apache Commons Text 1.15.0 API)
Replaces all the occurrences of variables with their matching values from the resolver using the given source array as a template. The array is not altered by this method. Only the specified portion of the array will be processed. The rest of the array is not processed, and is not returned.
🌐
Delft Stack
delftstack.com › home › howto › java › java string template
String Template in Java | Delft Stack
October 12, 2023 - // Importing necessary packages import java.util.HashMap; import java.util.Map; import org.apache.commons.text.StringSubstitutor; public class StringReplace { public static void main(String args[]) { Map<String, String> MyMap = new HashMap<String, String>(); // Declaring a Map MyMap.put("Name", "Alen Walker"); // Creating a replacement String MyString = "Good Morning!!
Top answer
1 of 3
9

The compile concept produced incorrect results for me. When I run your code the template does not produce the correct results. For the input parameters:

final Map<String,String> parms = new HashMap<>();
Stream.of("USER_NAME", "USER_PHONE", "USER_EMAIL", "LOGIN_URL")
      .forEach(tag -> parms.put(tag, tag));

I would expect the input String:

"Dear {{USER_NAME}},\n\n" +
"According to our records, your phone number is {{USER_PHONE}} and " +
"your e-mail address is {{USER_EMAIL}}.  If this is incorrect, please " +
"go to {{LOGIN_URL}} and update your contact information."

to produce:

Dear USER_NAME,

According to our records, your phone number is USER_PHONE and your e-mail address is USER_EMAIL.  If this is incorrect, please go to LOGIN_URL and update your contact information.

But, instead, it produces:

Dear USER_NAMEAccording to our records, your phone number is USER_PHONE and your e-mail address is USER_EMAIL.  If this is incorrect, please go to LOGIN_URL and update your contact information.

I have looked through the code, and I am not sure why it is dropping the newlines, and the comma-punctuation after "USER_NAME".

I looked through the TemplateCompile code, and while I like that you use a Pattern/Matcher to parse the template, the actual loop structure is really complicated. You shoe-horn the process in to a for-loop, when a while-loop would be much better. Additionally, you use a complicated double-matching named-group regular expression, when a single-matching one would be more than adequate.

I particularly dislike the rest variable, and how it is used.

I wonder if this complicated regex logic is the cause of the broken output?

I wrote a "competing" code block, and I also chose regex to parse the template, but my loop is very different:

private static final Pattern token = Pattern.compile("\\{\\{(\\w+)\\}\\}");

public static Template compile(String text) {

    Matcher mat = token.matcher(text);

    int last = 0;

    while (mat.find()) {
        // the non-token text is from the last match end,
        // to this match start
        final String constant = text.substring(last, mat.start());
        // this token's key is the regex group
        final String key = mat.group(1);

        // do stuff with the text and subsequent token
        ....

        last = mat.end();
    }
    final String tail = text.substring(last);
    if (!tail.isEmpty()) {
        // do something with trailing text after last token.
        ....
    }

}

A while loop on the Matcher.find() result is the natural loop constraint.

Instead of compiling the code down, I used an array of text injectors to perform the write. Some injectors inject a constant value, others inject a lookup value from the Parameters. I was able to reduce your class down to much simpler constructs, with no code abstraction and compilation, etc. From a readability and maintenance perspective I believe it is clearly better:

public class MonkeyFix implements Template {

    @FunctionalInterface
    private interface Injector {
        String get(Map<String,String> params);
    }

    private static final Pattern token = Pattern.compile("\\{\\{(\\w+)\\}\\}");

    public static Template compile(final String text) {
        final Matcher mat = token.matcher(text);
        final List<Injector> sequence = new ArrayList<>();
        int last = 0;

        while (mat.find()) {
            final String constant = text.substring(last, mat.start());
            final String key = mat.group(1);

            sequence.add(params -> constant);
            sequence.add(params -> params.get(key));

            last = mat.end();
        }

        final String tail = text.substring(last);
        if (!tail.isEmpty()) {
            sequence.add(params -> tail);
        }

        return new MonkeyFix(sequence.toArray(new Injector[sequence.size()]));
    }

    private final Injector[] sequence;

    public MonkeyFix(Injector[] sequence) {
        this.sequence = sequence;
    }

    @Override
    public void write(Writer out, Map<String, String> params) throws IOException {
        for (Injector lu : sequence) {
            out.write(lu.get(params));
        }
    }

}

How about the performance, though?

I pout the code through my MicroBench suite, using the following code (I had to use a different validation string for your code, I called that one wrong ... ;-) :

public class TemplateMain {

    private static final String text = 
            "Dear {{USER_NAME}},\n\n" +
            "According to our records, your phone number is {{USER_PHONE}} and " +
            "your e-mail address is {{USER_EMAIL}}.  If this is incorrect, please " +
            "go to {{LOGIN_URL}} and update your contact information.";

    private static final Template inmemcomp = TemplateCompiler.compile(text);

    private static final Template monkeyfix = MonkeyFix.compile(text);

    private static final String inMemFunc(Template t, Map<String, String> params) {
        StringWriter sw = new StringWriter();
        try {
            t.write(sw, params);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return sw.toString();
    }

    public static void main(String[] args) {
        UUtils.setStandaloneLogging(Level.INFO);
        UBench bench = new UBench("Templating");
        final String expect = "Dear USER_NAME,\n\n" +
                "According to our records, your phone number is USER_PHONE and " +
                "your e-mail address is USER_EMAIL.  If this is incorrect, please " +
                "go to LOGIN_URL and update your contact information.";
        final String wrong = "Dear USER_NAMEAccording to our records, your phone number is USER_PHONE and your e-mail address is USER_EMAIL.  If this is incorrect, please go to LOGIN_URL and update your contact information.";

        System.out.println(expect);
        System.out.println(wrong);

        final Map<String,String> parms = new HashMap<>();
        Stream.of("USER_NAME", "USER_PHONE", "USER_EMAIL", "LOGIN_URL").forEach(tag -> parms.put(tag, tag));

        bench.addTask("InMemCompile", () -> inMemFunc(inmemcomp, parms), got -> wrong.equals(got));
        bench.addTask("MonkeyFix", () -> inMemFunc(monkeyfix, parms), got -> expect.equals(got));

        bench.press(10000).report();

    }
}

The results are inconclusive on my computer, sometimes your code wins, sometimes mine does. Regardless, they are both "fast enough", and the differences are marginal.

2 of 3
3

@rolfl has uncovered some embarrassing bugs in the output.

Parts of the literal strings were being dropped due to a missing Pattern.DOTALL flag:

private static final Pattern SUBST_PAT = Pattern.compile(
    "(?<LITERAL>.*?)(?:\\{\\{(?<SUBST>[^}]*)\\}\\})", Pattern.DOTALL
);

In stringLiteral(), all three cases were wrong:

        switch (c) {
          // JLS SE7 3.10.5: 
          // It is a compile-time error for a line terminator to appear
          case '\r':
            matcher.appendReplacement(result, "\\\\r");
            break;
          case '\n':
            matcher.appendReplacement(result, "\\\\n");
            break;
          default:
            matcher.appendReplacement(result, String.format("\\\\u%04x", (int)c));
        }
🌐
Medium
maffonso.medium.com › java-string-templates-simplifying-text-handling-1f36f864056e
Java String Templates: Simplifying Text Handling | by Mauricio Afonso | Medium
June 14, 2024 - mechanism that will process the text STR is a template processor defined in the Java Platform. It performs string interpolation by replacing each embedded expression in the template with the (stringified) value of that expression.
🌐
HappyCoders.eu
happycoders.eu › java › string-templates
String Templates in Java
June 12, 2025 - int a = ...; int b = ...; String concatenated = a + " times " + b + " = " + a * b; String format = String.format("%d times %d = %d", a, b, a * b); String formatted = "%d times %d = %d".formatted(a, b, a * b);Code language: Java (java) Often we also use a StringBuilder, less often MessageFormat. None of the available variants is particularly good to read. Almost every modern programming language offers the possibility of string interpolation, i.e., the possibility of evaluating placeholders in a string and replacing them with the result of this evaluation. Precisely this (and even more, see below) is enabled by “string templates” introduced by JDK Enhancement Proposal 430.
🌐
Belief Driven Design
belief-driven-design.com › looking-at-java-21-string-templates-c7cbc
Looking at Java 21: String Templates | belief driven design
June 20, 2023 - The StringTemplate gives us more than just an argument-less interpolate method. We have access to the expression results and can manipulate them! That means the template can be simplified, as the Processor will be responsible for handling the values correctly, like escaping double quotes in the user value, etc. ... To achieve this, the Processor evaluates the results of the expressions (template.values()) and creates new replacements to be matched with fragment literals (template.fragments()):
🌐
OpenJDK
openjdk.org › jeps › 459
JEP 459: String Templates (Second Preview)
August 14, 2023 - STR is a template processor defined in the Java Platform. It performs string interpolation by replacing each embedded expression in the template with the (stringified) value of that expression.