CLOVER🍀

That was when it all began.

JAX-RS 3.1.0のSeBootstrapを使って、RESTEasy(+Undertow、CDI)をJava SE環境で動かす

これは、なにをしたくて書いたもの?

RESTEasy 6.1.0に関するブログを見ていて、JAX-RS(Jakarta RESTful Web Services) 3.1ではJava SE環境でJAX-RSを動かすことが
できるようになっていたことに気づいたので、少し試してみようかなと。

RESTEasy Releases

RESTEasy 6.1.0.Final Release

SeBootstrap

Java SE環境でJAX-RS 3.1を使うには、SeBootstrapというクラスを使うようです。

以下のブログエントリーで登場し、「Example SeBootstrap Usage」にサンプルコードがあります。

RESTEasy Releases

JAX-RS 3.1.0の仕様。

Jakarta RESTful Web Services 3.1.0 / Applications / Publication / Java SE / Java SE Bootstrap

SeBootstrapの記述があり、Java SE環境でJAX-RS実装により起動された組み込みHTTPサーバーを使って、アプリケーションを公開できる
仕組みだとされています。

設定に関しては、SeBootstrap.Configurationを使って行えるようです。

ベンダー間でコードに移植性があるので、Java SE環境でJAX-RSを動かす時にはSeBootstrapの使用が推奨されているようです。

このように書くと別の方法もありそうなのですが、createEndpointとRuntimeDelegateというものに関する記述がありました。

Jakarta RESTful Web Services 3.1.0 / Applications / Publication / Java SE /

この方法を使ってJAX-RSのエンドポイントを公開する方法は仕様の範囲外らしく、RESTEasyのドキュメントにも記載がないので
今回はパスします。

RESTEasy 6.1.0のリリースに関するブログを見ると、SeBootstrapを使う時に組み合わせられるアーティファクトが以下のように
記載されています。

  • org.jboss.resteasy:resteasy-undertow-cdi
    • 推奨
  • org.jboss.resteasy:resteasy-undertow
    • 推奨
  • org.jboss.resteasy:resteasy-netty4
  • org.jboss.resteasy:resteasy-vertx
    • SSL実装なし

RESTEasy 6.1.0.Final Release

resteasy-netty4-cdiは入ってないんですね(実際、resteasy-undertow-cdiと入れ替えてみたらCDIの部分が動かなかったのでそうなんだなと)。

なお、RESTEasyのドキュメントでSeBootstrapに関する記載があるのはこちらです。

RESTEasy / Jakarta RESTful Web Services SeBootstrap

では、ドキュメントを見るのはこれくらいにして、実際に使っていってみましょう。

環境

今回の環境は、こちらです。

$ java --version
openjdk 17.0.5 2022-10-18
OpenJDK Runtime Environment (build 17.0.5+8-Ubuntu-2ubuntu120.04)
OpenJDK 64-Bit Server VM (build 17.0.5+8-Ubuntu-2ubuntu120.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.8.6 (84538c9988a25aec085021c365c560670ad80f63)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 17.0.5, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-135-generic", arch: "amd64", family: "unix"

お題

resteasy-undertow-cdiが使えるということだったので、今回は簡単なJAX-RSとCDIを使ったアプリケーションを、SeBootstrapを使って
作ってみます。

また、Jandexを使ったパターンも試してみます。

resteasy-undertow-cdi以外のアーティファクトは、今回は試しません。

JAX-RS+CDIアプリケーションを作成する

ではまずは、簡単なJAX-RSとCDIを使ったアプリケーションを作成してみましょう。

Maven依存関係等。

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-core</artifactId>
            <version>6.2.2.Final</version>
        </dependency>
        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-undertow-cdi</artifactId>
            <version>6.2.2.Final</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <version>3.4.0</version>
                <executions>
                    <execution>
                        <id>copy-dependencies</id>
                        <phase>prepare-package</phase>
                        <goals>
                            <goal>copy-dependencies</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${project.build.directory}/libs</outputDirectory>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.3.0</version>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                            <classpathPrefix>libs/</classpathPrefix>
                            <mainClass>org.littlewings.resteasy.sebootstrap.App</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>

RESTEasyは、6.2.2.Finalを使います。また、このpom.xmlの記述は後から追加していきます。

こちらは、Maven Dependency PluginとMaven JAR Pluginを使って、java -jarでアプリケーションを実行できるようにする設定です。

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <version>3.4.0</version>
                <executions>
                    <execution>
                        <id>copy-dependencies</id>
                        <phase>prepare-package</phase>
                        <goals>
                            <goal>copy-dependencies</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${project.build.directory}/libs</outputDirectory>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.3.0</version>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                            <classpathPrefix>libs/</classpathPrefix>
                            <mainClass>org.littlewings.resteasy.sebootstrap.App</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>

JAX-RSリソースクラス。

src/main/java/org/littlewings/resteasy/sebootstrap/MessageResource.java

package org.littlewings.resteasy.sebootstrap;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;

@Path("message")
@ApplicationScoped
public class MessageResource {
    @Inject
    MessageService messageService;

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String get(@QueryParam("text") String text) {
        return messageService.decorate(text);
    }
}

CDI管理Bean。

src/main/java/org/littlewings/resteasy/sebootstrap/MessageService.java

package org.littlewings.resteasy.sebootstrap;

import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class MessageService {
    public String decorate(String target) {
        return "★★★" + target + "★★★";
    }
}

JAX-RS有効化のためのクラス。

src/main/java/org/littlewings/resteasy/sebootstrap/JaxrsActivator.java

package org.littlewings.resteasy.sebootstrap;

import java.util.Set;

import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;

@ApplicationPath("")
public class JaxrsActivator extends Application {
    @Override
    public Set<Class<?>> getClasses() {
        return Set.of(MessageResource.class);
    }
}

この時点では、ApplicationのサブクラスからJAX-RSリソースクラスを返した方がよいです。

mainメソッドを持ったクラス。この中身は、あとで埋めていきます。

src/main/java/org/littlewings/resteasy/sebootstrap/App.java

package org.littlewings.resteasy.sebootstrap;

import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.ExecutionException;

import jakarta.ws.rs.SeBootstrap;
import org.jboss.jandex.Index;
import org.jboss.jandex.IndexReader;
import org.jboss.logging.Logger;
import org.jboss.resteasy.core.se.ConfigurationOption;

public class App {
    public static void main(String... args) throws ExecutionException, InterruptedException, IOException {
        Logger logger = Logger.getLogger(App.class);

        // 後で
    }
}

beans.xmlも必要ですが、中身は空でかまいません。

src/main/resources/META-INF/beans.xml




このアプリケーションは、パッケージングして

$ mvn package

java -jarで起動できるものとします。

$ java -jar target/[JARファイル名] 

SeBootstrapを使う

まずは、簡単にSeBootstrapを使ってみます。こんな感じですね。

public class App {
    public static void main(String... args) throws ExecutionException, InterruptedException, IOException {
        Logger logger = Logger.getLogger(App.class);

        SeBootstrap.Instance instance =
                SeBootstrap
                        .start(new JaxrsActivator())
                        .toCompletableFuture()
                        .get();

        logger.info("server startup.");
        System.console().readLine("> Enter stop.");

        instance
                .stop()
                .toCompletableFuture()
                .get();
    }
}

SeBootstrap#startに、Applicationのサブクラスのインスタンスを渡します。

        SeBootstrap.Instance instance =
                SeBootstrap
                        .start(new JaxrsActivator())
                        .toCompletableFuture()
                        .get();

パッケージングして、アプリケーションを起動。

$ mvn package
$ java -jar target/[JARファイル名] 

確認。

$ curl localhost:8081/message?text=Hello
★★★Hello★★★

起動してもUndertowのログにリッスンポートが表示されたりはしていないのですが、デフォルトでは8081ポートでリッスンしています。

こちらの内容ですね。

https://github.com/resteasy/resteasy/blob/6.2.2.Final/resteasy-core/src/main/java/org/jboss/resteasy/core/se/ConfigurationOption.java#L57

https://github.com/resteasy/resteasy/blob/6.2.2.Final/resteasy-core/src/main/java/org/jboss/resteasy/util/PortProvider.java#L14

SeBootstrap.Configurationを使ってSeBootstrapの設定を行う

次に、SeBootstrap.Configurationを使ってSeBootstrapの設定を行ってみます。

RESTEasy / Jakarta RESTful Web Services SeBootstrap

SeBootstrap.Configuration.Builderで設定を指定して、

SeBootstrap.Configuration.Builder (Jakarta EE Platform API)

SeBootstrap.Configurationのインスタンスを作成してSeBootstrap#startに渡すことになります。

SeBootstrap.Configuration (Jakarta EE Platform API)

標準の範囲でもプロトコル、ホスト(アドレス)、ポート、ルートパスやSSLの設定は行えそうです。

あとは実装固有のプロパティを指定することができます。

ここでは、リッスンするアドレスとポートを指定してみましょう。

public class App {
    public static void main(String... args) throws ExecutionException, InterruptedException, IOException {
        Logger logger = Logger.getLogger(App.class);

        SeBootstrap.Configuration configuration =
                SeBootstrap
                        .Configuration
                        .builder()
                        .host("0.0.0.0")
                        .port(8080)
                        .build();

        SeBootstrap.Instance instance =
                SeBootstrap
                        .start(new JaxrsActivator(), configuration)
                        .toCompletableFuture()
                        .get();

        logger.info("server startup.");
        System.console().readLine("> Enter stop.");

        instance
                .stop()
                .toCompletableFuture()
                .get();
    }
}

パッケージング、アプリケーションを起動して

$ mvn package
$ java -jar target/[JARファイル名] 

確認。

$ curl localhost:8080/message?text=Hello
★★★Hello★★★

リッスンポートが変更できていることを確認できました(リッスンしているアドレスの確認は省略します)。

Jandex Maven Pluginを使う

次に、Jandex Maven Pluginも使ってみましょう。こちらは、RESTEasyのドキュメントにも記載があります。

It's also suggested that if the resources to be used in the application are not explicitly define, then use the org.jboss.jandex:jandex-maven-plugin to create a Jandex Index. Without this the class path will be scanned for resources which could have significant performance impacts.

RESTEasy / Jakarta RESTful Web Services SeBootstrap

JAX-RSリソースクラスが明示されていない場合は、こちらを使用することが勧められています。

インデックスを作成してくれるみたいですね。

pom.xmlに、Jandex Maven Pluginを追加します。

            <plugin>
                <groupId>org.jboss.jandex</groupId>
                <artifactId>jandex-maven-plugin</artifactId>
                <version>1.2.3</version>
                <executions>
                    <execution>
                        <id>make-index</id>
                        <goals>
                            <goal>jandex</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

Applicationのサブクラスからは、JAX-RSリソースクラスに関する情報を返さないようにしてみます。

@ApplicationPath("")
public class JaxrsActivator extends Application {
    /*
    @Override
    public Set<Class<?>> getClasses() {
        return Set.of(MessageResource.class);
    }
     */
}

mainメソッドを持ったクラスは、以下のように変更してみます。

public class App {
    public static void main(String... args) throws ExecutionException, InterruptedException, IOException {
        Logger logger = Logger.getLogger(App.class);

        SeBootstrap.Configuration configuration =
                SeBootstrap
                        .Configuration
                        .builder()
                        .host("0.0.0.0")
                        .port(8080)
                        .property(ConfigurationOption.JANDEX_INDEX.key(), Index.of(MessageResource.class))
                        .build();

        SeBootstrap.Instance instance =
                SeBootstrap
                        .start(new JaxrsActivator(), configuration)
                        .toCompletableFuture()
                        .get();

        logger.info("server startup.");
        System.console().readLine("> Enter stop.");

        instance
                .stop()
                .toCompletableFuture()
                .get();
    }
}

実装固有のプロパティとして、JANDEX_INDEXを指定します。この時、JAX-RSリソースクラスはJandexを使って指定します。

        SeBootstrap.Configuration configuration =
                SeBootstrap
                        .Configuration
                        .builder()
                        .host("0.0.0.0")
                        .port(8080)
                        .property(ConfigurationOption.JANDEX_INDEX.key(), Index.of(MessageResource.class))
                        .build();

パッケージング、アプリケーションを起動して

$ mvn package
$ java -jar target/[JARファイル名] 

確認。

$ curl localhost:8080/message?text=Hello
★★★Hello★★★

Applicationのサブクラスから、JAX-RSリソースクラスを返さなくても動作するようになりました。

この時、META-INF/jandex.idxというファイルがJARファイルの中にあることを確認できます。これがインデックスファイルです。

$ jar -tvf target/[JARファイル名]  | grep jandex
  1087 Wed Dec 28 00:09:38 JST 2022 META-INF/jandex.idx

ちなみに、Jandexのインデックスがなくても今回のコードで動作はするのですが、パフォーマンスに影響があるという話のようですね。

この部分がインデックスがなくても動きます、ということです。

                        .property(ConfigurationOption.JANDEX_INDEX.key(), Index.of(MessageResource.class))

なお、今回はmvn packageでパッケージングしていますが、mvn compileだとJandex Maven Pluginは動作しません。
これはデフォルトでprocess-classesフェーズにマッピングされているからです。

パッケージングせずに動かすなら、こうですね。

$ mvn process-classes

もしくはフェーズの紐付けを変更しましょう。

JandexのインデックスファイルにJAX-RSリソースクラスの探索を任せる

最後に、JandexのインデックスファイルにJAX-RSリソースクラスの探索を任せるようにしてみましょう。

つまり、今回作成したMessageResourceというClassクラスへの参照を削除するようにしてみます。

結果はこちら。

public class App {
    public static void main(String... args) throws ExecutionException, InterruptedException, IOException {
        Logger logger = Logger.getLogger(App.class);

        SeBootstrap.Instance instance =
                SeBootstrap
                        .start(new JaxrsActivator())
                        .toCompletableFuture()
                        .get();

        logger.info("server startup.");
        System.console().readLine("> Enter stop.");

        instance
                .stop()
                .toCompletableFuture()
                .get();
    }
}

1番最初に戻りました。

こちらでも構わないのですが。

public class App {
    public static void main(String... args) throws ExecutionException, InterruptedException, IOException {
        Logger logger = Logger.getLogger(App.class);

        SeBootstrap.Configuration configuration =
                SeBootstrap
                        .Configuration
                        .builder()
                        .host("0.0.0.0")
                        .port(8080)
                        .build();

        SeBootstrap.Instance instance =
                SeBootstrap
                        .start(new JaxrsActivator(), configuration)
                        .toCompletableFuture()
                        .get();

        logger.info("server startup.");
        System.console().readLine("> Enter stop.");

        instance
                .stop()
                .toCompletableFuture()
                .get();
    }
}

今回は、バインドするアドレスとポートを変えた方を使ってみましょうか。

パッケージング、アプリケーションを起動して

$ mvn package
$ java -jar target/[JARファイル名] 

確認。

$ curl localhost:8080/message?text=Hello
★★★Hello★★★

OKですね。JAX-RSリソースクラスの探索を、Jandexに任せることができました。

これはどうなっているかというと、META-INF/jandex.idxファイルが存在する場合はRESTEasyが検索してくれるからです。

https://github.com/resteasy/resteasy/blob/6.2.2.Final/resteasy-core/src/main/java/org/jboss/resteasy/core/scanner/ResourceScanner.java#L125-L136

つまり、先ほどJANDEX_INDEXを使ったコードを書いていましたが、実はなくてもよかったんだ、という話になります。

        SeBootstrap.Configuration configuration =
                SeBootstrap
                        .Configuration
                        .builder()
                        .host("0.0.0.0")
                        .port(8080)
                        .property(ConfigurationOption.JANDEX_INDEX.key(), Index.of(MessageResource.class))
                        .build();

ドキュメントに使い方の例として書かれていたので、そのままマネしていたのが裏目に出ました…。

もっと言うと、ApplicationのサブクラスがJAX-RSリソースクラスに関する情報を返さない場合は、ファイルシステムからクラスを探しに
いくようです。

https://github.com/resteasy/resteasy/blob/6.2.2.Final/resteasy-core/src/main/java/org/jboss/resteasy/core/scanner/ResourceScanner.java#L137-L159

なので、Jandexを使わない場合はApplicationのサブクラスがJAX-RSリソースクラスに関する情報を返した方がよい、となります。

組み込みサーバーの実装の決定方法

今回、組み込みサーバーとしてUndertowを使いましたが、RESTEasyの場合はサービスプロバイダーの仕組みで実装を探すようです。

具体的には、EmbeddedServerインターフェースの実装が対象になります。

https://github.com/resteasy/resteasy/blob/6.2.2.Final/resteasy-core/src/main/java/org/jboss/resteasy/plugins/server/embedded/EmbeddedServer.java

resteay-undertow-cdiの場合は、以下のクラスおよびファイルになります。

https://github.com/resteasy/resteasy/blob/6.2.2.Final/server-adapters/resteasy-undertow-cdi/src/main/java/dev/resteasy/embedded/server/UndertowCdiEmbeddedServer.java

https://github.com/resteasy/resteasy/blob/6.2.2.Final/server-adapters/resteasy-undertow-cdi/src/main/resources/META-INF/services/org.jboss.resteasy.plugins.server.embedded.EmbeddedServer

そして、これらのクラスをJAX-RSのAPIからどうやって紐付けるのかというと、今回扱わなかった、JAX-RSのJava SE向けの仕様で
出てきたRuntimeDelegateの実装が使われることになります。

https://github.com/resteasy/resteasy/blob/6.2.2.Final/resteasy-core-spi/src/main/java/org/jboss/resteasy/spi/ResteasyProviderFactory.java#L43

https://github.com/resteasy/resteasy/blob/6.2.2.Final/resteasy-core/src/main/java/org/jboss/resteasy/core/providerfactory/ResteasyProviderFactoryImpl.java

https://github.com/resteasy/resteasy/blob/6.2.2.Final/resteasy-core/src/main/resources/META-INF/services/jakarta.ws.rs.ext.RuntimeDelegate

こちらもサービスプロバイダーの仕組みで探索されることになります。

まとめ

SeBootstrapを使って、JAX-RSをJava SE環境で動作させてみました。

RESTEasy自体は以前からJava SE環境で動かすことができたのですが、より簡単にできるようになって良いですね。