SpringフレームワークのアプリケーションをQuarkusで包む その2

それでは前回の続きから。
今回はstep1からstep4までに、どのような変更を行ったか詳しく見ていきたいと思います。

目次

step1

コアライブラリ側

Springフレームワークベースで開発したアプリケーションのコアとなるライブラリ(コアライブラリ)でUTが通っており、Springフレームワークのアプリケーションコンテキストの初期化やBeanのDIが出来ている状態です。
masterブランチとの差分はありませんが、以下のConfigクラスを起点としてコンポーネントスキャンが行われており、ごく一般的なSpringフレームワークのアプリケーションとなっています。
コアライブラリは、

./gradlew publishToMavenLocal
でローカルのMavenリポジトリに保存しています。

Quarkus側

step1のコアライブラリをbuild.gradleから参照しています。
ところが、BootApplicationを実行すると、Springフレームワークのアノテーションを付与したBeanの解決ができずに、実行に失敗します。

実行時エラーの内容

スタックトレースは割愛していますが、コアライブラリの「FileService」というBeanの解決ができないようです。


2023-11-21 15:31:50,242 ERROR [io.qua.dep.dev.IsolatedDevModeMain] (main) Failed to start quarkus: java.lang.RuntimeException: io.quarkus.builder.BuildException: Build failure: Build failed due to errors
    [error]: Build step io.quarkus.arc.deployment.ArcProcessor#validate threw an exception: jakarta.enterprise.inject.spi.DeploymentException: jakarta.enterprise.inject.UnsatisfiedResolutionException: Unsatisfied dependency for type jp.co.mukeisoftllc.ex.spring.world.services.FileService and qualifiers [@Default]
    - java member: jp.co.mukeisoftllc.ex.grpc.FileServiceExposer#fileService
    - declared on CLASS bean [types=[io.grpc.BindableService, jp.co.mukeisoftllc.ex.quarkus.gpc.FileGrpcServiceGrpc$FileGrpcServiceImplBase, jp.co.mukeisoftllc.ex.quarkus.gpc.FileGrpcServiceGrpc$AsyncService, java.lang.Object, jp.co.mukeisoftllc.ex.grpc.FileServiceExposer], qualifiers=[@Any, @GrpcService], target=jp.co.mukeisoftllc.ex.grpc.FileServiceExposer]

「FileService」Beanを注入しているのは、gRPCサービスとして公開しているクラスの14行目です。
Quarkus側はCDIのアノテーションで依存関係の宣言をしていますが、Quarkusのspring diエクステンションを使えば、「FileService」Beanの解決はできるはずです。

step2

コアライブラリ側

step1からstep2への差分はこちらです。

step1では、「FileService」Beanの解決ができなかったため、CDIの流儀に則りbeans.xmlをMETA-INFに追加します。中身は空でも大丈夫です。

Quarkus側

step1からstep2への差分はこちらです。

コアライブラリのバージョンだけあげていますが、BootApplicationの起動ができず、別のエラーが発生します。

エラーの内容

以下のとおり、3つのエラーがあるようです。

  • org.springframework.core.env.Environmentが解決できない
  • FileServiceでコンストラクタインジェクションしているPath型のBeanがあいまい
  • 上記と同じでコンストラクタインジェクションしているPath型のBeanがあいまい

Environmentの件は環境設定がSpringとQuarkusで異なるため、おそらく他の方法で依存性注入を試みた方が良さそうであるため、いったん無視するとして、Path型のBean定義があいまいだというエラーはコアライブラリ側の挙動を変えずに解消できそうです。


[1] Unsatisfied dependency for type org.springframework.core.env.Environment and qualifiers [@Default]
    - java member: jp.co.mukeisoftllc.ex.spring.world.Config#env
    - declared on CLASS bean [types=[jp.co.mukeisoftllc.ex.spring.world.Config, java.lang.Object], qualifiers=[@Default, @Any], target=jp.co.mukeisoftllc.ex.spring.world.Config]
[2] Ambiguous dependencies for type java.nio.file.@NonNull Path and qualifiers [@Default]
    - java member: jp.co.mukeisoftllc.ex.spring.world.services.FileService():textPath
    - declared on CLASS bean [types=[jp.co.mukeisoftllc.ex.spring.world.services.FileService, java.lang.Object], qualifiers=[@Default, @Any, @Named("fileService")], target=jp.co.mukeisoftllc.ex.spring.world.services.FileService]
    - available beans:
        - PRODUCER METHOD bean [types=[java.nio.file.Watchable, java.nio.file.Path, java.lang.Comparable<java.nio.file.Path>, java.lang.Object, java.lang.Iterable<java.nio.file.Path>], qualifiers=[@Default, @Any, @Named("textPath")], target=java.nio.file.Path textPath(), declaringBean=jp.co.mukeisoftllc.ex.spring.world.Config]
        - PRODUCER METHOD bean [types=[java.nio.file.Watchable, java.nio.file.Path, java.lang.Comparable<java.nio.file.Path>, java.lang.Object, java.lang.Iterable<java.nio.file.Path>], qualifiers=[@Default, @Any, @Named("imagePath")], target=java.nio.file.Path imagePath(), declaringBean=jp.co.mukeisoftllc.ex.spring.world.Config]
[3] Ambiguous dependencies for type java.nio.file.@NonNull Path and qualifiers [@Default]
    - java member: jp.co.mukeisoftllc.ex.spring.world.services.FileService():imagePath
    - declared on CLASS bean [types=[jp.co.mukeisoftllc.ex.spring.world.services.FileService, java.lang.Object], qualifiers=[@Default, @Any, @Named("fileService")], target=jp.co.mukeisoftllc.ex.spring.world.services.FileService]
    - available beans:
        - PRODUCER METHOD bean [types=[java.nio.file.Watchable, java.nio.file.Path, java.lang.Comparable<java.nio.file.Path>, java.lang.Object, java.lang.Iterable<java.nio.file.Path>], qualifiers=[@Default, @Any, @Named("textPath")], target=java.nio.file.Path textPath(), declaringBean=jp.co.mukeisoftllc.ex.spring.world.Config]
        - PRODUCER METHOD bean [types=[java.nio.file.Watchable, java.nio.file.Path, java.lang.Comparable<java.nio.file.Path>, java.lang.Object, java.lang.Iterable<java.nio.file.Path>], qualifiers=[@Default, @Any, @Named("imagePath")], target=java.nio.file.Path imagePath(), declaringBean=jp.co.mukeisoftllc.ex.spring.world.Config]

step3

コアライブラリ側

step2からstep3への差分はこちらです。

step2であいまいだとされていた2つのPath型のBeanについて、定義とDIの書き方を改めます。
step2ブランチとstep3ブランチの差分はこちらのリンクから参照してください)

まずはConfigクラスで@Beanアノテーションを付与した2つのプロデューサーメソッドに明示的なBean名を付けます。Springフレームワーク上では、こういったプロデューサーメソッドの名前をBean名として別にBeanに注入してくれますが、より明示的な宣言をして曖昧さを排除した方が良いでしょう。

次に、FileServiceクラスのコンストラクタでDIしているPath型の2つのBeanに関し、@Qualifierを使って明示的にします。

Quakrus側

step2からstep3への差分はこちらです。

特に変更はありませんが、コアライブラリのバージョンをあげています。
期待値としては、Environemntに関するエラー以外は解消されているはず、ですね。
それではBootApplicationを起動してみましょう。

エラーの内容

予想どおり、Path型2つのBeanの曖昧さは排除できましたが、特になにも手を打っていないEnvironmentがエラーとなっています。
EnvironmentはSpringフレームワークで一般的に実行時のふるまいを変えるために利用しますが、Quakrusのspring diエクステンションは、Beanの解決はしてくれるものの、Springフレームワーク自体の初期処理の一切を行いませんので、step4ではQuakrus側でEnvironment型のBeanを初期化して、コアライブラリに注入するアプローチでチャレンジしてみましょう。


 ERROR [io.qua.dep.dev.IsolatedDevModeMain] (main) Failed to start quarkus: java.lang.RuntimeException: io.quarkus.builder.BuildException: Build failure: Build failed due to errors
    [error]: Build step io.quarkus.arc.deployment.ArcProcessor#validate threw an exception: jakarta.enterprise.inject.spi.DeploymentException: jakarta.enterprise.inject.UnsatisfiedResolutionException: Unsatisfied dependency for type org.springframework.core.env.Environment and qualifiers [@Default]
    - java member: jp.co.mukeisoftllc.ex.spring.world.Config#env
    - declared on CLASS bean [types=[jp.co.mukeisoftllc.ex.spring.world.Config, java.lang.Object], qualifiers=[@Default, @Any], target=jp.co.mukeisoftllc.ex.spring.world.Config]

step4

コアライブラリ側

step3からstep4への差分はこちらです。

特に変更はありません。Environmentの注入待ちです。

Quakrus側

step3からstep4への差分はこちらです。

StandardEnvironmentのインスタンスを作成して、Quakrusのapplication.propertiesで定義したプロファイルを設定してあげるようなプロデューサーメソッドを作成し、BootApplicationを実行してみます。


Press [e] to edit command line args (currently ''), [h] for more options>
Tests paused
Press [e] to edit command line args (currently ''), [r] to resume testing, [h] for more options>
Press [e] to edit command line args (currently ''), [r] to resume testing, [o] Toggle test output, [h] for more options>
__  ____  __  _____   ___  __ ____  ______
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2023-11-21 16:52:14,987 WARN  [io.qua.grp.run.GrpcServerRecorder] (Quarkus Main Thread) Using legacy gRPC support, with separate new HTTP server instance. Switch to single HTTP server instance usage with quarkus.grpc.server.use-separate-server=false property
2023-11-21 16:52:15,135 INFO  [io.qua.grp.run.GrpcServerRecorder] (Quarkus Main Thread) Registering gRPC reflection service
2023-11-21 16:52:15,359 INFO  [io.qua.grp.run.GrpcServerRecorder] (vert.x-eventloop-thread-0) Started gRPC server on 0.0.0.0:9000 [TLS enabled: false]
2023-11-21 16:52:15,401 INFO  [io.quarkus] (Quarkus Main Thread) spring-di-quarkus-example 1.0-SNAPSHOT on JVM (powered by Quarkus 3.4.1) started in 3.910s. Listening on: http://localhost:8080
2023-11-21 16:52:15,404 INFO  [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated.
2023-11-21 16:52:15,405 INFO  [io.quarkus] (Quarkus Main Thread) Installed features: [cdi, grpc-server, smallrye-context-propagation, spring-di, vertx]

StandardEnvironmentのプロデューサーメソッドに改良の余地は大きく残していますが、まずは起動できましたので、以下のコマンドで実際にgRPCクライアントからファイルの読み書きを試してみたいと思います。


gradlew test --tests "FileGrpcServiceTest"

BootApplicationで起動したgRPCサーバー(JVMモード)に対して、ファイルの読み書きを要求することはできたようです。

しかしながら、ゴールはあくまでもネイティブアプリケーションとしてビルドして実行させるという所ですので、これでめでたしめでたしとは行きません。
次回はstep5からstep7までの工程で、ネイティブ化のステップを進めていきたいと思います。
それでは!