Java 21 – String templates

Contents:

The Problem

“String” is a basic and widely used data type in any programming language.

Most of the data we deal with are texts which are represented by String datatype.

One of the operations we often do on text is replacing part of the text with dynamic values .

This is called interpolation.

Languages like Javascript keep this simpler.

For example ,

The below code :

const name = 'Alice';
const age = '25';

// String interpolation using template literals
const greeting = `Hello, ${name}! You are ${age} years old.`;

console.log(greeting);

will produce the below output:

Hello, Alice! You are 25 years old.

If you had to do the same in Java , you have to write:

String name = "Alice";
String age = "25";

String  greeting = "Hello , "+ name +" You are "+age+" years old";

System.out.println(greeting);

This has few problems.

  1. It looks less readable
  2. It wastes memory as every plus operator creates new string objects
  3. It is more error prone . For example , you might miss the spaces required before and after the interpolated text.
  4. It is less secure. If you are constructing an SQL query as above it can be exposed to SQL injection attacks.

The Solution

Java 21 resolved these issues.

Not only it made interpolation easier , it made it more secure (which is not the case in Javascript)

You can now interpolate the text using String Template Processor.

There are two parts to this.

The first part is similar to java script.

This helps in String interpolation.

The second part is exclusive to Java which makes interpolation more secure.

The first part

Instead of using ${} symbols to represent dynamic text , Java uses \{}.

For example the above code can be rewritten as :

String name = "Alice";
String age = "25";


String greeting = "Hello, \{name}! You are \{age} years old.";

System.out.println(greeting);

The second part

I missed the second part in the above code.

The above code won’t work.

You need to append a string template processor to the above greeting template.

Here is the refactored code:

String name = "Alice";
String age = "25";


String greeting = STR."Hello, \{name}! You are \{age} years old.";

System.out.println(greeting);

Notice the keyword “STR”.

This is a static reference variable which comes inbuilt with Java 21.

It is called String Template Processor.

It can act like a security guard and even more.

We will see that shortly.

Now you might ask how the above code is secure?

Well , it is not in this case.

STR is a template processor which just interpolates the template string.

It is the default template processor .

If your use case is just String interpolation and doesn’t have any security concerns you can use it.

But you can create your own which will do the validation.

For example ,

you can write code similar to below to make sure SQL injection doesn’t happen while constructing a dynamic query

String query =SQL."select * from Product where name = \{name}"

In the above example SQL is a custom template processor that we need to create to make sure hackers don’t pass insecure values to “name” parameter.

Now , let’s see how to build this “SQL” custom template processor.

Custom Template Processors

To create a custom template processor , you need to implement StringTemplate.Processor interface provided by Java 21.

Here is the interface for reference:

package java.lang;
public interface StringTemplate {
    ...
    @FunctionalInterface
    public interface Processor<R, E extends Throwable> {
        R process(StringTemplate st) throws E;
    }
    ...
}

As you see the interface is a functional interface.

It has one method named “process”.

We need to implement this method .

The method returns a custom data type (it need not be just a string!)

And it can also throw an exception, By default it throws a RunTimeException so developers need not handle it in their code using try catch block.

It accepts a StringTemplate as an input.

What is this StringTemplate?

String Template Class

String template represents the template string that you pass to the template processor.

So in the below code :

String greeting = STR."Hello, \{name}! You are \{age} years old.";

The line :

"Hello, \{name}! You are \{age} years old.";

represents the String Template.

The StringTemplate class comes with two useful methods.

StringTemplate::fragments method which returns a list of the words before and after each embedded expression.

So in the above case it will return :

["Hello, "," You are "," years old" ]

StringTemplate::values method which returns a list of the values passed to the above template.

So in the above example it will return :

["Alice","25"]

These two methods are useful to create our own template processors.

Now let’s create one.

The Custom SQL template processor

Now let’s implement a SQL template processor as discussed by implementing the above interface.

public class QueryBuilder(Connection conn)
  implements StringTemplate.Processor<PreparedStatement, SQLException> {

    public PreparedStatement process(StringTemplate st) throws SQLException {
        // 1. Replace StringTemplate placeholders with PreparedStatement placeholders
        String query = String.join("?", st.fragments());

        // 2. Create the PreparedStatement on the connection
        PreparedStatement ps = conn.prepareStatement(query);

        // 3. Set parameters of the PreparedStatement
        int index = 1;
        for (Object value : st.values()) {
            switch (value) {
                case Integer i -> ps.setInt(index++, i);
                case Float f   -> ps.setFloat(index++, f);
                case Double d  -> ps.setDouble(index++, d);
                case Boolean b -> ps.setBoolean(index++, b);
                default        -> ps.setString(index++, String.valueOf(value));
            }
        }

        return ps;
    }
}

In the above code,

We are first creating a class which implements StringTemplate.Processor interface.

We are passing two arguments to the interface here.

One is the Prepared Statement object.

Using PreparedStatement helps in preventing SQL Injection.

So we will do it here.

The next is SQLException , this will be thrown if any insecure value is passed.

So the developer using SQL template processor should handle this exception.

And for the implementation of the process method ,

we first get all the words before and after the dynamic query parameters in order and then append “?” to represent the dynamic parameters as we do in PreparedStatement.

For example,

For the below query:

String query = "select * from Product where name = \{name}";

We need to create the below string:

String query = "select * from Product where name = ?";

The below code snippet will do that:

        String query = String.join("?", st.fragments());

The method st.fragments() will return all the words before and after the dynamic query parameter “name” and append the character “?” in between.

Once we construct the query this way we need to create a PreparedStatement object and then set the parameter values.

Setting the values is done by the below code snippet:

 int index = 1;
        for (Object value : st.values()) {
            switch (value) {
                case Integer i -> ps.setInt(index++, i);
                case Float f   -> ps.setFloat(index++, f);
                case Double d  -> ps.setDouble(index++, d);
                case Boolean b -> ps.setBoolean(index++, b);
                default        -> ps.setString(index++, String.valueOf(value));
            }
        }

We are all set.

Now you can do String interpolation for the query using the below code:

var SQL = new QueryBuilder(conn);
PreparedStatement ps = SQL."SELECT * FROM Person p WHERE p.last_name = \{name}";
ResultSet rs = ps.executeQuery();


where “conn” represents the Connection object.

name is the dynamic query parameter which can be passed as input.

If not for the custom template processor we built , we would have done this the below way using traditional code:

String query = "SELECT * FROM Person p WHERE p.last_name = '" + name + "'";
ResultSet rs = conn.createStatement().executeQuery(query);



As earlier explained , it looks less readable , error prone and subject to SQL injection.

More

Calling methods inside a template

You can also call other methods inside a String template!

For example the below code will work :

String greeting = STR."Hello, \{getName()}! You are \{getAge()} years old.";

Notice the getName() and getAge() methods instead of the variables “name” and “age”

Using Text Blocks

If you want to create a multiline string and then using String Templates , that is possible using text blocks , another feature introducted in latest versions of Java.

Example:

String title = "Full Stack Developer Blog";
String text  = "Hello, Developers!";
String html = STR."""
        <html>
          <head>
            <title>\{title}</title>
          </head>
          <body>
            <p>\{text}</p>
          </body>
        </html>
        """;

Notice the three quotes “”” before and after the multi line string.

These are called text blocks and they work fine with String Templates.

Notice that this is a preview feature in Java 21 so you need to use –enable-preview and –source flag while running your program.

That’s it!

Reference:

https://openjdk.org/jeps/430

Comments

Leave a Reply

Discover more from The Full Stack Developer

Subscribe now to keep reading and get access to the full archive.

Continue reading