2023/12/25

ヒープだけでコンパイル&クラスロード (Compiler API補遺)

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

本エントリーはJava Advent Calendarシリーズ2の最後のエントリーです。

qiita.com

ここまで、3回に渡ってString Templateについて紹介してきました。

www.javainthebox.com

www.javainthebox.com

www.javainthebox.com

また、動的にテンプレートを作るために使ったのがCompiler APIです。

www.javainthebox.com

動的にテンプレートを作れるようになったのはいいのですが、微妙に使いにくい点があります。

たとえば、DynamicTemplate.jarにクラスをまとめて実行してみると。

C:\sample>dir
 ドライブ C のボリューム ラベルは OS です
 ボリューム シリアル番号は BCC7-8689 です

 C:\sample のディレクトリ

2023/12/23  22:21    <DIR>          .
2023/12/23  22:20             3,896 DynamicTemplate.jar
2023/12/21  21:09                60 hello.temp
               2 個のファイル               3,956 バイト
               1 個のディレクトリ  378,476,552,192 バイトの空き領域

C:\sample>java --enable-preview -cp DynamicTemplate.jar TemplateTest
ノート: /Template.javaはJava SE 22のプレビュー機能を使用します。
ノート: 詳細は、-Xlint:previewオプションを指定して再コンパイルしてください。
Exception in thread "main" java.lang.ClassNotFoundException: Template
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:525)
        at DynamicTemplateBuilder.createTemplate(DynamicTemplateBuilder.java:42)
        at TemplateTest.main(TemplateTest.java:13)

C:\sample>

といように、動的に作成したTemplateクラスをロードできないため、失敗しています。

この理由は簡単で、ワーキングディレクトリに生成されたTemplate.classファイルが出力されてしまっているからです。

C:\sample>dir
 ドライブ C のボリューム ラベルは OS です
 ボリューム シリアル番号は BCC7-8689 です

 C:\sample のディレクトリ

2023/12/23  22:24    <DIR>          .
2023/12/23  22:20             3,896 DynamicTemplate.jar
2023/12/21  21:09                60 hello.temp
2023/12/23  22:24               728 Template.class
               3 個のファイル               4,684 バイト
               1 個のディレクトリ  378,476,572,672 バイトの空き領域

C:\sample>

しかし、クラスパスはJARファイルしか指定していないため、クラスロードできなくなってしまいます。

これを動作させるためには、ワーキングディレクトリもクラスパスに追加すればOKです。

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

C:\sample>

このように実行はできるものの、クラスパスを毎回指定しなければいけないのは、ちょっとめんどうです。

そもそも自動生成したクラスなのですから、クラスファイルをファイルとして出力せずにヒープ内でどうにかしてくれないかなぁと思うわけです。

ところが、そんなことをしてくれる機能が実はすでにあるんです。

たとえば、実行すると単にSystem.outに"Hello, World!"を出力するHelloWorldクラスがあったとします。Java 11から、1つのJavaファイルであればコンパイルせずに直接実行できるようになっています。

C:\sample>dir
 ドライブ C のボリューム ラベルは OS です
 ボリューム シリアル番号は BCC7-8689 です

 C:\sample のディレクトリ

2023/12/23  22:53    <DIR>          .
2023/12/23  22:53               123 HelloWorld.java
               1 個のファイル                 123 バイト
               1 個のディレクトリ  378,463,444,992 バイトの空き領域

C:\sample>java HelloWorld.java
Hello, World!

C:\sample>dir
 ドライブ C のボリューム ラベルは OS です
 ボリューム シリアル番号は BCC7-8689 です

 C:\sample のディレクトリ

2023/12/23  22:53    <DIR>          .
2023/12/23  22:53               123 HelloWorld.java
               1 個のファイル                 123 バイト
               1 個のディレクトリ  378,464,772,096 バイトの空き領域

C:\sample>

実行してみれば分かりますが、Javaファイルを直接実行した場合、HelloWorld.classファイルは出力されていません。

この機能はコンパイルした結果をヒープに保持し、クラスロードはヒープから行うということを行っているからです。

この機能を行っているのが、java.compilerモジュールのcom.sun.tools.javac.launcherパッケージのクラス群です。

なので、このパッケージにあるクラスを使えば、DynamicTemplateもクラスファイルを出力せずに実行することができるはず... なのですが、残念なことにcom.sun.tools.javac.launcherパッケージは外部に公開されていないパッケージなのです(module-info.javaでexport指定されていないのです)。

しかたないので、これらを参考に作ってみましょう。

ここで作成したファイルはGistにおいておきます。


Gist: 動的にString Templateのテンプレートを作成し、ヒープだけでコンパイル、クラスロードまで行う例


まずはJavaFileManagerインタフェースです。

通常はStandardJavaFileManagerインタフェースを使用しますが、部分的に機能を拡張するために提供されているのがForwardingJavaFileManagerクラスです。

ForwardingJavaFileManagerクラスは、コンストラクタでベースとなるJavaFileManagerオブジェクトを指定します。そして、このベースとなるJavaFileManagerオブジェクトに処理を委譲するようになっています。

機能を拡張したい部分だけメソッドをオーバーライドすればいいというわけです。

ここでは、バイトコードを出力するためのgetJavaFileForOutputメソッドをオーバーライドします。

    private Map<String, byte[]> classBytes;

    public Map<String, byte[]> getClassBytes() {
        return classBytes;
    }

    private class ClassOutputBuffer extends SimpleJavaFileObject {
        private final String name;

        ClassOutputBuffer(String name) {
            super(toURI(name), Kind.CLASS);
            this.name = name;
        }

        @Override
        public OutputStream openOutputStream() {
            return new FilterOutputStream(new ByteArrayOutputStream()) {
                @Override
                public void close() throws IOException {
                    out.close();
                    ByteArrayOutputStream bos = (ByteArrayOutputStream)out;
                    classBytes.put(name, bos.toByteArray());
                }
            };
        }
    }

    @Override
    public JavaFileObject getJavaFileForOutput(JavaFileManager.Location location,
                                    String className,
                                    Kind kind,
                                    FileObject sibling) throws IOException {
        if (kind == Kind.CLASS) {
            return new ClassOutputBuffer(className);
        } else {
            return super.getJavaFileForOutput(location, className, kind, sibling);
        }
    }

出力用にSimpleJavaFileObjectクラスを派生させたClassOutputBufferクラスを用意して、それを出力用に使用します。

ClassOutputBufferクラスでは出力にByteArrayOutputStreamクラスを使用しているため、バイト配列に結果が出力されるわけです。このバイトコードはマップのclassBytesにクラス名と一緒に保持しておきます。

classBytes変数はgetClassBytesメソッドで取り出すことができます。


続いて、クラスローダーの方です。

バイト配列を利用してクラスロードするMemoryClassLoaderクラスを定義しました。このMemoryClassLoaderクラスはURLClassLoaderクラスのサブクラスになっています。

public final class MemoryClassLoader extends URLClassLoader {
    private final Map<String, byte[]> classBytes;

    public MemoryClassLoader(Map<String, byte[]> classBytes) {
        super(new URL[]{});
        this.classBytes = classBytes; 
    }

    @Override
    protected Class findClass(String className) throws ClassNotFoundException {
        byte[] buf = classBytes.get(className);
        if (buf != null) {
            classBytes.put(className, null);
            return defineClass(className, buf, 0, buf.length);
        } else {
            return super.findClass(className);
        }
    }
}

MemoryJavaFileManagerクラスで作られたバイトコードを保持したマップをコンストラクタで指定するようにしてあります。

findClassメソッドではマップのclassBytesにクラスがあれば、バイトコードであるバイト配列を取り出して、defineClassメソッドに渡すようにしています。

これで、ヒープからクラスロードができます。


最後にDynamicTemplateクラスの変更点を示します。

オレンジで示したところが、MemoryJavaFileManagerクラスを使用するところです。その前の行でStandardJavaFileManagerオブジェクトを取得しておき、そのオブジェクトをMemoryJavaFileManagerクラスのコンストラクタ引数に使用します。

これで、MemoryJavaFileManagerクラスで変更した部分以外の機能はStandardJavaFileManagerオブジェクトに委譲するようになります。

青字の部分がMemoryClassLoaderクラスに変更した部分です。先ほどのMemoryJavaFileMangerオブジェクトでバイトコードを保持させているマップをgetClassBytesメソッドで取得して、コンストラクタ引数にしています。

        // 仮想ファイルマネージャの取得
        try (StandardJavaFileManager fm
                = compiler.getStandardFileManager(null, null, null);
                var fileManager = new MemoryJavaFileManager(fm)) {
            
            
            // コンパイルするファイルの準備
            List<? extends JavaFileObject> fileobjs
                    = createJavaFileObjects(template, argName);

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

            // コンパイル
            if (task.call()) {
                // クラスのロード
//                ClassLoader loader = ClassLoader.getSystemClassLoader();
                ClassLoader loader = new MemoryClassLoader(fileManager.getClassBytes());
                Class<?> clss = loader.loadClass("Template");


では、実行してみましょう。

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

C:\sample>dir
 ドライブ C のボリューム ラベルは OS です
 ボリューム シリアル番号は BCC7-8689 です

 C:\sample のディレクトリ

2023/12/23  22:24    <DIR>          .
2023/12/23  23:18            16,248 DynamicTemplate.jar
2023/12/21  21:09                60 hello.temp
               2 個のファイル              16,308 バイト
               1 個のディレクトリ  378,393,817,088 バイトの空き領域

C:\sample>

クラスパスにワーキングディレクトリーを指定せずに実行することができました。

また、Template.classも出力されていません!

これで、ソースが文字列、バイトコードはヒープに作成することができ。また、ヒープ上に保持してあるバイトコードを利用してクラスロードをすることもできました。


動的にコード生成をする場合、ヒープだけでバイトコードを保持し、クラスロードできるようになれば、クラスパスなどを気にすることがなくなり、使う上でのハードルは下がるはずです。

ここでは、String Templateを例にとりましたが、汎用に使えるテクニックなので、動的にコードを生成したい場合にはぜひご活用ください。


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つに限定されているなどありますが、できることは確認できました!!

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


2023/12/21

Compiler API

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

本エントリーはJava Advent Calendarの21日目のエントリーです。

qiita.com


もともとの予定ではString Template その 3として、ちょっとマニアックな使い方を紹介するつもりだったのですが、そこで使用するAPIのCompiler APIをまず紹介しておきます。


Compiler APIは名前の通り、コンパイラーを使うためのAPIで、Java 6で導入されました。Javaのコンパイラーといえばjavacです。これをプログラムの中から使うためのAPIになります。

ところで、プログラム中にソースコードをコンパイルすることがあるのでしょうか?

 古いところだと、JSPがそうです。

JSPはHTML中にJavaのコードを埋め込んだようになっていますが、サーブレットエンジンによってJavaのサーブレットに変換され、動的にコンパイルして実行されます。

最近だと(と言っても、すでに10年前ですが)、ラムダ式がそうです。ラムダ式は実行中にソースコードが生成され、コンパイルして実行されます。

この動作はJava 7で導入された新しいバイトコードのInvokeDynamic(通称 indy)によるものです。

他にもindyを使って動的にソースコード生成し、コンパイルしてから実行されるものとして、文字列の + 演算子などがあります。

以前は、+ 演算子を使用した文字列連結は、コンパイル時にStringBuilderクラスを使用するコードに置き換えられていましたが、今は違います(StringBufferクラスを使っていたのは、さらに前の話です)。

今はindyを使用して、動的にソースコードを生成、コンパイル、実行と進みます。

このように、アプリケーションの動作中にJavaのコードをコンパイルするために使われるのがCompiler APIです。


ところで、Compiler APIは前述したようにJava 6で導入されたのですが、JSPなどJava 6以前からあるものはどうやってコンパイルしていたのでしょう。

その答えは、javacを直接コールしていたです。

意外かもしれませんが、javacはJavaで書かれていて、そのメインクラスはcom.sun.tools.java.Mainクラスです。モジュールが導入される前はtools.jarに含まれていましたが、今はjdk.compilerモジュールに含まれています。

このMainクラスのmainメソッドをコールすればコンパイルできます。当然ですが、mainメソッドの引数は、javacの引数です。

たとえば、Hello.javaをコンパイルするのであれば、次のように記述します。

import com.sun.tools.javac.Main;
 
public class CompileTest {
  public static void main(String... args) {
    try {
      String[] javacArgs 
          = new String[] {"Hello.java"};
      Main.main(javacArgs );
    } catch (Exception ex) {
      ex.printStackTrace();
    }
  }
}

では、実行してみましょう。

C:\sample>dir /b
CompileTest.class
CompileTest.java
Hello.java

C:\sample>java CompileTest

C:\sample>dir /b
CompileTest.class
CompileTest.java
Hello.class
Hello.java

Hello.javaがコンパイルされて、Hello.classが生成されました。

このように、javacを呼び出すのと同じことをプログラム中でできました。

では、なぜCompiler APIが必要なのでしょうか?

理由はいくつかあります。

  • Mainクラスのパッケージがcom.sun.tools.javacであり、今後公開され続けるとは限らない
  • コンパイルエラーが扱いにくい
  • コンパイル対象がファイルに限定される

java.compilerモジュールではMainクラスが含まれているcom.sun.tools.javacパッケージはexportされています。ただし、パッケージがcom.sunで始まることから分かるように、このパッケージは内部で使用すること想定しているパッケージです。このため、今後このパッケージがexportされなくなることも考えなくてはいけません。

また、Eclipseなどが提供しているjavacコンパイラなどを使用することもできません。

それよりも、標準的なインタフェースを定義して、実装を隠す方が使いやすくなります。

その他の2つの理由はjavacが標準出力やファイルを対象としているため、しかたありません。とはいえ、特に最後の理由はソースコードをプログラム中で自動生成してコンパイルする場合にちょっとやっかいです。

ということで、Compiler APIが導入されたわけです。

ただ、いろいろと柔軟な使い方ができるようになった一方で、コード量が増えてしまうのはいかんともしがたいところではあります。


Compiler API

Compiler APIでは、ソースコードをコンパイルするために、下に示したインタフェースを使用します。

  • コンパイラ: JavaCompilerインタフェース
  • コンパイルタスク: JavaCompiler.CompilationTaskインタフェース
  • 仮想ファイルマネージャ: JavaFileManagerインタフェース
  • 抽象ソースファイル: JavaFileObjectインタフェース
  • エラー処理: DiagnosticListenerインタフェース

これらのインタフェースはいずれもjava.compilerモジュールのjavax.toolsパッケージで定義されています。

ソースコードのコンパイルは、以下のような流れになります。

  1. JavaCompilerオブジェクトの取得
  2. 必要に応じてコンパイルエラー処理コールバックを記述
  3. 仮想ファイルマネージャを生成し、コンパイル対象のソースファイルを取得
  4. ファイルマネージャ、抽象ソースファイル、エラー処理を指定してコンパイルタスクを生成
  5. コンパイル

ここでは、必要最低限の説明にとどめるため、コンパイルエラー処理は省略します。

では、順々に説明していきましょう。


JavaCompilerオブジェクトの取得

Javaのコンパイラを表すJavaCompilerインタフェースは、javax.tools.Toolインタフェースのサブインタフェースです。

Toolインタフェースはアプリケーション中から何らかの処理を行うための汎用インタフェースで、標準ではJavaコンパイラのJavaCompilerインタフェースと、Javadocを処理するDocletを実行するDocumentationToolインタフェースが提供されています。

実際にコンパイルを行うにはJavaCompilerインタフェースのgetTaskメソッドでCompilationTaskオブジェクトを生成し、CompilationTaskオブジェクトのcallメソッドをコールします。

さて、このJavaCompilationオブジェクトを取得するには、javax.tools.ToolProviderクラスのgetSystemJavaCompilerメソッドを利用します。

ただし、getSystemJavaCompilerメソッドで取得できるのはOpenJDKが提供しているjavacコンパイラです。

    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
    if (compiler == null) {
      // 例外処理
    }

コンパイラが取得できない場合、getSystemJavaCompilerメソッドはnullを返すので必要に応じて例外処理をしてください。


仮想ファイルマネージャ

続いて、仮想ファイルマネージャを生成し、ソースファイルを用意します。

仮想ファイルマネージャを表すJavaFileManagerインタフェースは汎用のインタフェースなので、物理ファイルを扱う場合はサブインタフェースのStandardJavaFileManagerインタフェースを使用します。

StandardJavaFileManagerオブジェクトはJavaCompilerオブジェクトから取得することができます。物理ファイル以外のソースファイルを使用する場合は、JavaFileManagerインタフェースを実装したクラスを用意します。

ここでは、まず標準的な物理ファイルをコンパイルすることを考えます。

StandardJavaFileManagerオブジェクトを取得するには、JavaCompilerインタフェースのgetStandardFileMangerメソッドを使用します。

getStandardFileManagerメソッドは第1引数がコンパイルエラー処理のコールバックであるDiagnosticListenerインタフェースです。エラー処理を行わないのであれば、nullを指定します。

第2引数がロケール、第3引数が文字セットになります。これらは引数がnullの場合、デフォルトの値が使用されます。

また、JavaFileManagerインタフェースはAutoClosableインタフェースを実装しているので、try-with-resources形式でクローズすることができます。


次に抽象化したソースファイルです。こちらはJavaFileObjectインタフェースを使用します。

通常はソースファイルは1つとは限りません。複数のソースファイルはリストではなくIterableインタフェースで表します。Iterableインタフェースが使われていたのは、そういう時代だったからだと思ってくださいw

このIterableオブジェクトはStandardJavaFileManagerインタフェースのgetJavaFileObjectsメソッドで取得で取得できます。getJavaFileObjectsメソッドは可変長引数なので、ソースファイルを列挙していきます。

    // 仮想ファイルマネージャの取得
    // 第1引数 コンパイルエラー処理コールバック
    // 第2引数 ロケール nullだとデフォルトロケール
    // 第3引数 文字セット nullだとデフォルト文字セット
    try (StandardJavaFileManager fileManager 
      = compiler.getStandardFileManager(null, null, null)) {

      // 抽象ソースファイルの取得
      Iterable<? extends JavaFileObject> files 
        = fileManager.getJavaFileObjects("Hello.java");


コンパイルタスク生成とコンパイル

コンパイルタスクはJavaCompilerインタフェースのgetTaskメソッドで生成します。

このメソッドは引数が6個もあるので、指定するのがちょっとめんどうですが、使わないものはnullを指定しておけば大丈夫です。

CompilationTaskオブジェクトを生成出来たら、callメソッドでコンパイルを行います。コンパイルが成功すればtrueが戻ります。

    // コンパイルタスクの生成
    // 第1引数 コンパイラメッセージの出力 nullの場合System.errが使われる
    // 第2引数 仮想ファイルマネージャー
    // 第3引数 コンパイルエラーのコールバック 指定しない場合はnull
    // 第4引数 コンパイラオプション 指定しない場合はnull
    // 第5引数 アノテーションプロセッサ 指定しない場合はnull
    // 第6引数 ソースファイル群
    JavaCompiler.CompilationTask task = compiler.getTask(
            null, fileManager, null, null, null, files);
    
    // コンパイル
    // 成功すればtrueが戻る
    var result = task.call();

上記のコードではHello.javaをコンパイルしているので、実行すればHello.classが出力されるはずです。


String Templateを含んだソースをコンパイル

コンパイルできるようになったので、String Templateを含んだソースをコンパイルしてみましょう。

Hello.javaを次のようにしたとします。

public class Hello {
  public void sayHello(String name) {
    System.out.println(STR."Hello, \{name}!");
  }
}

これで実行してみましょう。

C:\sample>java CompilerTest
Hello.java:3: エラー: 文字列テンプレートはプレビュー機能であり、デフォルトで無効になっています。
        System.out.println(STR."Hello, \{name}!");
                           ^
  (文字列テンプレートを有効にするには--enable-previewを使用します)
エラー1個

String TemplateはJava 22ではプレビュー機能なので、コンパイルが失敗してしまいました。

これを解決するのは簡単で、CompilationTaskオブジェクトを生成する時に、コンパイルオプションを指定するだけです。

    // コンパイルタスクの生成
    // 第1引数 コンパイラメッセージの出力 nullの場合System.errが使われる
    // 第2引数 仮想ファイルマネージャー
    // 第3引数 コンパイルエラーのコールバック 指定しない場合はnull
    // 第4引数 コンパイラオプション 指定しない場合はnull
    // 第5引数 アノテーションプロセッサ 指定しない場合はnull
    // 第6引数 ソースファイル群
    JavaCompiler.CompilationTask task = compiler.getTask(
            null, fileManager, null,
            List.of("--enable-preview", "--release", "22"),
            null, files);
    
    // コンパイル
    // 成功すればtrueが戻る
    var result = task.call();

--release 22は1つの意味を示すオプションですが、タスクを生成する時は別々に指定するようにします。

これでコンパイルできるはずです。


文字列をソース対象としてコンパイルする

ここまでは、ソースはファイルとして扱っていました。

これに対し、文字列をソースとして扱うこともできます。

ソースファイルを表すJavaFileObjectオブジェクトは仮想ファイルマネージャStandardJavaFileManagerオブジェクトから取得していましたが、文字列をソースとして扱う場合はJavaFileObjectオブジェクトを自分で生成しなくてはなりません。

JavaFileObjectインタフェースを実装したクラスとして、SimpleJavaFileObjectクラスが提供されています。このSimpleJavaFileObjectクラスでは、ソースコードを返すメソッドとしてgetCharContentというメソッドが定義されています。SimpleJavaFileObjectクラスの実装ではUnsupportedOperationException例外をスローするようになっているので、このメソッドをオーバーライドします。

この拡張に関してはJavaCompilerインタフェースのAPIドキュメントに書いてあるので、それをそのまま使います。

public class StringJavaFileObject extends SimpleJavaFileObject {
  private String content;

  public StringJavaFileObject(String className, String content) {
    super(URI.create("string:///"
                    + className.replace('.', '/')
                    + Kind.SOURCE.extension),
          Kind.SOURCE);

    this.content = content;
  }

  @Override
  public CharSequence getCharContent(boolean ignoreEncodingErrors) {
    return content;
  }
}

StringJavaFileObjectクラスを使用して、抽象ソースを生成します。

    String HELLOCLASS = """
        public class Hello {
            public void sayHello(String name) {
                System.out.println("Hello, " + name + "!");
            }
        }
        """;

    // 文字列を抽象ソースとして扱う
    Iterable<? extends JavaFileObject> files 
            = List.of(new StringJavaFileObject("Hello", HELLOCLASS));

他の部分は先ほどと同じで大丈夫です。

これで実行すれば、Hello.classが作成されるはずです。

思ったよりも簡単にできたと思いませんか。javacのMainクラスを直接使うよりも、Compiler APIを使うとこのような応用的な使い方が簡単にできるのです。


ということで、Compiler APIの簡単な紹介でした。次回はCompiler APIを使ったString Templateのちょっとマニアックな使い方を紹介します。


ここで使用したソースはGistにのせてあります。

Compiler APIサンプル その1

Compiler APIサンプル その2 文字列をソースにする

2023/12/12

String Templateによる文字列補間 その2 - カスタムテンプレートプロセッサー

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

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

qiita.com


前回のエントリーで、String Templateの基本的な使い方を紹介しました。

www.javainthebox.com

基本的にはSTRを使って、\{}を使って式を埋め込めばOKです。ただ、STRの直後にピリオドで文字列という書き方は今までのJavaとは違うので、ちょっと違和感はあるかもしれません。

今日はその後半です。発表資料だとp15ぐらいからの内容です。


文字列補間はたいていのプログラミング言語で使うことができる当たり前の機能です。今までJavaになかったのが不思議なくらい。

しかし、後発だからこそ他にはあまりない機能も盛り込んでいます。それがテンプレートプロセッサーのカスタム化です。

つまり、STRと同じようなテンプレートプロセッサーを簡単に作ることができるのです。本エントリーでは、このテンプレートプロセッサーのカスタム化について説明していきます。


テンプレートプロセッサーの動作

カスタム化について説明する前に、テンプレートプロセッサーの動作を確認しておきましょう。

前回、テンプレートプロセッサーであるStringTemplate.Processorインタフェースのprocessメソッドで文字列補間を行うと紹介しました。

ところで、processメソッドの引数の型はStringTemplateインタフェースです。ということは、どこかでStringTemplateオブジェクトが生成されているはずです。

実をいうと、String Templateは、バイトコードだとinvokeDynamicで実行されます。最終的にはProcessor.processメソッドをコールしますが、その前の処理を含めてinvokeDynamicで処理されています。

実をいうとSTR、RAW、FMT、そしてカスタムテンプレートプロセッサーでInvokeDynamicの動作がすべて異なります。それらをすべて解説するのはちょっと深入りしすぎるので、共通的に行われている処理の手順だけを説明しておきます。

たとえば、次のString Templateの処理を考えてみます。

STR."Hello, \{name}!";

このテンプレート引数である "Hello, \{name}!" は、まず文字列の部分と値の部分にバラバラに分解されます。

"Hello, " name "!"

次に文字列だけを集めたリストfragmentsと、値の部分を集めたリストvaluesに集約されます。

List<String> fragments: "Hello, ", "!"

List<Object> values: name

そして、fragmentsとvaluesを使用してStringTemplateオブジェクトを生成します。

var template = StringTemplate.of(fragments, values);

最後に、生成したStringTemplateオブジェクトを引数にしてテンプレートプロセッサーのprocessメソッドをコールします。

STR.process(template);

重要なのはStringTemplateオブジェクトは文字列部分を集めたリストfragmentsと、値を集めたリストvaluesを持つということです。

テンプレートプロセッサーは、これら2つのリストを使用して文字列の補間を行います。

なお、テンプレート引数の先頭が\{}の場合、fragmentsの先頭は""になります。また、末端が\{}で終わった場合、fragmentsの末尾は""になります。つまり、fragmentsのサイズは必ずvaluesより1つ多くなります。


カスタムテンプレートプロセッサー

テンプレートプロセッサーを自作するには2つの方法があり、下記の2つのメソッドのいずれかを使用します。

  • static <T> Processor<T, RuntimeException> of(Function<? super StringTemplate, ? extends T> process)
  • R process(StringTemplate stringTemplate) throws E

ofメソッドはProcessorオブジェクトを生成するファクトリーメソッドです。引数には文字列補間を行うラムダ式を指定します。

2つめの方法は、Processorインタフェースを実装したクラスを作成して、processメソッドをオーバーライドします。ただし、Processorインタフェースは関数型インタフェースなので、こちらもラムダ式で記述できます。

これらのメソッドの使い分けは、例外を扱うかどうかです。ofメソッドの戻り値の型の2つめの型パラメータはRuntimeException例外に固定されています。

一方のprocessメソッドは型パラメータEが例外を示しており、任意の例外をスローすることができます。

つまり、例外を扱わないでおくにはofメソッド、例外を扱いたいのであればprocessメソッドを使います。

では、まずofメソッドを使う方法から試してみましょう。


ofメソッドを使用したカスタム化

ofメソッドの引数のラムダ式は引数はStringTemplateオブジェクトですが、戻り値は任意に決めることができます。

まずはSTRと同じ動作をする、つまり文字列を戻すテンプレートプロセッサーを作ってみましょう。

StringTemplateインタフェースはfragmentsを戻すfragmentsメソッド、valuesを戻すvaluesメソッドが定義されています。これらのメソッドを使用すれば、STRと同じことができるはずです。

コードはこんな感じです。

    StringTemplate.Processor<String, RuntimeException> proc
        = StringTemplate.Processor.of(st -> {

        // fragments()とvalues()を使用して文字列補間

        return // 補間結果の文字列を戻す
    });

このラムダ式で文字列補間処理を行います。

STRと同じ処理であるのであれば、fragmentsとvaluesを1つづつ取り出し、文字列連結をすればよいことになります。

    StringTemplate.Processor<String, RuntimeException> proc
        = StringTemplate.Processor.of(st -> {

        var fragments = st.fragments();
        var values = st.values();

        var builder = new StringBuilder(fragments.get(0));
        for (int i = 0; i < values.size(); i++) {
            builder.append(values.get(i));
            builder.append(fragments.get(i+1));
        }

        return builder.toString();
    });

個人的にはこういうミュータブルなリストを使ったコードはもう書きたくないのですが、Streamインタフェースにzipメソッドがないので、しかたありません。

ところで、ほとんどのテンプレートプロセッサーはfragmentsとvaluesを順々に連結する処理を行います。このため、StringTemplateインタフェースには、この処理を行うinterpolateメソッドが定義されています。

interpolateメソッドを使用して、上のコードを書き直してみましょう。

    StringTemplate.Processor<String, RuntimeException> proc
        = StringTemplate.Processor.of(StringTemplate::interpolate);

かなりスッキリしました。

これだとSTRとまったく同じなので、たとえばテンプレート引数の文字列の部分がアルファベットであれば、それをすべて大文字にしてみましょう。

これにはfragmentsを大文字に変換してから、interpolateメソッドをコールするようにします。

    StringTemplate.Processor<String, RuntimeException> proc
        = StringTemplate.Processor.of(st -> StringTemplate.interpolate(
                st.fragments().stream().map(String::toUpperCase).toList(),
                st.values()));

簡単にできますね。


processメソッドのオーバーライドによるカスタム化

processメソッドをオーバーライドしたProcessorインタフェースの実装クラスを作るというと大げさな感じですが、実際にはofメソッドの引数で指定したラムダ式とほぼ同じです。

STRと同じ動作をさせるのであれば、interpolateメソッドを使用して次のように記述できます。

    StringTemplate.Processor<String, RuntimeException> proc
        = st -> st.interpolate();

さらにメソッド参照を使用すれば、さらに簡潔に記述できます。

    StringTemplate.Processor<String, RuntimeException> proc
        = StringTemplate::interpolate;

これだけだとofメソッドの場合と同じなので、例外を扱えるようにしてみましょう。

STRは値がnullだった場合、nullと表記されます。

jshell> String name = null
name ==> null

jshell> STR."Hello, \{name}!"
$2 ==> "Hello, null!"

もちろん、これはこれでいいのですが、たとえば値がnullの場合、NullPointerException例外をスローするようにしてみましょう。

    StringTemplate.Processor<String, NullPointerException> proc
        = st -> {
            if (st.values().contains(null)) {
                throw new NullPointerException("Value is NULL");
            }
            return st.interpolate();
        };

NullPointerException例外をスローするので、Processorインタフェースの型パラメーターをRuntimeException例外からNullPointerException例外に変更してあります。

そして、ラムダ式の中でvaluesにnullが含まれるかどうかをチェックし、含まれていればNullPointerException例外をスロー、含まれていなければinterpolateメソッドをコールします。


また、カスタムテンプレートプロセッサーは文字列以外のオブジェクトを生成することも可能です。

たとえば、JSONを直接扱うこともできます。

JSON in Javaを使って、JSONObjectオブジェクトを生成するテンプレートプロセッサーは次のようになります。

    StringTemplate.Processor<JSONObject, JSONException> JSON
        = st -> new JSONObject(st.interpolate());

Processorインタフェースの第1型パラメータがJSONObject、第2型パラメータがJSONExceptionになっていることに注意してください。

後は、JSONObjectクラスのコンストラクタ引数に文字列補間の結果を渡すだけです。

使い方は今までのテンプレートプロセッサーと同じです。

    var name = "Sakuraba";
    var city = "Tokyo";
    
    JSONObject jsonObj = JSON."""
        {
            "name": "\{name}",
            "city": "\{city}"
        }
        """;

これまでJSONObjectオブジェクトを生成するために文字列を作るのがめんどうだったのが、簡単にできるようになりました。

カスタムテンプレートプロセッサーはいろいろと使い道がありそうですね。たとえば、以下のような使い方がありそうです。

  • SQL
  • JSON
  • HTML/CSS/XML
  • ログメッセージ
  • エラーメッセージ

定型的な文字列処理になるような部分は、テンプレートプロセッサーで担えそうです。


文字列補間の危険性

文字列補間は便利なのですが、その反面、危険性を伴うこともあります。

安易に文字列補間を行った結果、SQLインジェクションやクロスサイトスクリプティングなどの原因になってしまうことがあります。

たとえば、SQLインジェクションについて見てみましょう。

    String query = STR."SELECT * FROM Person p WHERE p.name = '\{name}'";

このテンプレートに対し、変数nameが次の値だったらどうでしょう。

    name = "Sakuraba' OR p.name <> 'Sakuraba'";

名前がSakurabaでもSakuraba以外でもOKになってしまい、結果登録されている全件がクエリー結果になってしまいます。

このような問題に対し、カスタムテンプレートプロセッサーで対応することもできます。

たとえば、次のような手法が考えられます。

  • 使用できない文字を置き換える
  • 使用できない文字が含まれていれば例外をスロー
  • 型で制限する

まず、使用できない文字を置き換えるテンプレートプロセッサーを作ってみましょう。

valuesの値に使用できない文字があれば、置き換える処理を加えればできそうです。

    StringTemplate.Processor<String, RuntimeException> PROC = st -> {
        List<String> replacedValues
            = st.values().stream()
                         .map(v -> v.toString().replace("'", "\\'"))
                         .toList();
        return StringTemplate.interpolate(st.fragments(), replacedValues);
    };

これで、先ほどのクエリーは次のように変換されました。

jshell> StringTemplate.Processor<String, RuntimeException> PROC = st -> {
   ...>             List<String> replacedValues
   ...>                 = st.values().stream()
   ...>                              .map(v -> v.toString().replace("'", "\\'"))
   ...>                              .toList();
   ...>             return StringTemplate.interpolate(st.fragments(), replacedValues);
   ...>     };
PROC ==> $Lambda/0x000002380c00a000@60c6f5b

jshell> var name = "Sakuraba' OR p.name <> 'Sakuraba'"
name ==> "Sakuraba' OR p.name <> 'Sakuraba'"

jshell> var query = PROC."SELECT * FROM Person p WHERE p.name = '\{name}'"
query ==> "SELECT * FROM Person p WHERE p.name = 'Sakuraba\ ... p.name <> \\'Sakuraba\\''"

jshell> System.out.println(query)
SELECT * FROM Person p WHERE p.name = 'Sakuraba\' OR p.name <> \'Sakuraba\''

ここではクオーテーション'が\'に置き換えられていることが分かります。

2番目の方法はそもそも置き換えられるような文字が入っていることがまちがっているということです。この場合は、例外をスローするようにします。

    StringTemplate.Processor<String, IllegalArgumentException> PROC = st -> {
        st.values().stream()
                   .map(v -> v.toString())
                   .filter(v -> v.contains("'"))
                   .findFirst()
                   .ifPresent(s -> {
                       throw new IllegalArgumentException(STR. "Illegal Text: \{s}" );
                   });

        return st.interpolate();
    };

ストリームでvaluesの中に使用できない文字が含まれていないか調べ、含まれていたらIllegalArgumentException例外をスローするようにしてあります。

使用できない文字が含まれていなければ、interpolateメソッドで文字列補間を行います。

ここまでの2種類の方法は、文字列補間の時にバリデーションをして対応する手法です。しかし、そもそもバリデーションはもっと早い段階、たとえばWebからの入力を受け取った時に行うこともできるはずです。

バリデーションを行って、適切なデータ型に変換し、カスタムテンプレートプロセッサーではそのデータ型に制限して処理を行うのが最後の方法です。

たとえば、name属性を持つPersonレコード( record Person(String name){} )があったとしましょう。カスタムテンプレートプロセッサーは、このPersonレコードだけを使用できるようにします。

    StringTemplate.Processor<String, IllegalArgumentException> PROC = st -> {
        var values = st.values().stream()
           .map(v -> {
               if (v instanceof Person(var name)) {
                   return name;
               } else {
                   throw new IllegalArgumentException(STR."Not Person class \{v.getClass()}: \{v}");
               }
           })
           .toList();

           return StringTemplate.interpolate(st.fragments(), values);
    };

型の比較には、Java 21で導入されたパターンマッチングを使用しています。


システムによってどのようにバリデーションを行うかの方針は異なると思いますが、その方針に応じてカスタムテンプレートプロセッサーを作成して、文字列補間の危険性を下げていくことができるはずです。


このように、いろいろと便利なString Templateなのですが、使いにくい点が1つだけあります。それはテンプレート引数が文字列リテラルもしくはテキストブロックしか使えないところです。

通常のテンプレートエンジンであれば、テンプレートをファイルから読み込むなど、任意の文字列をテンプレートにすることができます。

しかし、String Templateではそれができません。でも、できないと言われると、やりたくなりますよね。

そこで、次回は動的にテンプレート引数を作成する方法を紹介します。


2023/12/11

String Templateによる文字列補間 その1 - 基本的な使い方

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

本エントリーはJava Advent Calendarの11日目です。

qiita.com


11月11日にJJUG CCC 2023 Fallが開催されました。

ccc2023fall.java-users.jp


今回、取り上げたのはString Templateです。Java 22では、Second PreviewのJEP 459が入る予定です。

openjdk.org

String Templateは文字列中に式を埋め込んで、文字列の補間を行う機能です。発表資料はこちら。


導入の背景

文字列と変数を組み合わせて、新たに文字列を作ることは、Javaを書いたことがある開発者であれば多々あると思います。

たとえば、Hello, World!のWorldの部分を変数nameで変更できるようにするには

  1. "Hello, " + name + "!"
  2. new StringBuilder("Hello ").append(name).append("!").toString()
  3. String.format("Hello, %s!");
  4. new Formatter().format("Hello, %s!", name).toString();
  5. MessageFormat.format("Hello, {0}!", name);

などの書き方があります。

たぶん、変数が少なければ1や2の方法が使われて、フォーマットをちゃんとしたい場合は3以降の方法になるのではないでしょうか。

ちなみに、1の方法はコンパイルするとかつては2に置き換えられましたが、現在のJavaではInvokeDynamicによって実行時に動的に処理が行われるようになっています。

さて、このようにいろいろと方法はあるのですが、どれも一長一短。

1や2の方法は変数が少なければいいのですが、多くなってくると書くのがめんどうくさい。

3はformatメソッドの内部で4と同じことをしているので、3と4は実質的には同じ手法です。

3からの5の手法は文字列に埋め込む場所(フォーマット記述子)と変数を別々に書かなくてはなりません。フォーマットを指定できるので、表現力は1や2に比べると高くなります。

しかし、変数が多くなると、変数とフォーマット記述子の組み合わせが分かりにくくなる欠点があります。

特に5のMessageFormatは変数の並び順とは異なる順番でも埋め込むことができるので、なおさら組み合わせを間違いやすくなります。ちなみに、3と4でも変数の並び順と埋め込む場所を別々に指定できますが、普通はやらないですね。

もっと簡単に、しかも間違いも起こしにくい方法があればいいですね。


こういう時に、今まで使われてきたのがテンプレートエンジンです。

古くはJSPやJSFなどがありますし、Spring FWではThymeleafがよく使われているようです。HTMLのレンダリングなどではテンプレートエンジンが使えるとは思いますが、テンプレートエンジンを使うほどではない文字列補間も多くあります。

そこで、簡単にしかも言語仕様として新たに導入されるのがSpring Templateです。


String Template

String Templateは文字列中に式を埋め込み、文字列補間を行う言語仕様とAPIです。

たとえば、先ほどのHelloの例だと

"Hello, " + name + "!";

を、String Templateを使うと次のように記述することができます。

STR."Hello \{name}!";

ここで、STRはテンプレートプロセッサーと呼ばれます。具体的には、java.lang.StringTemplate.Processorインタフェースです。このStringTemplate.Processorインタフェースが文字列補間の処理を行います。

標準で提供されているStringTemplate.ProcessorはSTRのほかにFMTとRAWがあります。これらについては後で説明します。

ピリオドの後の文字列リテラルがテンプレート引数と呼ばれます。ここでは文字列リテラルを使用しましたが、複数行の文字列を記述できるテキストブロックも使用することができます。

また、\{ }に式を埋め込むことができます。

定数の後にカッコもなく、いきなり文字列リテラルを書くなんて、変な感じがしますが、これが新しいString Templateの書き方になります。

String TemplateはまだPreviewなので、Java Language Specification (JLS)に正式に記述が追加されたわけではありませんが、JSLの変更分が提案されています。

docs.oracle.com

このリンクはJava 21のものですが、Java 22でもたぶん同じようになるはずです。

変更点はいろいろありますが、String TemplateのメインとなるのはJLS 15.8.6 Template Expressionsです。

JLS 15.8.6はJEP 459のSyntax and semanticsに書いてあることとほぼ同等なので、こちらを読めば大丈夫です。


StringTemplate.Processor

前述したようにテンプレートプロセッサーが文字列補間を行う主体です。インタフェースとしてはjava.lang.StringTemplate.Processorインタフェースです。

標準で提供されているテンプレートプロセッサーは次の3種類です。

  • java.lang.StringTemplate.STR
  • java.util.FormatProcessor.FMT
  • java.lang.StringTemplate.RAW

それぞれを簡単に説明していきます。


StringTemplate.STR

STRは最も基本的な文字列補間を行うテンプレートプロセッサーです。

STRはStringTemplateインタフェースの定数ですが、static importをしなくても使用することができます。

\{}に埋め込んだ式はプリミティブの場合はその値がそのまま使われ、参照型(オブジェクト)の場合toStringメソッドの結果が使われます。

文字列補間した結果は文字列になります。

jshell> var x = 10
x ==> 10

jshell> var y = 20
y ==> 20

jshell> STR."\{x} + \{y} = \{x+y}"
$3 ==> "10 + 20 = 30"

jshell> record Name(String first, String last) {}
|  次を作成しました: レコード Name

jshell> var name = new Name("Yuichi", "Sakuraba")
name ==> Name[first=Yuichi, last=Sakuraba]

jshell> STR."My name is \{name.first()} \{name.last()}."
$6 ==> "My name is Yuichi Sakuraba."

jshell> STR."""
   ...> {
   ...>   "firstName": "\{name.first()}",
   ...>   "lastName":  "\{name.last()}"
   ...> }
   ...> """
$7 ==> "{\n  \"firstName\": \"Yuichi\",\n  \"lastName\":  \"Sakuraba\"\n}\n"

jshell> System.out.println($7)
{
  "firstName": "Yuichi",
  "lastName":  "Sakuraba"
}
 

式であればよいので、\{}の中にswitch式なども記述できます。また、\{}は複数行になってもかまいません。

たとえば、下のようにも書くことができます。

jshell> import java.time.*

jshell> var today = LocalDate.now()
today ==> 2023-12-11

jshell> STR."""
   ...>    Today is \{today}, \{switch (today.getDayOfWeek()) {
   ...>       case SATURDAY, SUNDAY -> "Weekend";
   ...>       default -> "Weekday";
   ...> }}."""
$10 ==> "Today is 2023-12-11, Weekday."
 

書けることは書けますが、可読性がとても低下するので、こういう書き方はしない方がいいと思います。


FormatProcessor.FMT

FMTは文字列補間の結果が文字列になる点ではSTRと同じです。しかし、値の表示にjava.util.Formatterクラスと同じフォーマット記述子を使用することができます(厳密には完全に同じではないですが...)。

フォーマット記述子は\{}の直前に記述します。

なお、FMTはSTRは異なり、使用するにはstatic importが必要です。

jshell> import static java.util.FormatProcessor.FMT

jshell> var x = 10
x ==> 10

jshell> var y = 20.0
y ==> 20.0

jshell> FMT."%d\{x} / %.2f\{y} = %08.3f\{x/y}"
$5 ==> "10 / 20.00 = 0000.500"

jshell> FMT."Today is %tA\{LocalDate.now()}."
$6 ==> "Today is Mon."

最後の曜日のフォーマットで分かると思いますが、ロケールにja_JPが使用されていません。

FMTのロケールはLocal.ROOTになるので注意が必要です。

任意のロケールで使用する場合はFormatProcessorクラスのcreateメソッドでテンプレートプロセッサーを生成する必要があります。

jshell> var PROC = FormatProcessor.create(Locale.of("ja", "JP"))
PROC ==> java.util.FormatProcessor@2038ae61

jshell> PROC."Today is %tA\{LocalDate.now()}."
$10 ==> "Today is 月曜日."


StringTemplate.RAW

STRとFMTは処理の結果が文字列になりましたが、RAWだけは異なります。

RAWが返すのは、StringTemplateオブジェクトです。

ここまでテンプレートプロセッサーが何かということを触れずに来ましたが、テンプレートプロセッサーであるStringTemplate.Processorインタフェースはprocessメソッドだけ定義するインタフェースです(staticメソッドのofメソッドも定義していますが...)。

public interface StringTemplate {
        ...

    @FunctionalInterface
    public interface Processor<R, E extends Throwable> {
        R process(StringTemplate stringTemplate) throws E;

        static <T> Processor<T, RuntimeException> 
            of(Function<? super StringTemplate, ? extends T> process) { ... }

        ...
    }
}

このprocessメソッドが文字列補間を行うメソッドになり、その引数の型がStringTemplateインタフェースとなるわけです。

RAWで処理した結果はStringTemplateオブジェクトなので、他のテンプレートプロセッサーのprocessメソッドの引数にすることができます。

つまり、下に示すようなことができます。

jshell> var name = "Java"
name ==> "Java"

jshell> var st = RAW."Hello, \{name}!"
st ==> StringTemplate{ fragments = [ "Hello, ", "!" ], values = [Java] }

jshell> st instanceof StringTemplate
$15 ==> true

jshell> STR.process(st)
$16 ==> "Hello, Java!"

RAWで処理した結果をSTRの引数にしています。

このままだとあまり役に立つようには思えないかもしれませんが、テンプレートプロセッサーをカスタム化する場合に使えます。


長くなってしまったので、テンプレートプロセッサーのカスタム化は次のエントリーで説明することにします。

2023/12/01

Devoxx Belgium 2023 その2 セッション

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

前回に引き続き、Devoxxの参加記です。

なお、本エントリーはJava Advent Calendarの1日目のエントリーになります。

qiita.com


さて、後編ではDevoxxで、さくらばが聴講したセッションの中で面白かったセッションを紹介します。私が聴講するのはJava言語に関するセッションか、JVMに関するセッションばかりで、かなり偏っています。


なお、DevoxxのセッションはすべてYoutubeで公開されいます。

www.youtube.com

本エントリーでもセッション動画のリンクを貼っておきます。


Deep Dive

まずは、1日目と2日目に行われたDeep Diveのセッションです。


You. Me. Java 21

OracleのアドボケイターのNicolai ParlogによるJava 21を俯瞰的に説明するセッションです。

NicolaiはOracleのJava公式チャネルでいろいろと動画を公開していますけど、ちょっと釣り気味なところが鼻につくのですが、聞き取りやすい英語ですし、Java 21の概要を知るにはちょうどいいと思います。

ただ、途中で休憩があるものの3時間近いので、手っ取り早くJava 21について知りたいのであれば後述するBrian Goetzのキーノートセッションの方がいいと思います。


Value types and Pattern matching: bringing your data back on stage

OracleのアドボケイターのJosé Paumardと、Universite Gustave EiffelのRemi ForaxによるProject Valhallaの紹介セッション。

JoséはJava公式チャネルでJEP Cafeの動画に出ている人ですが、もともと大学の先生だったので、2人ともアカデミック系な人です。

ValhllaではValue Typeという新しい型を導入しようとしていますが、それについてのセッションです。ただし、Value Typeは正式に導入されるまで、まだまだ紆余曲折ありそうなので、今のところはイメージでとらえるのがいいと思います。


With Java 21, Your Code Runs Even Faster. But How is that Possible?

OracleでHotSpotグループに属しているPer Minborgのセッション。

Perさん、Oracleの開発者では数少ないブログを続けてくれている方です。ブログはよく読んでいたのですが、ご本人を見たのははじめて。

minborgsjavapot.blogspot.com


JVM的にパフォーマンスのいいコードはどのように書けばいいかという話。

なお、このセッションはDeep Diveの日でしたが、Tools-in-Actionというタイプの30分のセッションです。


Keynote

Keynoteは3日目の水曜日の午前中に行われました。

やっぱり注目はBrian Goetzですね。


Java 21

JavaのチーフアーキテクトであるBrian Goetzのキーノートセッション。

Brianが今のJavaの方向性を決めているといっても過言ではないです。

Java 21を俯瞰する内容なので、キャッチアップするにはちょうどいいかもしれません。とはいうものの、Brianはものすごく早口なので、追いつくだけでもたいへんですが。


Conference Session

3日目の午後からの通常のセッションがはじまります。

Deep Diveと異なり、セッションの時間は50分です。


Game of Loom 2: life and dead(lock) if a virtual thread

Red HatでQuarkusなどを担当されているMario FuscoのVirtual Threadのセッション。

QuarkusはすでにVirtual Threadに対応していますが、実際に担当されていた方の話はなかなか説得力があります。

1つのスレッドでデッドロックになってしまうなんて事象は、はじめてききましたw


Teaching old Streams new tricks

OracleのHot Spotグループに属しているViktor KlangのStreamのセッション。

JVMではなく、ライブラリに関するセッションで、現在提案されているStreamのGathere (JEP 461)などを紹介していました。


The Panama Dojo: Black-Belt Programming with Java 21 and the FFM API

再び、Per Minborg。

FFMはForeign Function & Memory APIのことです。Java 22でやーーーーーっと正式な機能として取り込まれる予定のFFMの紹介。

後半のサンプルは、イマイチついていけなかったのですが、FFMに関する情報がまだ少ないので、貴重です。


Java Language update

Brian Goetzはキーノート以外にも複数のセッションを担当されていたのですが、最後の最後のセッションがこちら。

まだJEPとして提案していないことなどを含めて、今検討中の機能について紹介してくれました。ここまで話してくれるのはなかなかないことかもしれません。

なお、この内容については11月22日に行われたJJUGナイトセミナーで伊藤さんが紹介してくれています。

資料はこちら。


動画がすべて公開されているので、わざわざ高いお金を払って海外までカンファレンスに参加するのはなんでなんででしょうね。

生の声を集中して聞けるのは、実際にカンファレンスに参加しているからだと思うのです。動画を見るのは受動的で集中力も落ちがちじゃないですか。少なくとも私はそうです。

どこに力を置いて話しているのかとか、何を強調したいのかなど、動画だとなかなか分からなくて...

しかもセッションがこれだけ多いと、動画を探すのも一苦労です。

時間があいたら普段は聞かないようなセッションを聴講できるのも、実際にカンファレンスに参加しているからですね。ここでは紹介しませんでしたけど、Nettyのセッションとか普段は絶対に聞かないと思うのですが、なかなかおもしろかったです。

といろいろ理由書きましたけど、やっぱりカンファレンスに参加するの楽しいんですよ。

日常とは異なる環境で技術にどっぷりつかった1週間なんて、なかなかないですから。


例年、JavaOneとDevoxxが近い日程で開催されるので、来年JavaOneが復活すればJavaOneに行ってしまうかもしれません。もし、行ってみたいという方がいらっしゃったら、ぜひお声がけください。

また、海外でなくても、日本国内、たとえばJJUGのカンファレンスに参加するのでも全然かまわないので、ぜひ実際にカンファレンスに足を運んでいただけれと思っています。