JEP-430 String Template が気になったので簡単な範囲で調べてみた。
触ってみた系のブログが好きでない人は閉じてもらってよいです。
ざっくり
- JEP-430 の仕様が詳しいので、これを読むだけでだいたい必要なことは覚えられる
processor."String Template"
の形式でオブジェクトを作る"String Template"
部分に入れ込む値はそのままでも、式でも可能だが、for
やwhile
などの 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 の使い方、値の形式
STR
は StringTemplate.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の例ではSQLの PreparedStatement
を作っていたので、ここでもそれにあわせて 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