逆引きQuarkus

[backend-example-quarkus](https://github.com/unhurried/backend-example-quarkus)を実装する過程での知見を整理した。QuarkusでReactiveなRestful APIを実装する際の勘所をまとめている。

#### 基礎知識を学ぶ

RESTful APIを実装するのであれば、下記が全体を理解するのに役に立つ。

* [WRITING JSON REST SERVICES](https://quarkus.io/guides/rest-json)
* [WRITING REST SERVICES WITH RESTEASY REACTIVE](https://quarkus.io/guides/resteasy-reactive)

#### 開発環境を構築する

[Quarkus CLI](https://quarkus.io/guides/cli-tooling)というスタンドアロンなコマンドが提供されているが、[Mavenプラグイン](https://quarkus.io/guides/maven-tooling)で同じことができるため、こちらの方が手軽。Quarkus CLIを利用する場合、WindowsであればScoopでインストールするのが簡単である。

IDEにVisual Studio Codeを利用してDevContainerで開発環境を作る方法がおすすめ。[devcontainer.json設定例](https://github.com/unhurried/backend-example-quarkus/blob/v0.3.2/.devcontainer/devcontainer.json)では、[Java Feature](https://github.com/devcontainers/features/tree/main/src/java)に加えてQuarkusプラグインと[SonarLintプラグイン](https://marketplace.visualstudio.com/items?itemName=SonarSource.sonarlint-vscode)を設定している。

#### プロジェクトを新規作成する

[code.quarkus.io](https://code.quarkus.io/)で作成したプロジェクトをインポートするか、Quarkus CLIもしくはMavenプラグインでプロジェクト作成する。

Quarkus CLIでのプロジェクト作成

```bash
quarkus create app {Group ID}:{Artifact ID} --extension=resteasy-reactive-jackson --no-code
```

#### 基本的な開発の流れ

Visual Studio Codeでソースコードを編集して、Maven (Wrapper) でアプリケーションを開発モードで起動・テスト実行、必要に応じてVisual Studio Codeのデバッガを接続する、という流れが効率がよい。

```bash
# 開発モードでアプリケーションを起動する
./mvnw quarkus:dev
```

デバッガの接続のための設定は[launch.json](https://github.com/unhurried/backend-example-quarkus/blob/v0.3.2/.vscode/launch.json)を参照。Visual Studio Codeの「実行とデバッグ」メニューからデバッガを起動する他に、[Quarkusプラグイン](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-quarkus)で`Ctrl + Shift + P` → `Quarkus: Debug current Quarkus project`を実行すると、アプリ起動＋デバッグ実行を一発でできる。（参考：[Quarkus developer joy for VS Code](https://quarkus.io/blog/quarkus-developer-joy-for-vs-code/)）

ファイル更新時にSonarLintの分析が動作して問題パネルに指摘が表示される。現在のところ、全ファイルを一括で分析することはできないとのこと。（参考：[How to enable SonarLint on all files in Visual Studio code - SonarLint / VS Code - Sonar Community](https://community.sonarsource.com/t/how-to-enable-sonarlint-on-all-files-in-visual-studio-code/46704)）

#### 入力に対するバリデーションを行う

Hibernate Validator (Bean Validation) がサポートされている。Extenstionをインストールすれば使えるようになる。

```bash
quarkus extension add 'hibernate-validator'
```

参考：[VALIDATION WITH HIBERNATE VALIDATOR](https://quarkus.io/guides/validation)

Hibernate Validatorビルトインのアノテーションは[Hibernate Validatorのドキュメント](https://docs.jboss.org/hibernate/validator/7.0/reference/en-US/html_single/#section-builtin-constraints)を参照。独自バリデーション（アノテーション）の実装方法は[6. Creating custom constraints](https://docs.jboss.org/hibernate/validator/7.0/reference/en-US/html_single/#validator-customconstraints)を参照。

#### 共通の例外処理を実装する

JAX-RSのExceptionMapperとRESTEasyのServerExceptionMapperの両方がサポートされている。（参考：[Exception Mapping](https://quarkus.io/guides/resteasy-reactive#exception-mapping)）両者の機能的な差異は、ServerExceptionMapperでは複数のExceptionを受け取る設定を[アノテーションで記載する機能](https://github.com/quarkusio/quarkus/pull/20216)がある程度。

ビルトインのExceptionMapperが定義されており、エラーハンドリングをカスタマイズするにはPriorityを設定して上書き登録が必要となる。また、これが原因で全ての例外を受け取るExceptionMapper定義もできない制約がある。（参考：[built-in ExceptionMappers cause unexpected behavior · Issue #7883 · quarkusio/quarkus](https://github.com/quarkusio/quarkus/issues/7883)）

Quarkus Securityにて発生する例外をExceptionMapperで扱うには以下の設定を入れる必要がある。

```properties
quarkus.http.auth.proactive=false
```

ただし、smallrye-jwt (quarkus-smallrye-jwt) のトークン検証エラー（AuthenticationFailedException）を受け取るExceptionMapperを定義しようとすると `java.lang.IllegalStateException: Response has already been written` が発生することを確認している。原因は調査できていない。

#### データベースにアクセスする

ORM (+α) を使いたい場合はHibernate Reactive + Panache ([quarkus-hibernate-reactive-panache](https://code.quarkus.io/?extension-search=quarkus-hibernate-reactive-panache)) を利用するとよい。（2022年9月時点ではPreview扱い。）

実装例：[TodoEntity.java](https://github.com/unhurried/backend-example-quarkus/blob/v0.3.2/src/main/java/io/github/unhurried/example/backend/quarkus/entity/TodoEntity.java)

実装上のTips

* レコードのIDをカスタマイズする場合はPanacheEntityではなくPanacheEntityBaseを使う。（参考：[Custom IDs](https://quarkus.io/guides/hibernate-orm-panache#custom-ids)）

* ページングはpageメソッドで実装できる。（参考：[Paging](https://quarkus.io/guides/hibernate-reactive-panache#paging)）

  ```java
  MyEntity.findAll().page(Page.of(...))
  ```

* テーブルにインデックスを設定するには `@Table` アノテーションの `indexes` プロパティを設定すればよい。（参考：[Defining Indexes in JPA | Baeldung](https://www.baeldung.com/jpa-indexes)）

* アプリケーション起動時にデータベースの初期化を実行したい場合はStartupEventを捕捉するメソッドを用意すればよい。（参考：[DBInit.java](https://github.com/quarkusio/quarkus-quickstarts/blob/main/getting-started-reactive-crud/src/main/java/org/acme/reactive/crud/DBInit.java)）

  ```java
  @ApplicationScoped public class DBInit {
    void onStart(@Observes StartupEvent ev) { ... }
  ```

参考

- [SIMPLIFIED HIBERNATE REACTIVE WITH PANACHE](https://quarkus.io/guides/hibernate-reactive-panache)
- [hibernate-reactive-panache-quickstart](https://github.com/quarkusio/quarkus-quickstarts/tree/main/hibernate-reactive-panache-quickstart)
- [QuarkusのHibernate Reactive with Panache Extensionを試してみようとした話](https://kazuhira-r.hatenablog.com/entry/2022/01/27/000616)

#### トランザクションを利用する

Bean（コンポーネント）のメソッドに`@ReactiveTransactional`を記述することで、トランザクション境界を設定できる。もしくは`Panache.withTransaction()`の中でDB操作を記述することで手続き的に記述もできる。

参考：[Simplified Hibernate Reactive with Panache - Quarkus](https://quarkus.io/guides/hibernate-reactive-panache#transactionshttps://quarkus.io/guides/hibernate-reactive-panache#transactions)

トランザクションを使っている場合、複数のクエリを `Uni.combine` で結合すると以下のエラーが発生するため、`withTransaction` で囲う記述とする必要がある。（参考：[First Request to Reactive SQL fail · Issue #14709 · quarkusio/quarkus](https://github.com/quarkusio/quarkus/issues/14709)）

```
java.lang.IllegalStateException: session is currently connecting to database
```

#### アクセストークンを利用した認証を実装する

以下の3つのExtensionが選択肢となる。

* quarkus-oidc
* quarkus-smallrye-jwt
* quarkus-elytron-security-oauth2

今回のプロジェクトではJWT形式のトークンの検証ができればよいのでquarkus-smallrye-jwtを選択した。Quarkus CLIでインストールできる。

```bash
quarkus extension add 'smallrye-jwt'
```

javax.annotation.securityやJAX-RS (javax.ws.rs.core) のアノテーションの他にQuarkus独自 (io.quarkus.security) のアノテーションを使って権限設定ができる。（参考：[Authorization using Annotations](https://quarkus.io/guides/security-authorization#standard-security-annotations)）

実装上のTips

* デフォルトではJWTトークンにiatクレームが指定されていることが必須となるが、下記の設定で任意に変更できる。

  ```
  smallrye.jwt.time-to-live=-1
  ```

#### インターセプタを実装する

Java Interceptors ([javax.interceptor](https://javaee.github.io/javaee-spec/javadocs/javax/interceptor/package-summary.html)) がサポートされているため、[@Interceptor](https://javaee.github.io/javaee-spec/javadocs/javax/interceptor/Interceptor.html)などのアノテーションを利用できる。（参考：[Interceptors](https://quarkus.io/guides/cdi#interceptors)）

#### ログを出力する

JDK (JUL), [JBoss Logging](https://github.com/jboss-logging/jboss-logging), [SLF4J](https://www.slf4j.org/), [Apache Commons Logging](https://commons.apache.org/proper/commons-logging/)のAPIがサポートされている。(内部でJBoss Loggingに橋渡しされる仕組み。)

参考：[Configuring Logging - Quarkus](https://quarkus.io/guides/logging)

LoggerインスタンスをDIする方法の他に、staticメソッドで呼び出す仕組み（コンパイル時にインスタンスを利用するコードに置き換えられる）も提供される。

```java
package com.example;

import io.quarkus.logging.Log; 

class MyService { 
    public void doSomething() {
        Log.info("Simple!"); 
    }
}
```

ログ設定はapplication.propertiesに記載する。（参考：[Runtime configuration](https://quarkus.io/guides/logging#runtime-configuration)）

#### 単体テストを実装する

[quarkus-junit5](https://mvnrepository.com/artifact/io.quarkus/quarkus-junit5)を依存関係に追加してテストクラスに@QuarkusTestを設定すると、JUnit5を利用したテスト実装にて、アプリケーションのテスト用の起動やテスト対象クラスのDIなどの便利機能が利用できるようになる。（参考：[TESTING YOUR APPLICATION](https://quarkus.io/guides/getting-started-testing)）

Uni/MultiのアサーションにはSubscriberを利用する。（参考：[How can I write unit / integration tests? - SmallRye Mutiny](https://smallrye.io/smallrye-mutiny/1.6.0/guides/testing/)）

Mockitoの基本的なサポートは[quarkus-junit5](https://mvnrepository.com/artifact/io.quarkus/quarkus-junit5)に含まれているが、[quarkus-junit5-mockito](https://mvnrepository.com/artifact/io.quarkus/quarkus-junit5-mockito)を追加すると@InjectMockなど便利アノテーションが使えるようになる。（参考：[Mock Support](https://quarkus.io/guides/getting-started-testing#mock-support)）

Panacheをモック化するには[quarkus-panache-mock](https://mvnrepository.com/artifact/io.quarkus/quarkus-panache-mock)を使う。

- PanacheQueryインスタンスを返すEntityメソッドをモック化する場合はPanacheQueryのモックオブジェクトを用意する。（詳細は[公式ガイド](https://quarkus.io/guides/hibernate-orm-panache#mocking-entitymanager-session-and-entity-instance-methods)を参照。）
- PanacheQueryのモック生成時はMockitoの制約により、キャストが必要になることに注意。（参考：[Using Mockito to mock classes with generic parameters](https://stackoverflow.com/questions/1652692/using-mockito-to-mock-classes-with-generic-parameters)）

Quarkus Securityをモック化するには[quarkus-test-security](https://mvnrepository.com/artifact/io.quarkus/quarkus-test-security)を使う。（参考：[Security Testing](https://quarkus.io/guides/security-testing)）

- 単体テストのコンテキストでJAX-RSのSecurityContextを参照しようとしたタイミングで `java.lang.IllegalStateException: No RESTEasy Reactive request in progress` が発生するため、Mockitoでモック化する必要がある。（原因不明）

単体テストの実行はVisual Studio Codeのテストツールから行ってもよいが、起動に時間がかかるため、Quarkusのdev modeを起動（`mvn quarkus:dev`）しておき、テスト再実行（`r`）すると効率がよい。

#### E2Eテストを実装する

E2Eテストの実装にはRESTAssuredを利用できる。@TestHttpEndpointをクラスもしくはメソッドに指定するとRestassuredのbasePathをリソースのパスに自動で設定してくれる。（参考：[5.2 Restassured](https://quarkus.io/guides/getting-started-testing#restassured)）

#### アプリケーションの設定を管理する

アプリケーション設定はシステムプロパティ、環境変数、.envファイル、プロパティファイル等で入れ込める。ソースの優先順位は[Config Sources](https://quarkus.io/guides/config-reference#configuration-sources)を参照。

起動ポートなどの動作設定など設定可能な項目は[Configuring Quarkus](https://quarkus.io/guides/config#configuring-quarkus)を参照。独自のアプリケーション設定を定義することもできる。

環境（の分類）ごとの設定をプロファイルとして管理できる。デフォルトでは `dev` `test` `prod` の3つのプロファイルが定義される。（参考：[4. Profiles](https://quarkus.io/guides/config-reference#profiles)）

プロパティのキーのプレフィックスを `%{profile-name}.` の形式にすることで、特定のプロファイル用の設定を定義できる。もしくは特定のプロファイル用のプロパティファイルを `application-{profole-name}.properties` として作成できる。

プログラム中でプロファイル名を取得するには `ProfileManager.getActiveProfile()` を呼び出せばよい。（参考：[How to determine programmatically the current active profiles in Quarkus - Stack Overflow](https://stackoverflow.com/questions/56617504/how-to-determine-programmatically-the-current-active-profiles-in-quarkus)）

#### Nativeイメージをビルドする

自力でGraalVMをインストールしてビルドすることもできるがDockerコンテナを使うのが簡単。Quarkus CLI (Mavenプラグイン) ではさらにコンテナを自動で起動してビルドしてくれるコマンドが提供されている。

```bash
./mvnw package -Pnative -Dquarkus.native.container-build=true
```

参考：[Creating Linux executable without GraalVM installed](https://quarkus.io/guides/building-native-image#container-runtime)

Nativeイメージが `./target/{project name and version}-runner` に作成されるので、実行すればOK。