mike-neckのブログ

Java or Groovy or Swift or Golang

JEP-430 String Template を簡単な範囲で調べた

JEP-430 String Template が気になったので簡単な範囲で調べてみた。

触ってみた系のブログが好きでない人は閉じてもらってよいです。

ざっくり

  • JEP-430 の仕様が詳しいので、これを読むだけでだいたい必要なことは覚えられる
  • processor."String Template" の形式でオブジェクトを作る
  • "String Template" 部分に入れ込む値はそのままでも、式でも可能だが、 forwhile などの Statement はエラーになる
  • StringTemplate.Processor を自作するのも難しくない
  • StringTemplate 部分の中で別の StringTemplate を使える
  • String Template 部分は invokedynamic 命令で StringTemplate のオブジェクトを作るコードになる

JEP-430 の仕様が詳しいので、これを読むだけでだいたい必要なことは覚えられる

JEP-430 String Template の仕様書にString Templateのことが 詳しく書いてあるので、ちゃんと理解したい場合はこのブログを読むよりは仕様書を読んだほうがよい。 最近のJEPは非常に丁寧でわかりやすい。

さっそく試してみる

早速試してみる。なお、試したのは build 21-ea+25-2212 。

public interface HelloWorld {

    static void main(String [] args) {
        String greeting = "Hello";
        String name = "String Template";
        int java = 21;
        String text = STR."\{greeting} \{name}@Java\{java}";
        System.out.println(text);
    }
}

出力は次のようになる

Hello String Template@Java21

String Template の使い方、値の形式

STRStringTemplate.Processor<String, RuntimeException>インスタンスで 実際に値を埋め込んでいる String Template の部分をメソッド(フィールド?)のように呼び出す(アクセスする?)ことで、文字列に出力される。 値を埋め込む時の指定は \{expression} の形式になる。

expression の部分はリテラルでも変数でも式でもメソッド呼び出しでもよい。例えば、次のように switch も埋め込める

public class Switch {

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            System.out.println(STR."item-\{i}\{ switch(i) {
                    case 1 -> "st";
                    case 2 -> "nd";
                    case 3 -> "rd";
                    default -> "th";
                }
            }");
        }
    }
}

しかし、繰り返し処理のようなケースには対応できない

public class For {
    public static void main(String [] args) {
        //コンパイルエラー
        System.out.println(STR." \{ for(int i = 0; i < 5; i++) { } foo \{ } }");
    }
}

ここまでに、取り扱ったのは STR というProcessor<String, RuntimeException> 。 ほかにも java.util.FormatProcessor というクラスには FMT という FormatProcessorインスタンスがある。

import static java.util.FormatProcessor.FMT;

class Example {
    public static void main(String[] args) {
        for (int i = 1; i < 100000; i=i*10) {
            System.out.println(FMT."value = %05d\{i}");
        }
    }
}

出力

value = 00001
value = 00010
value = 00100
value = 01000
value = 10000

自作 StringTemplate$Processor

JEPを読み進めると、自作 StringTemplate$Processor を作る話が出てくる。 StringTemplate$Processor<R, E extends Throwable>R process(StringTemplate st) throws E の単一メソッドを実装するだけのインターフェース。 R の型は自由に決められるので、 String Template 部分で XML を作っておいて、 R はそれを読み込んだ状態の Document を指定することも可能。 SMT."text \{object.getValue()}" のようなテキストと埋め込む式の部分はコンパイル時に StringTemplate という オブジェクトに変換され、 invokedynamic 命令で作られる。

JEPの例ではSQLPreparedStatement を作っていたので、ここでもそれにあわせて Processor<PreparedStaement, SQLException> を 実装してみる。

import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.List;
import java.time.Instant;

import org.jetbrains.annotations.NotNull;

record SqlProcessor(
        @NotNull Connection connection
) implements StringTemplate.Processor<PreparedStatement, SQLException> {
    @Override
    public PreparedStatement process(@NotNull StringTemplate st) throws SQLException {
        List<String> fragments = st.fragments(); // 1
        String query = String.join("?", fragments);
        PreparedStatement statement = connection.prepareStatement(query);

        List<Object> values = st.values();   // 2
        for (int i = 0; i < values.size(); i++) {
            Object value = values.get(i);
            int index = i + 1;
            switch (value) {
                case Integer v -> statement.setInt(index, v);
                case Instant v -> statement.setDate(index, new Date(v.toEpochMilli()));
                default -> statement.setString(index, String.valueOf(value));
            }
        }
        return statement;
    }
}

fragments() というメソッドでは、 String Template の文字の部分が取得される。

String titleLike = "助成団体決定%";
Instant created = OffsetDateTime
        .of(2020, 1, 2, 15, 4, 5, 6, ZoneOffset.ofHours(9))
        .toInstant();

SqlProcessor sql = new SqlProcessor();
//language=sql
PreparedStatement statement = sql."""
select *
from documents d
where d.title like \{titleLike}
  and d.created_at < \{created}
"""

例えば、上記のString Template に対しては次の3つが fragment() メソッドで返される。

  • select *d.title like の部分
  • \n and d.created_at < の部分
  • ``(empty な文字列)の部分

よって、これを ? で結合すると、 PreparedStatement のクエリが作られる。

values() には埋め込んだ部分 \{...} の値が入ってくる。上記の例では String Instant の値があり、 一つの型には決められないので List<Object> となっており、大体の場合は switch で処理を分けることになると思われる。

作成した StringTemplate.Processor<PreparedStatement, SQLException> については、 具体例にもあるように . を挟んで String Template を記述するようになっている。 switch 式も埋め込めることから、次のように動的な SQL も記述できる。

String titleLike = null;
Instant created = OffsetDateTime
        .of(2020, 1, 2, 15, 4, 5, 6, ZoneOffset.ofHours(9))
        .toInstant();

SqlProcessor sql = new SqlProcessor();
//language=sql
PreparedStatement statement = sql."""
select *
from documents d
where d.created_at < \{created}
\{ switch (titleLike) {
    case null -> "";
    default -> STR."and d.title like \{titleLike}";
} }
"""

入れ子

上記のように、 String Template 中に String Template を使用することももちろん可能である。

コンパイラーによる処理

String Template は実行中に String を解析するのではなく、コンパイラーで解析して、 invokedynamic 命令によって String Template のオブジェクトを作っている。

javap で解析してみると、このよう invokedynamic 命令と断片化されたSQLがBootstrapMethodsにあるのがわかる。

       147: invokedynamic #104,  0            // InvokeDynamic #6:process:(ILcom/example/string/ext/ConditionalSQLFragment;Lcom/example/string/ext/SQLFragment;)Ljava/lang/StringTemplate;
       152: invokevirtual #107                // Method com/example/string/ext/SqlProcessor.process:(Ljava/lang/StringTemplate;)Ljava/lang/Object;
       155: checkcast     #108                // class java/sql/PreparedStatement
BootstrapMethods:

  6: #231 REF_invokeStatic java/lang/runtime/TemplateRuntime.newStringTemplate:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;[Ljava/lang/String;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #209 select\n  s.id,\n  s.name,\n  p.position,\n  d.id,\n  d.name,\n  m.since\nfrom staff s\n  join positions p on s.id = p.staff_id\n  join member m on s.id = m.staff_id\n  join department d on d.id = m.depart_id\nwhere\n  d.id =
      #211 \n
      #211 \n
      #211 \n

まとめ

  • String Template 便利だし、自作 Processor も楽しい、万能感ある
  • 自作 Processor はちゃんと使えるものを作ろうとすると結構沼かもしれない
  • したがって、SQL周りとかはフレームワークやライブラリーの作者が作ってくれると思う