The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.

アプリケーションデータのキャッシング

このガイドでは、QuarkusアプリケーションのCDI管理されたBeanでアプリケーションデータのキャッシングを有効にする方法について説明します。

前提条件

このガイドを完成させるには、以下が必要です:

  • 約15分

  • IDE

  • JDK 17+がインストールされ、 JAVA_HOME が適切に設定されていること

  • Apache Maven 3.9.6

  • 使用したい場合は、 Quarkus CLI

  • ネイティブ実行可能ファイルをビルドしたい場合、MandrelまたはGraalVM(あるいはネイティブなコンテナビルドを使用する場合はDocker)をインストールし、 適切に設定していること

シナリオ

Quarkusアプリケーションで、ユーザーが今後3日間の天気予報を取得できるREST APIを公開したいとします。問題は、一度に1日分のリクエストしか受け付けず、応答に時間がかかる外部の気象サービスに依存しなければならないことです。天気予報は12時間に一度更新されるので、サービスのレスポンスをキャッシュすればAPIのパフォーマンスは間違いなく向上します。

これをQuarkusの単一のアノテーションを使用して行います。

このガイドでは、デフォルトのQuarkus Cacheバックエンド(Caffeine)を使用します。代わりにRedisを使用することができます。Redisバックエンドを設定するには Redisキャッシュバックエンドのリファレンス を参照してください。

ソリューション

次の章で紹介する手順に沿って、ステップを踏んでアプリを作成することをお勧めします。ただし、完成した例にそのまま進んでも構いません。

Gitレポジトリをクローン git clone https://github.com/quarkusio/quarkus-quickstarts.git するか、 アーカイブ をダウンロードします。

ソリューションは cache-quickstart ディレクトリ にあります。

Mavenプロジェクトの作成

まず、以下のコマンドで新しいQuarkusプロジェクトを作成します。

コマンドラインインタフェース
quarkus create app org.acme:cache-quickstart \
    --extension='cache,rest-jackson' \
    --no-code
cd cache-quickstart

Gradleプロジェクトを作成するには、 --gradle または --gradle-kotlin-dsl オプションを追加します。

Quarkus CLIのインストールと使用方法の詳細については、 Quarkus CLI ガイドを参照してください。

Maven
mvn io.quarkus.platform:quarkus-maven-plugin:3.9.4:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=cache-quickstart \
    -Dextensions='cache,rest-jackson' \
    -DnoCode
cd cache-quickstart

Gradleプロジェクトを作成するには、 -DbuildTool=gradle または -DbuildTool=gradle-kotlin-dsl オプションを追加します。

Windowsユーザーの場合:

  • cmdを使用する場合、(バックスラッシュ \ を使用せず、すべてを同じ行に書かないでください)。

  • Powershellを使用する場合は、 -D パラメータを二重引用符で囲んでください。例: "-DprojectArtifactId=cache-quickstart"

このコマンドはプロジェクトを生成し、 cacherest-jackson エクステンションモジュールをインポートします。

すでにQuarkusプロジェクトが設定されている場合は、プロジェクトのベースディレクトリーで以下のコマンドを実行することで、プロジェクトに cache エクステンションを追加することができます。

コマンドラインインタフェース
quarkus extension add cache
Maven
./mvnw quarkus:add-extension -Dextensions='cache'
Gradle
./gradlew addExtension --extensions='cache'

これにより、ビルドファイルに以下の内容が追加されます。

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-cache</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-cache")

REST APIの作成

まずは、外部気象サービスへの非常に遅い呼び出しをシミュレートするサービスを作成してみましょう。以下の内容で src/main/java/org/acme/cache/WeatherForecastService.java を作成します。

package org.acme.cache;

import java.time.LocalDate;

import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class WeatherForecastService {

    public String getDailyForecast(LocalDate date, String city) {
        try {
            Thread.sleep(2000L); (1)
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return date.getDayOfWeek() + " will be " + getDailyResult(date.getDayOfMonth() % 4) + " in " + city;
    }

    private String getDailyResult(int dayOfMonthModuloFour) {
        switch (dayOfMonthModuloFour) {
            case 0:
                return "sunny";
            case 1:
                return "cloudy";
            case 2:
                return "chilly";
            case 3:
                return "rainy";
            default:
                throw new IllegalArgumentException();
        }
    }
}
1 遅さはここに由来します。

また、ユーザーが次の3日間の天気予報を求めたときに送信されるレスポンスを含むクラスも必要です。 src/main/java/org/acme/cache/WeatherForecast.java をこのように作成します:

package org.acme.cache;

import java.util.List;

public class WeatherForecast {

    private List<String> dailyForecasts;

    private long executionTimeInMs;

    public WeatherForecast(List<String> dailyForecasts, long executionTimeInMs) {
        this.dailyForecasts = dailyForecasts;
        this.executionTimeInMs = executionTimeInMs;
    }

    public List<String> getDailyForecasts() {
        return dailyForecasts;
    }

    public long getExecutionTimeInMs() {
        return executionTimeInMs;
    }
}

あとは、RESTリソースを作成するだけです。 この内容で src/main/java/org/acme/cache/WeatherForecastResource.java ファイルを作成します。

package org.acme.cache;

import java.time.LocalDate;
import java.util.Arrays;
import java.util.List;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.MediaType;

import org.jboss.resteasy.reactive.RestQuery;

@Path("/weather")
public class WeatherForecastResource {

    @Inject
    WeatherForecastService service;

    @GET
    public WeatherForecast getForecast(@RestQuery String city, @RestQuery long daysInFuture) { (1)
        long executionStart = System.currentTimeMillis();
        List<String> dailyForecasts = Arrays.asList(
                service.getDailyForecast(LocalDate.now().plusDays(daysInFuture), city),
                service.getDailyForecast(LocalDate.now().plusDays(daysInFuture + 1L), city),
                service.getDailyForecast(LocalDate.now().plusDays(daysInFuture + 2L), city));
        long executionEnd = System.currentTimeMillis();
        return new WeatherForecast(dailyForecasts, executionEnd - executionStart);
    }
}
1 daysInFuture クエリパラメーターが省略された場合、3 日間の天気予報は現在の日から始まります。それ以外の場合は、現在の日に daysInFuture の値を加えたものから始まります。

完了です!うまくいっているか確認してみましょう。

まず、プロジェクトディレクトリからDevモードでアプリケーションを実行します。

コマンドラインインタフェース
quarkus dev
Maven
./mvnw quarkus:dev
Gradle
./gradlew --console=plain quarkusDev

次に、ブラウザから http://localhost:8080/weather?city=Raleigh を呼び出します。6秒ほど長い時間が経過すると、アプリケーションはこのような回答をします:

{"dailyForecasts":["MONDAY will be cloudy in Raleigh","TUESDAY will be chilly in Raleigh","WEDNESDAY will be rainy in Raleigh"],"executionTimeInMs":6001}

コードを実行する日によってレスポンスの内容が異なる場合があります。

何度同じURLを呼び出してみても、必ず6秒でレスポンスが返却されます。

キャッシュの有効化

Quarkusアプリケーションが動いたので、外部の気象サービスのレスポンスをキャッシュすることで、レスポンスタイムを大幅に改善してみましょう。 WeatherForecastService クラスを次のように修正します。

package org.acme.cache;

import java.time.LocalDate;

import jakarta.enterprise.context.ApplicationScoped;

import io.quarkus.cache.CacheResult;

@ApplicationScoped
public class WeatherForecastService {

    @CacheResult(cacheName = "weather-cache") (1)
    public String getDailyForecast(LocalDate date, String city) {
        try {
            Thread.sleep(2000L);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return date.getDayOfWeek() + " will be " + getDailyResult(date.getDayOfMonth() % 4) + " in " + city;
    }

    private String getDailyResult(int dayOfMonthModuloFour) {
        switch (dayOfMonthModuloFour) {
            case 0:
                return "sunny";
            case 1:
                return "cloudy";
            case 2:
                return "chilly";
            case 3:
                return "rainy";
            default:
                throw new IllegalArgumentException();
        }
    }
}
1 このアノテーション(もちろん関連するインポートも)を追加しただけです。

http://localhost:8080/weather?city=Raleigh をもう一度呼び出して確認してみてください。返事が来るまでにまだ長い時間待たされています。これはサーバーが再起動したばかりでキャッシュが空になっているので正常です。

ちょっと待って!? WeatherForecastService のアップデート後、サーバーが勝手に再起動した?はい、これは、 live coding と呼ばれる開発者のためのQuarkusの驚くべき機能の一つです。

前回の呼び出しでキャッシュが読み込まれたので、同じ URL を呼び出してみてください。今度は、 executionTimeInMs の値が 0 に近い超高速な応答が返ってくるはずです。

URL http://localhost:8080/weather?city=Raleigh&daysInFuture=1 を使って未来のある日から始めるとどうなるか見てみましょう。要求された日のうち2つはすでにキャッシュに読み込まれていたので、2秒後に回答が得られるはずです。

また、同じURLを別の都市で呼び出してみて、再度キャッシュの動作を確認することもできます。最初の呼び出しには6秒ほどかかり、次の呼び出しにはすぐに出ます。

おめでとうございます!たった1行のコードでQuarkusアプリケーションにアプリケーションデータのキャッシングを追加しました!

Quarkusアプリケーションのデータキャッシング機能について詳しく知りたいですか?以下のセクションでは、この機能について知っておくべきことをすべて紹介します。

アノテーションを利用したキャッシング

Quarkusは、CDI管理されたBeanで使用できる、キャッシュ機能を有効にするアノテーションのセットを提供します。

プライベートメソッドではアノテーションのキャッシュは許可されていません。package-private (明示的な修飾子を持たない) を含む他のアクセス修飾子では問題なく動作します。

@CacheResult

可能な限り、メソッドボディを実行せずにキャッシュからメソッドの結果を読み込みます。

@CacheResult でアノテーションされたメソッドが呼び出されると、Quarkusはキャッシュキーを計算し、それを使用して、そのメソッドがすでに呼び出されているかどうかをキャッシュでチェックします。キャッシュキーの計算方法については、このガイドの キャッシュキー構築ロジック のセクションを参照してください。 キャッシュ内に値が見つかった場合、その値が返され、アノテーションされたメソッドが実際に実行されることはありません。 値が見つからない場合、アノテーションされたメソッドが呼び出され、返された値は計算されたキーを使用してキャッシュに格納されます。

CacheResult でアノテーションされたメソッドは、キャッシュミスのロックメカニズムによって保護されています。複数の同時呼び出しが同じ欠落キーからキャッシュ値を取得しようとした場合、メソッドは一度だけ呼び出されます。最初の同時呼び出しはメソッドの呼び出しをトリガし、その後の同時呼び出しはキャッシュされた結果を取得するためにメソッドの呼び出しの終了を待ちます。 lockTimeout パラメーターを使用すると、所定の遅延後にロックを中断することができます。ロックのタイムアウトは既定では無効になっており、ロックが中断されることはありません。詳細は、パラメーター Javadoc を参照してください。

このアノテーションは、 void を返すメソッドでは使用できません。

Quarkusは、基礎となるCaffeineプロバイダとは異なり、 null の値をキャッシュすることもできます。 このトピックの詳細は以下 を参照してください。

@CacheInvalidate

キャッシュからエントリーを削除します。

@CacheInvalidate のアノテーションが付いたメソッドが呼び出されると、Quarkus はキャッシュキーを計算し、それを使ってキャッシュから既存のエントリを削除しようとします。 キャッシュキーの計算方法については、このガイドの キャッシュキー構築ロジック のセクションを参照してください。 もしキーがどのキャッシュエントリも特定できない場合は、何も起こりません。

@CacheInvalidateAll

@CacheInvalidateAll でアノテーションされたメソッドが呼び出されると、Quarkusはキャッシュからすべてのエントリーを削除します。

@CacheKey

メソッドの引数が @CacheKey でアノテーションされている場合、 @CacheResult または @CacheInvalidate でアノテーションされたメソッドの呼び出し時にキャッシュキーの一部として識別されます。

このアノテーションはオプションで、メソッドの引数の一部がキャッシュキーの一部ではない場合にのみ使用されるべきです。

キャッシュキー構築ロジック

キャッシュキーはアノテーションAPIにより、以下のロジックで構築されます。

  • io.quarkus.cache.CacheKeyGenerator@CacheResult または @CacheInvalidate アノテーションで宣言されている場合、キャッシュ・キーの生成に使用されます。いくつかのメソッド引数の存在する可能性のある @CacheKey アノテーションは無視されます。

  • このメソッドに引数がなければ、キャッシュキーはキャッシュ名から作成される io.quarkus.cache.DefaultCacheKey のインスタンスとなります。

  • このメソッドが正確に1つの引数を持つ場合、この引数はキャッシュキーとなります。

  • メソッドに複数の引数があり、 @CacheKey でアノテーションされたものが1つだけある場合、このアノテーションされた引数がキャッシュキーとなります。

  • メソッドが @CacheKey でアノテーションされた複数の引数を持つ場合、キャッシュキーはこれらのアノテーションされた引数から構築される io.quarkus.cache.CompositeCacheKey のインスタンスになります。

  • メソッドが複数の引数を持ち、そのどれにも @CacheKey でアノテーションされていない場合、キャッシュキーはすべてのメソッド引数から構築される io.quarkus.cache.CompositeCacheKey のインスタンスになります。

キーの一部である非プリミティブメソッドの各引数は、キャッシュが期待通りに動作するために、 equals()hashCode() を正しく実装する必要があります。

キャッシュキーが複数のメソッド引数から構築される場合、それらが明示的に @CacheKey で識別されているかどうかに関わらず、構築ロジックはメソッドシグネチャ内のこれらの引数の順序に依存します。一方、引数名は全く使用されず、キャッシュキーには何の影響も与えません。

package org.acme.cache;

import jakarta.enterprise.context.ApplicationScoped;

import io.quarkus.cache.CacheInvalidate;
import io.quarkus.cache.CacheResult;

@ApplicationScoped
public class CachedService {

    @CacheResult(cacheName = "foo")
    public Object load(String keyElement1, Integer keyElement2) {
        // Call expensive service here.
    }

    @CacheInvalidate(cacheName = "foo")
    public void invalidate1(String keyElement2, Integer keyElement1) { (1)
    }

    @CacheInvalidate(cacheName = "foo")
    public void invalidate2(Integer keyElement2, String keyElement1) { (2)
    }

    @CacheInvalidate(cacheName = "foo")
    public void invalidate3(Object notPartOfTheKey, @CacheKey String keyElement1, @CacheKey Integer keyElement2) { (3)
    }

    @CacheInvalidate(cacheName = "foo")
    public void invalidate4(Object notPartOfTheKey, @CacheKey Integer keyElement2, @CacheKey String keyElement1) { (4)
    }
}
1 このメソッドを呼び出すと、キー要素名が入れ替わっていても load メソッドでキャッシュされた値が無効になります。
2 このメソッドを呼び出すと、キー要素の順序が異なるため、 load メソッドでキャッシュされた値が無効になることはありません。
3 このメソッドを呼び出すと、キー要素の順序が同じなので、 load メソッドでキャッシュされた値が無効になります。
4 このメソッドを呼び出すと、キー要素の順序が異なるため、 load メソッドでキャッシュされた値が無効になることはありません。

CacheKeyGenerator でキャッシュキーを生成する

メソッドの引数以外もキャッシュ・キーに含めたい場合があります。これは、 io.quarkus.cache.CacheKeyGenerator インターフェイスを実装し、その実装を @CacheResult または @CacheInvalidate アノテーションの keyGenerator フィールドで宣言することで実現できます。

このクラスは、CDI Bean を表すか、public no-args コンストラクタを宣言する必要があります。 それが CDI Bean を表している場合、キー・ジェネレータはキャッシュ・キーの計算中に注入されます。 そうでない場合は、キャッシュ・キーの計算ごとに、その既定のコンストラクタを使用してキー・ジェネレータの新しいインスタンスが作成されます。

CDIの場合,そのBean型の集合にそのクラスをもつBeanが正確に一つ存在しなければならず,そうでない場合,ビルドは失敗します。 Beanのスコープに関連付けられたコンテキストは, CacheKeyGenerator#generate() メソッドが呼び出されるとき,有効でなければなりません。 スコープが @Dependent の場合, CacheKeyGenerator#generate() メソッドが完了すると,Bean インスタンスは破棄されます。

以下のキージェネレータはCDI Beanとして注入されます。

package org.acme.cache;

import java.lang.reflect.Method;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

import io.quarkus.cache.CacheKeyGenerator;
import io.quarkus.cache.CompositeCacheKey;

@ApplicationScoped
public class ApplicationScopedKeyGen implements CacheKeyGenerator {

    @Inject
    AnythingYouNeedHere anythingYouNeedHere; (1)

    @Override
    public Object generate(Method method, Object... methodParams) { (2)
        return new CompositeCacheKey(anythingYouNeedHere.getData(), methodParams[1]); (3)
    }
}
1 キージェネレータにCDI Beanをインジェクトすることで、外部データをキャッシュキーに含めることができます。
2 Method を使っている間、メソッドのいくつかは高価になることがあるため注意してください。
3 インデックスからアクセスする前に、このメソッドが十分な引数を持っていることを確認してください。そうでないと、キャッシュキーの計算中に IndexOutOfBoundsException が投げられるかもしれません。

以下のキージェネレータは、デフォルトのコンストラクタを使用してインスタンス化されます。

package org.acme.cache;

import java.lang.reflect.Method;

import io.quarkus.cache.CacheKeyGenerator;
import io.quarkus.cache.CompositeCacheKey;

public class NotABeanKeyGen implements CacheKeyGenerator {

    // CDI injections won't work here because it's not a CDI bean.

    @Override
    public Object generate(Method method, Object... methodParams) {
        return new CompositeCacheKey(method.getName(), methodParams[0]); (1)
    }
}
1 メソッド名をキャッシュキーに含めることは、 Method の他のメソッドとは異なり、高価なものではありません。

どちらの種類のキージェネレーターも、同様の方法で使用することができます。

package org.acme.cache;

import jakarta.enterprise.context.ApplicationScoped;

import org.acme.cache.ApplicationScopedKeyGen;
import org.acme.cache.NotABeanKeyGen;

import io.quarkus.cache.CacheKey;
import io.quarkus.cache.CacheInvalidate;
import io.quarkus.cache.CacheResult;

@ApplicationScoped
public class CachedService {

    @CacheResult(cacheName = "foo", keyGenerator = ApplicationScopedKeyGen.class) (1)
    public Object load(@CacheKey Object notUsedInKey, String keyElement) { (2)
        // Call expensive service here.
    }

    @CacheInvalidate(cacheName = "foo", keyGenerator = NotABeanKeyGen.class) (3)
    public void invalidate(Object keyElement) {
    }

    @CacheInvalidate(cacheName = "foo", keyGenerator = NotABeanKeyGen.class)
    @CacheInvalidate(cacheName = "bar")
    public void invalidate(Integer param0, @CacheKey BigDecimal param1) { (4)
    }
}
1 このキージェネレーターはCDI Beanです。
2 キー・ジェネレータが @CacheResult アノテーションで宣言されているため、 @CacheKey アノテーションは無視されます。
3 このキージェネレーターはCDI Beanではありません。
4 @CacheKey アノテーションは foo キャッシュデータが無効化されると無視されますが、bar キャッシュデータが無効化されると param1 がキャッシュキーになります。

プログラムAPIを使ったキャッシング

Quarkusは、アノテーションAPIを使用して宣言された任意のキャッシュから値を保存、取得、削除するために使用できるプログラムAPIも提供しています。プログラムAPIからのすべての操作はノンブロッキングで、フードの下で Mutiny に依存しています。

キャッシュされたデータにプログラムでアクセスする前に、 io.quarkus.cache.Cache のインスタンスを取得する必要があります。以下のセクションでは、その方法をご紹介します。

@CacheName アノテーションとともに Cache をインジェクトする

io.quarkus.cache.CacheName は、フィールド、コンストラクタパラメータ、またはメソッドのパラメータに使用して、 Cache をインジェクトすることができます。

package org.acme.cache;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

import io.quarkus.cache.Cache;
import io.quarkus.cache.CacheName;
import io.smallrye.mutiny.Uni;

@ApplicationScoped
public class CachedExpensiveService {

    @Inject (1)
    @CacheName("my-cache")
    Cache cache;

    public Uni<String> getNonBlockingExpensiveValue(Object key) { (2)
        return cache.get(key, k -> { (3)
            /*
             * Put an expensive call here.
             * It will be executed only if the key is not already associated with a value in the cache.
             */
        });
    }

    public String getBlockingExpensiveValue(Object key) {
        return cache.get(key, k -> {
            // Put an expensive call here.
        }).await().indefinitely(); (4)
    }
}
1 これはオプションです。
2 このメソッドは、ノンブロッキングである Uni<String> タイプを返します。
3 k 引数には、キャッシュキーの値が含まれています。
4 呼び出しをノンブロッキングにする必要がない場合は、このようにしてブロッキングしてキャッシュ値を取得することができます。

CacheManager から Cache を取得しています。

Cache のインスタンスを取得するもう一つの方法は、まず io.quarkus.cache.CacheManager をインジェクトし、その名前から目的の Cache を取得するやり方です。

package org.acme.cache;

import jakarta.inject.Inject;
import jakarta.inject.Singleton;

import io.quarkus.cache.Cache;
import io.quarkus.cache.CacheManager;

import java.util.Optional;

@Singleton
public class CacheClearer {

    private final CacheManager cacheManager;

    public CacheClearer(CacheManager cacheManager) {
        this.cacheManager = cacheManager;
    }

    public void clearCache(String cacheName) {
        Optional<Cache> cache = cacheManager.getCache(cacheName);
        if (cache.isPresent()) {
            cache.get().invalidateAll().await().indefinitely();
        }
    }
}

プログラムキャッシュキーの構築

プログラムキャッシュキーを構築する前に、アノテーションメソッドが呼び出されたときにアノテーションAPIによってキャッシュキーがどのように構築されるかを知っておく必要があります。これについては、このガイドの <<cache-keys-building-logic> セクションで説明されています。

アノテーションAPIを使って保存されたキャッシュ値を、プログラムAPIを使って取得または削除したい場合、両方のAPIで同じキーが使われていることを確認する必要があります。

CaffeineCache からすべてのキーを取得します。

特定の CaffeineCache からのキャッシュキーは、以下に示すように、変更不可能な Set として取得できます。セットに対するイテレーションが進行している間にキャッシュエントリが変更された場合、セットは変更されないままとなります。

package org.acme.cache;

import jakarta.enterprise.context.ApplicationScoped;

import io.quarkus.cache.Cache;
import io.quarkus.cache.CacheName;
import io.quarkus.cache.CaffeineCache;

import java.util.Set;

@ApplicationScoped
public class CacheKeysService {

    @CacheName("my-cache")
    Cache cache;

    public Set<Object> getAllCacheKeys() {
        return cache.as(CaffeineCache.class).keySet();
    }
}

CaffeineCache にデータを追加する

CaffeineCache にデータを追加するには CaffeineCache#put(Object, CompletableFuture) メソッドを使います。 このメソッドは CompletableFuture をキャッシュ内の与えられたキーと関連付けます。キャッシュに以前からそのキーに関連する値が入っていた場合、古い値はこの CompletableFuture に置き換えられます。非同期計算が失敗した場合、そのエントリは自動的に削除されます。

package org.acme.cache;

import jakarta.enterprise.context.ApplicationScoped;

import io.quarkus.cache.Cache;
import io.quarkus.cache.CacheName;
import io.quarkus.cache.CaffeineCache;

import java.util.concurrent.CompletableFuture;

@ApplicationScoped
public class CacheService {

    @CacheName("my-cache")
    Cache cache;

    @PostConstruct
    public void initialize() {
        cache.as(CaffeineCache.class).put("foo", CompletableFuture.completedFuture("bar"));
    }
}

CaffeineCache からすべてのキーを取得する

特定の CaffeineCache からのキャッシュ値は、以下に示すように存在する場合に取得できます。 与えられたキーがキャッシュに含まれている場合、このメソッドは指定されたキーがマッピングされた CompletableFuture を返します。 その CompletableFuture は計算中であるか、あるいは既に終了している可能性があります。 そうでなければ、このメソッドは null を返します。

package org.acme.cache;

import jakarta.enterprise.context.ApplicationScoped;

import io.quarkus.cache.Cache;
import io.quarkus.cache.CacheName;
import io.quarkus.cache.CaffeineCache;

import java.util.concurrent.CompletableFuture;

@ApplicationScoped
public class CacheKeysService {

    @CacheName("my-cache")
    Cache cache;

    public CompletableFuture<Object> getIfPresent(Object key) {
        return cache.as(CaffeineCache.class).getIfPresent(key);
    }
}

リアルタイムで CaffeineCache の有効期限ポリシーや最大サイズを変更

CaffeineCache の有効期限ポリシーは、そのポリシーが Quarkus の設定で最初に指定されている場合、Quarkus アプリの実行中に変更することができます。同様に、 CaffeineCache の最大サイズも、設定に定義された初期最大サイズでキャッシュが構築された場合、リアルタイムで変更できます。

package org.acme.cache;

import jakarta.inject.Inject;
import jakarta.inject.Singleton;

import io.quarkus.cache.Cache;
import io.quarkus.cache.CacheManager;
import io.quarkus.cache.CaffeineCache;

import java.time.Duration;
import java.util.Optional;import javax.inject.Singleton;

@Singleton
public class CacheConfigManager {

    private final CacheManager cacheManager;

    public CacheConfigManager(CacheManager cacheManager) {
        this.cacheManager = cacheManager;
    }

    public void setExpireAfterAccess(String cacheName, Duration duration) {
        Optional<Cache> cache = cacheManager.getCache(cacheName);
        if (cache.isPresent()) {
            cache.get().as(CaffeineCache.class).setExpireAfterAccess(duration); (1)
        }
    }

    public void setExpireAfterWrite(String cacheName, Duration duration) {
        Optional<Cache> cache = cacheManager.getCache(cacheName);
        if (cache.isPresent()) {
            cache.get().as(CaffeineCache.class).setExpireAfterWrite(duration); (2)
        }
    }

    public void setMaximumSize(String cacheName, long maximumSize) {
        Optional<Cache> cache = cacheManager.getCache(cacheName);
        if (cache.isPresent()) {
            cache.get().as(CaffeineCache.class).setMaximumSize(maximumSize); (3)
        }
    }
}
1 この行は、キャッシュが expire-after-access という設定値で構築された場合にのみ機能します。そうでない場合は、 IllegalStateException がスローされます。
2 この行は、キャッシュが expire-after-write という設定値で構築された場合にのみ機能します。そうでない場合は、 IllegalStateException がスローされます。
3 この行は、キャッシュが maximum-size という設定値で構築された場合にのみ機能します。そうでない場合は、 IllegalStateException がスローされます。

CaffeineCachesetExpireAfterAccesssetExpireAfterWritesetMaximumSize メソッドは、 キャッシュ操作のアトミックスコープ内から決して呼び出してはいけません。

基礎となるキャッシュプロバイダの設定

このエクステンションは、基礎となるキャッシュプロバイダとして Caffeine を使用しています。Caffeine は高性能で最適に近いキャッシングライブラリです。

Caffeine設定プロパティ

Quarkusアプリケーションのデータキャッシュエクステンションをバックアップする各Caffeineキャッシュは、 application.properties ファイルの以下のプロパティーを使用して設定することができます。デフォルトでは、設定されていない場合、キャッシュはどのようなタイプのエヴィクションも実行しません。

以下のすべてのプロパティーで cache-name を設定したいキャッシュの実名に置き換える必要があります。

ビルド時に固定される設定プロパティ - その他の設定プロパティは実行時にオーバーライド可能です。

Configuration property

デフォルト

Whether or not the cache extension is enabled.

Environment variable: QUARKUS_CACHE_ENABLED

Show more

boolean

true

Default configuration applied to all Caffeine caches (lowest precedence)

デフォルト

Minimum total size for the internal data structures. Providing a large enough estimate at construction time avoids the need for expensive resizing operations later, but setting this value unnecessarily high wastes memory.

Environment variable: QUARKUS_CACHE_CAFFEINE_INITIAL_CAPACITY

Show more

int

Maximum number of entries the cache may contain. Note that the cache may evict an entry before this limit is exceeded or temporarily exceed the threshold while evicting. As the cache size grows close to the maximum, the cache evicts entries that are less likely to be used again. For example, the cache may evict an entry because it hasn’t been used recently or very often.

Environment variable: QUARKUS_CACHE_CAFFEINE_MAXIMUM_SIZE

Show more

Specifies that each entry should be automatically removed from the cache once a fixed duration has elapsed after the entry’s creation, or the most recent replacement of its value.

Environment variable: QUARKUS_CACHE_CAFFEINE_EXPIRE_AFTER_WRITE

Show more

Duration

Specifies that each entry should be automatically removed from the cache once a fixed duration has elapsed after the entry’s creation, the most recent replacement of its value, or its last read.

Environment variable: QUARKUS_CACHE_CAFFEINE_EXPIRE_AFTER_ACCESS

Show more

Duration

Whether or not metrics are recorded if the application depends on the Micrometer extension. Setting this value to true will enable the accumulation of cache stats inside Caffeine.

Environment variable: QUARKUS_CACHE_CAFFEINE_METRICS_ENABLED

Show more

boolean

Additional configuration applied to a specific Caffeine cache (highest precedence)

デフォルト

Minimum total size for the internal data structures. Providing a large enough estimate at construction time avoids the need for expensive resizing operations later, but setting this value unnecessarily high wastes memory.

Environment variable: QUARKUS_CACHE_CAFFEINE__CACHE_NAME__INITIAL_CAPACITY

Show more

int

Maximum number of entries the cache may contain. Note that the cache may evict an entry before this limit is exceeded or temporarily exceed the threshold while evicting. As the cache size grows close to the maximum, the cache evicts entries that are less likely to be used again. For example, the cache may evict an entry because it hasn’t been used recently or very often.

Environment variable: QUARKUS_CACHE_CAFFEINE__CACHE_NAME__MAXIMUM_SIZE

Show more

Specifies that each entry should be automatically removed from the cache once a fixed duration has elapsed after the entry’s creation, or the most recent replacement of its value.

Environment variable: QUARKUS_CACHE_CAFFEINE__CACHE_NAME__EXPIRE_AFTER_WRITE

Show more

Duration

Specifies that each entry should be automatically removed from the cache once a fixed duration has elapsed after the entry’s creation, the most recent replacement of its value, or its last read.

Environment variable: QUARKUS_CACHE_CAFFEINE__CACHE_NAME__EXPIRE_AFTER_ACCESS

Show more

Duration

Whether or not metrics are recorded if the application depends on the Micrometer extension. Setting this value to true will enable the accumulation of cache stats inside Caffeine.

Environment variable: QUARKUS_CACHE_CAFFEINE__CACHE_NAME__METRICS_ENABLED

Show more

boolean

期間フォーマットについて

To write duration values, use the standard java.time.Duration format. See the Duration#parse() Java API documentation for more information.

数字で始まる簡略化した書式を使うこともできます:

  • 数値のみの場合は、秒単位の時間を表します。

  • 数値の後に ms が続く場合は、ミリ秒単位の時間を表します。

その他の場合は、簡略化されたフォーマットが解析のために java.time.Duration フォーマットに変換されます:

  • 数値の後に hms が続く場合は、その前に PT が付けられます。

  • 数値の後に d が続く場合は、その前に P が付けられます。

キャッシュの設定は以下のようになります。

quarkus.cache.caffeine."foo".initial-capacity=10 (1)
quarkus.cache.caffeine."foo".maximum-size=20
quarkus.cache.caffeine."foo".expire-after-write=60S
quarkus.cache.caffeine."bar".maximum-size=1000 (2)
1 foo キャッシュの設定を行っています。
2 bar キャッシュの設定を行っています。

マイクロメーターメトリクスの有効化

アノテーションキャッシング API を使って宣言された各キャッシュは、マイクロメーターのメトリクスを使ってモニターすることができます。

キャッシュ・メトリクスの収集は、アプリケーションが quarkus-micrometer-registry-* エクステンションに依存している場合にのみ機能します。QuarkusでMicrometerを使用する方法については、 Micrometerメトリクスガイド を参照してください。

デフォルトでは、キャッシュメトリクスの収集は無効になっています。 application.properties ファイルから有効にすることができます。

quarkus.cache.caffeine."foo".metrics-enabled=true

全ての計測手法のように、メトリクスの収集にはわずかなオーバーヘッドが伴い、アプリケーションのパフォーマンスに影響を与える可能性があります。

収集されたメトリクスには、以下のようなキャッシュの統計情報が含まれています。

  • キャッシュ内のエントリーのおおよその数

  • キャッシュに追加されたエントリーの数

  • ヒットとミスに関する情報を含む、キャッシュルックアップの実行回数

  • エヴィクションの回数とエヴィクションエントリの重み

以下は、 quarkus-micrometer-registry-prometheus エクステンションに依存するアプリケーションで利用可能なキャッシュメトリクスの例です。

# HELP cache_size The number of entries in this cache. This may be an approximation, depending on the type of cache.
# TYPE cache_size gauge
cache_size{cache="foo",} 8.0
# HELP cache_puts_total The number of entries added to the cache
# TYPE cache_puts_total counter
cache_puts_total{cache="foo",} 12.0
# HELP cache_gets_total The number of times cache lookup methods have returned a cached value.
# TYPE cache_gets_total counter
cache_gets_total{cache="foo",result="hit",} 53.0
cache_gets_total{cache="foo",result="miss",} 12.0
# HELP cache_evictions_total cache evictions
# TYPE cache_evictions_total counter
cache_evictions_total{cache="foo",} 4.0
# HELP cache_eviction_weight_total The sum of weights of evicted entries. This total does not include manual invalidations.
# TYPE cache_eviction_weight_total counter
cache_eviction_weight_total{cache="foo",} 540.0

アノテーション付きBeanの例

暗黙の簡易キャッシュキー

package org.acme.cache;

import jakarta.enterprise.context.ApplicationScoped;

import io.quarkus.cache.CacheInvalidate;
import io.quarkus.cache.CacheInvalidateAll;
import io.quarkus.cache.CacheResult;

@ApplicationScoped
public class CachedService {

    @CacheResult(cacheName = "foo")
    public Object load(Object key) { (1)
        // Call expensive service here.
    }

    @CacheInvalidate(cacheName = "foo")
    public void invalidate(Object key) { (1)
    }

    @CacheInvalidateAll(cacheName = "foo")
    public void invalidateAll() {
    }
}
1 @CacheKey アノテーションがないので、キャッシュキーは暗黙的です。

明示的な複合キャッシュキー

package org.acme.cache;

import jakarta.enterprise.context.Dependent;

import io.quarkus.cache.CacheInvalidate;
import io.quarkus.cache.CacheInvalidateAll;
import io.quarkus.cache.CacheKey;
import io.quarkus.cache.CacheResult;

@Dependent
public class CachedService {

    @CacheResult(cacheName = "foo")
    public String load(@CacheKey Object keyElement1, @CacheKey Object keyElement2, Object notPartOfTheKey) { (1)
        // Call expensive service here.
    }

    @CacheInvalidate(cacheName = "foo")
    public void invalidate(@CacheKey Object keyElement1, @CacheKey Object keyElement2, Object notPartOfTheKey) { (1)
    }

    @CacheInvalidateAll(cacheName = "foo")
    public void invalidateAll() {
    }
}
1 キャッシュキーは明示的に 2 つの要素で構成されています。メソッドシグネチャには、キーの一部ではない第三引数も含まれています。

デフォルトのキャッシュキー

package org.acme.cache;

import jakarta.enterprise.context.Dependent;

import io.quarkus.cache.CacheInvalidate;
import io.quarkus.cache.CacheInvalidateAll;
import io.quarkus.cache.CacheResult;

@Dependent
public class CachedService {

    @CacheResult(cacheName = "foo")
    public String load() { (1)
        // Call expensive service here.
    }

    @CacheInvalidate(cacheName = "foo")
    public void invalidate() { (1)
    }

    @CacheInvalidateAll(cacheName = "foo")
    public void invalidateAll() {
    }
}
1 メソッドには引数がないため、キャッシュ名から派生した一意のデフォルトキャッシュキーが使用されます。

単一メソッドでの複数アノテーション

package org.acme.cache;

import jakarta.inject.Singleton;

import io.quarkus.cache.CacheInvalidate;
import io.quarkus.cache.CacheInvalidateAll;
import io.quarkus.cache.CacheResult;

@Singleton
public class CachedService {

    @CacheInvalidate(cacheName = "foo")
    @CacheResult(cacheName = "foo")
    public String forceCacheEntryRefresh(Object key) { (1)
        // Call expensive service here.
    }

    @CacheInvalidateAll(cacheName = "foo")
    @CacheInvalidateAll(cacheName = "bar")
    public void multipleInvalidateAll(Object key) { (2)
    }
}
1 このメソッドを使用して、指定されたキーに対応するキャッシュエントリーを強制的に更新することができます。
2 このメソッドは、一度の呼び出しで foo および bar キャッシュからのすべてのエントリーを無効にします。

すべてのアプリケーションキャッシュの消去

package org.acme.cache;

import jakarta.inject.Inject;
import jakarta.inject.Singleton;

import io.quarkus.cache.CacheManager;

@Singleton
public class CacheClearer {

    private final CacheManager cacheManager;

    public CacheClearer(CacheManager cacheManager) {
        this.cacheManager = cacheManager;
    }

    public void clearAllCaches() {
        for (String cacheName : cacheManager.getCacheNames()) {
            cacheManager.getCache(cacheName).get().invalidateAll().await().indefinitely();
        }
    }
}

ネガティブキャッシングとnull

(高価な)リモートコールの結果をキャッシュしたい場合があります。リモートコールが失敗した場合、結果や例外をキャッシュするのではなく、 次の呼び出しでリモートコールを再試行したい場合があります。

シンプルなアプローチとしては、例外をキャッチして null を返すことで、呼び出し元がそれに応じて行動できるようにすることができます。

サンプルコード
    public void caller(int val) {

        Integer result = callRemote(val); (1)
        if (result != null) {
            System.out.println("Result is " + result);
        else {
            System.out.println("Got an exception");
        }
    }

    @CacheResult(cacheName = "foo")
    public Integer callRemote(int val)  {

        try {
            Integer val = remoteWebServer.getResult(val); (2)
            return val;
        } catch (Exception e) {
            return null; (3)
        }
    }
1 リモートを呼び出すためにメソッドを実行
2 リモートコールを行い、その結果を返却
3 例外が発生時にリターンする

このアプローチには不幸な副作用があります。先に述べたように、Quarkusは null の値をキャッシュすることもできます。つまり、同じパラメーター値を持つ callRemote() への次の呼び出しは、キャッシュの外で応答され、 null が返され、リモートコールは行われないということです。これはシナリオによっては望ましいことかもしれませんが、通常は結果が返ってくるまでリモートコールを再試行したいものです。

例外をバブルアップさせる

リモートコールの結果をキャッシュ(マーカー)しないようにするには、コールされたメソッドから例外をバブルアップし、呼び出し側でキャッチする必要があります。

例外をバブルアップ
   public void caller(int val) {
       try {
           Integer result = callRemote(val);  (1)
           System.out.println("Result is " + result);
       } catch (Exception e) {
           System.out.println("Got an exception");
   }

   @CacheResult(cacheName = "foo")
   public Integer callRemote(int val) throws Exception { (2)

      Integer val = remoteWebServer.getResult(val);  (3)
      return val;

   }
1 リモートを呼び出すためにメソッドを実行
2 例外がバブルアップする場合がある
3 これは、あらゆる種類のリモート例外を投げることができます

リモートへの呼び出しが例外をスローした場合、キャッシュは結果を保存しないので、 同じパラメーター値を持つ callRemote() への後続の呼び出しがキャッシュから応答されることはありません。その代わりに、リモートへの呼び出しを再度試みることになります。

ネイティブ化

Cache エクステンションは、ネイティブ実行可能ファイルの構築をサポートしています。

実行時の速度を最適化するために、Caffeineはキャッシュ設定に応じて選択される多くのキャッシュ実装クラスを組み込んでいます。すべてのクラスを登録するのは非常にコストがかかるため、リフレクションのためにすべてのクラスを登録していません(登録されていないクラスはネイティブ実行可能ファイルに含まれません)。

ここでは、最も一般的な実装を登録していますが、キャッシュの設定によっては、以下のようなエラーが発生する場合があります。

2021-12-08 02:32:02,108 ERROR [io.qua.run.Application] (main) Failed to start application (with profile prod): java.lang.ClassNotFoundException: com.github.benmanes.caffeine.cache.PSAMS (1)
        at java.lang.Class.forName(DynamicHub.java:1433)
        at java.lang.Class.forName(DynamicHub.java:1408)
        at com.github.benmanes.caffeine.cache.NodeFactory.newFactory(NodeFactory.java:111)
        at com.github.benmanes.caffeine.cache.BoundedLocalCache.<init>(BoundedLocalCache.java:240)
        at com.github.benmanes.caffeine.cache.SS.<init>(SS.java:31)
        at com.github.benmanes.caffeine.cache.SSMS.<init>(SSMS.java:64)
        at com.github.benmanes.caffeine.cache.SSMSA.<init>(SSMSA.java:43)
1 PSAMS は、Caffeineの数あるキャッシュ実装クラスのひとつなので、この部分は変わるかもしれません。

このエラーが発生した場合は、アプリケーションクラスに以下のアノテーションを追加することで、簡単に修正することができます(あるいは、このアノテーションをホストするためだけに、 Reflections のような新しいクラスを作成することもできます)。

@RegisterForReflection(classNames = { "com.github.benmanes.caffeine.cache.PSAMS" }) (1)
1 配列なので、複数のキャッシュ実装が必要な構成の場合、一度に登録することができます。

このアノテーションは、リフレクション向けにキャッシュ実装クラスを登録し、そのクラスをネイティブ実行可能ファイルに含めます。

関連コンテンツ