2023/12/22

String Templateによる文字列補間 その3 - 動的なテンプレート作成

このエントリーをはてなブックマークに追加

本エントリーはJava Advent Calendarシリーズ2の22日目のエントリーです。

qiita.com

ここまで、String Templateの基本的な使い方と、カスタムテンプレートプロセッサーを紹介してきました。

www.javainthebox.com

www.javainthebox.com

String Templateは便利なので、チョコチョコと使っているのですが、1つだけ気にくわないところがあります。

それはテンプレートに文字列リテラルかテキストブロックしか使えないところです。

一般的なテンプレートエンジンであれば、外部のHTMLファイルなどをテンプレートとして使用することができます。

ところが、String Templateではそれができないのです!

STR.process("Hello, \{name}!");とか書ければいいんですけどね。


できないならば、作ってしまおうというのが本エントリーです。

JJUG CCCで使用した資料では32ページぐらいからの内容です。


テンプレート引数に文字列リテラルかテキストブロックしか使えないのであれば、任意の文字列を文字列リテラルにしてしまうクラスを実行中に作ってしまえばいいのです。

クラスを動的に作成するのは、現在のJavaでは当たり前に行われてます。ラムダ式をはじめ、いろいろなところでクラスの動的生成が使われています。

だったら、普通のアプリケーションでもクラスを動的に作成して、実行できるようにしておけばいろいろと拡張を考えることができるはず!

さて、実行中にテンプレート引数を持つクラスを作って、String Templateを実行するには道具として次の3種類のAPIが必要です。

  • Compiler API
  • クラスローダー
  • リフレクション、もしくはMethodHandle

Compiler APIは前回のエントリーで解説したAPIです。

それ以外の、クラスローダーやリフレクションは当たり前のように使われているAPIですよね。最近はリフレクションの代わりにMethodHandleクラスを使うことが多くなってきていますが、ここでは説明を省くためにリフレクションを使います。

手順はこんな感じ。

  1. 文字列(テキストブロック)でクラスのひな型を用意する
  2. テンプレート引数にする文字列をひな型に埋め込む
  3. クラスをコンパイル
  4. コンパイルしたクラスをクラスロード
  5. リフレクションを使って、クラスのメソッドを実行

前回、文字列でクラスを記述して、それをコンパイルする手法を紹介しましたが、ここで使うわけです。

今回使用したソースコードはGistにアップしてあるので、全体を見たい方はそちらを参照してください。

String Templateのテンプレートを動的に作成する方法


では、まず文字列でクラスを作成してみましょう。


動的にクラスを作成し、コンパイル

前回のCompiler APIの紹介で文字列をソースにする方法で使用したStringJavaFileObjectクラスを使用して、ソースのひな型を作ります。

これを行っているのが、createJavaFileObjectsメソッドです。

    private static List<? extends JavaFileObject> createJavaFileObjects(String template, String argName) {
        // Java のソースとなる文字列
        String templateSrc = STR."""
            public class Template {
                public static StringTemplate process(Object \{ argName }) {
                    return java.lang.StringTemplate.RAW.\"\"\"
                        \{ template }
                        \"\"\";
                }
            }
	    """ ;

        // 文字列をソースとする StringJavaFileObject オブジェクトを
        // 生成する
        JavaFileObject fileobj
                = new StringJavaFileObject("Template", templateSrc);

        return List.of(fileobj);
    }

createJavaFileObjectsメソッドの第1引数がテンプレートとなる文字列、第2引数がテンプレートに埋め込まれる変数の名前です。

ここでは、動的にTemplateクラスを作成して、Templateクラスのprocessメソッドをコールすると、StringTemplateオブジェクトができるようにします。

processメソッドの引数は動的に作成したテンプレートに埋め込む変数で、どのような型か指定することはできないので、Objectクラスにしてあります。

このTemplateクラスには2か所変数を埋め込んであり、それをString Templateで処理しています。1つがprocessメソッドの引数名、次がRAWで処理するテキストブロックです。

ここでRAWを使用しているのは、RAWであればこの後に任意のテンプレートプロセッサーでもう1度、文字列補間処理を行うことができるからです。

それにしても、StringTemplateオブジェクトを生成するために、String Templateを使うというちょっとメタ的な使い方なので、ちょっとこんがらがりますね。

このクラスの元となる文字列ができてしまえば、後はそれを使用してStringJavaFileObjectオブジェクトを生成し、リストにして返します。


さて、このcreateJavaFileObjectsメソッドをコールしている方です。

ソースをコンパイルするのは前回紹介した方法そのままです。

        // コンパイラの取得
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        if (compiler == null) {
            return Optional.empty();
        }

        // 仮想ファイルマネージャの取得
        try (StandardJavaFileManager fileManager
                = compiler.getStandardFileManager(null, null, null)) {

            // コンパイルするファイルの準備
            List<? extends JavaFileObject> fileobjs
                    = createJavaFileObjects(template, argName);

            // コンパイルタスクの生成
            JavaCompiler.CompilationTask task
                    = compiler.getTask(null,
                            fileManager,
                            null,
                            List.of("--release", "22", "--enable-preview"),
                            null,
                            fileobjs);

            // コンパイル
            boolean result = task.call();


クラスロードと実行

クラスのコンパイルが成功したら、次にコンパイルしたクラスをロードし、リフレクションで実行します。

            if (result) {
                // クラスのロード
                ClassLoader loader = ClassLoader.getSystemClassLoader();
                Class<?> clss = loader.loadClass("Template");

                // Method オブジェクトを取得し、リフレクションで実行する
                Method method = clss.getMethod("process", Object.class);
                StringTemplate dynamicTemplate
                        = (StringTemplate) method.invoke(null, new Object[]{variable});

                return Optional.of(dynamicTemplate);
            } else {
                return Optional.empty();
            }

クラスロードにはシステムクラスローダを使用しているので、実行するときは動的に生成したクラスファイルがクラスパスに含まれるようにしてください。

クラスがロードできれば、そのprocessメソッドをClassクラスのgetMethodメソッドで探索します。getMethodメソッドの引数はメソッド名と、可変長引数で引数の型です。

processメソッドを探すので、第1引数は"process"、第2引数はprocessメソッドの引数の型であるObject.classを指定します。

processメソッドを表すMethodオブジェクトが取得できれば、後は実行するだけです。

processメソッドはstaticメソッドなので、invokeメソッドの第1引数はnullにしておきます。


コンパイル、クラスロード、リフレクションはスローされる例外がいろいろあり、失敗することもあるので戻り値はOptionalクラスにしてあります。

成功すればOptional.ofメソッドでStringTemplateオブジェクトを戻し、失敗したらOptional.empty()を戻すようにしました。


さて、動的にテンプレートを記述できるようになったので、試してみましょう。

ここではテンプレートをhello.tempというファイルに記述しました。

\{variable}様
いつもお世話になっております

メールの先頭によくありがちな定型文です。

このファイルを読み込んで、StringTemplateオブジェクトを生成します。

        // ファイルからテンプレートを読み込み
        String template = new String(Files.readAllBytes(Path.of("hello.temp")));
            
        // 動的テンプレート生成
        // 第1引数: テンプレート 
        // 第2引数: テンプレートに埋め込む変数名
        // 第3引数: テンプレートに埋め込む値
        Optional<StringTemplate> dynamicTemplate
            = DynamicTemplateBuilder.createTemplate(template, "variable", "Bob Dylan");
        
        // 任意のテンプレートプロセッサで処理
        dynamicTemplate.ifPresent(t -> {
            var result = STR.process(t);
            System.out.println(result);
        });

ここで、Files.readAllBytesメソッドでファイルを読んだ後に文字列に変換しているのは、改行も含んですべてを文字列にしたかったからです。

Files.readAllLinesメソッドだと行単位に分割されてしまうんですよね。

いつもお世話になっているBob Dylanさんを埋め込んでみました。

実行すると、次のように処理された文字列が表示されるはず。

C:\sample>java --enable-preview TemplateTest
ノート: /Template.javaはJava SE 22のプレビュー機能を使用します。
ノート: 詳細は、-Xlint:previewオプションを指定して再コンパイルしてください。
Bob Dylan様
いつもお世話になっております

ちゃんと、Bob Dylanが埋め込まれました!


試しに作ってみただけなので、テンプレートを作成するたびにクラス生成からコンパイルをしてしまうとか、生成するクラス名が決め打ちなのでStringTemplateオブジェクトを複数生成できないとか、埋め込む値が1つに限定されているなどありますが、できることは確認できました!!

もうちょっと手を入れれば、汎用的に使えるようになるかも。


0 件のコメント: