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

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

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

この技術は、previewと考えられています。

preview では、下位互換性やエコシステムでの存在は保証されていません。具体的な改善には設定や API の変更が必要になるかもしれませんが、 stable になるための計画は現在進行中です。フィードバックは メーリングリストGitHub の課題管理 で受け付けています。

For a full list of possible statuses, check our FAQ entry.

前提条件

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

  • 約15分

  • IDE

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

  • Apache Maven 3.8.1+

  • 使用したい場合、 Quarkus CLI

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

シナリオ

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

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

ソリューション

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

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

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

Mavenプロジェクトの作成

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

CLI
quarkus create app org.acme:cache-quickstart \
    --extension=cache,resteasy-reactive-jackson \
    --no-code
cd cache-quickstart

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

Quarkus CLIのインストール方法については、Quarkus CLIガイドをご参照ください。

Maven
mvn io.quarkus.platform:quarkus-maven-plugin:2.11.1.Final:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=cache-quickstart \
    -Dextensions="cache,resteasy-reactive-jackson" \
    -DnoCode
cd cache-quickstart

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

This command generates the project and imports the cache and resteasy-reactive-jackson extensions.

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

CLI
quarkus extension add 'cache'
Maven
./mvnw quarkus:add-extension -Dextensions="cache"
Gradle
./gradlew addExtension --extensions="cache"

これにより、 pom.xml に以下が追加されます:

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 javax.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;
    }
}

Now, we just need to create the REST resource. Create the src/main/java/org/acme/cache/WeatherForecastResource.java file with this content:

package org.acme.cache;

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

import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.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 の値を加えたものから始まります。

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

First, run the application using dev mode from the project directory:

CLI
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 javax.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 に近い超高速な応答が返ってくるはずです。

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

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

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

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

Caching using annotations

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

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

@CacheResult

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

When a method annotated with @CacheResult is invoked, Quarkus will compute a cache key and use it to check in the cache whether the method has been already invoked. See the Cache keys building logic section of this guide to learn how the cache key is computed. If a value is found in the cache, it is returned and the annotated method is never actually executed. If no value is found, the annotated method is invoked and the returned value is stored in the cache using the computed key.

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

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

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

@CacheInvalidate

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

When a method annotated with @CacheInvalidate is invoked, Quarkus will compute a cache key and use it to try to remove an existing entry from the cache. See the Cache keys building logic section of this guide to learn how the cache key is computed. If the key does not identify any cache entry, nothing will happen.

@CacheInvalidateAll

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

@CacheKey

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

This annotation is optional and should only be used when some method arguments are NOT part of the cache key.

Cache keys building logic

Cache keys are built by the annotations API using the following logic:

  • If an io.quarkus.cache.CacheKeyGenerator is declared in a @CacheResult or a @CacheInvalidate annotation, then it is used to generate the cache key. The @CacheKey annotations that might be present on some method arguments are ignored.

  • Otherwise, if the method has no arguments, then the cache key is an instance of io.quarkus.cache.DefaultCacheKey built from the cache name.

  • Otherwise, if the method has exactly one argument, then that argument is the cache key.

  • Otherwise, if the method has multiple arguments but only one annotated with @CacheKey, then that annotated argument is the cache key.

  • Otherwise, if the method has multiple arguments annotated with @CacheKey, then the cache key is an instance of io.quarkus.cache.CompositeCacheKey built from these annotated arguments.

  • Otherwise, if the method has multiple arguments and none of them are annotated with @CacheKey, the cache key is an instance of io.quarkus.cache.CompositeCacheKey built from all the method arguments.

Each non-primitive method argument that is part of the key must implement equals() and hashCode() correctly for the cache to work as expected.

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

package org.acme.cache;

import javax.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 メソッドでキャッシュされた値が無効になることはありません。

Generating a cache key with CacheKeyGenerator

You may want to include more than the arguments of a method into a cache key. This can be done by implementing the io.quarkus.cache.CacheKeyGenerator interface and declaring that implementation in the keyGenerator field of a @CacheResult or @CacheInvalidate annotation.

If a CDI scope is declared on a key generator class and if that class has a default qualifier (no qualifier annotation or @javax.enterprise.inject.Default), then the key generator will be injected as a CDI bean during the cache key computation. Otherwise, the key generator will be instantiated using its default constructor. All CDI scopes supported by Quarkus can be used on a key generator.

The following key generator will be injected as a CDI bean:

package org.acme.cache;

import java.lang.reflect.Method;
import javax.enterprise.context.ApplicationScoped;
import javax.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 External data can be included into the cache key by injecting a CDI bean in the key generator.
2 Be careful while using Method, some of its methods can be expensive.
3 Make sure the method has enough arguments before accessing them from their index. Otherwise, an IndexOutOfBoundsException may be thrown during the cache key computation.

The following key generator will be instantiated using its default constructor:

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 Including the method name into the cache key is not expensive, unlike other methods from Method.

Both kinds of key generators can be used in a similar way:

package org.acme.cache;

import javax.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 This key generator is a CDI bean.
2 The @CacheKey annotation will be ignored because a key generator is declared in the @CacheResult annotation.
3 This key generator is not a CDI bean.
4 The @CacheKey annotation will be ignored when the foo cache data is invalidated, but param1 will be the cache key when the bar cache data is invalidated.

Caching using the programmatic API

Quarkus also offers a programmatic API which can be used to store, retrieve or delete values from any cache declared using the annotations API. All operations from the programmatic API are non-blocking and rely on Mutiny under the hood.

Before accessing programmatically the cached data, you need to retrieve an io.quarkus.cache.Cache instance. The following sections will show you how to do that.

Injecting a Cache with the @CacheName annotation

io.quarkus.cache.CacheName can be used on a field, a constructor parameter or a method parameter to inject a Cache:

package org.acme.cache;

import javax.enterprise.context.ApplicationScoped;
import javax.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 This is optional.
2 This method returns the Uni<String> type which is non-blocking.
3 The k argument contains the cache key value.
4 If you don’t need the call to be non-blocking, this is how you can retrieve the cache value in a blocking way.

Retrieving a Cache from the CacheManager

Another way to retrieve a Cache instance consists in injecting the io.quarkus.cache.CacheManager first and then retrieving the desired Cache from its name:

package org.acme.cache;

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

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

import java.util.Optional;

@ApplicationScoped
public class CacheClearer {

    @Inject
    CacheManager cacheManager;

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

Building a programmatic cache key

Before building a programmatic cache key, you need to know how cache keys are built by the annotations API when an annotated method is invoked. This is explained in the Cache keys building logic section of this guide.

Now, if you want to retrieve or delete, using the programmatic API, a cache value that was stored using the annotations API, you just need to make sure the same key is used with both APIs.

Retrieving all keys from a CaffeineCache

The cache keys from a specific CaffeineCache can be retrieved as an unmodifiable Set as shown below. If the cache entries are modified while an iteration over the set is in progress, the set will remain unchanged.

package org.acme.cache;

import javax.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();
    }
}

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

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

Caffeine設定プロパティ

Each of the Caffeine caches backing up the Quarkus application data caching extension can be configured using the following properties in the application.properties file. By default, caches do not perform any type of eviction if not configured.

You need to replace cache-name in all the following properties with the real name of the cache you want to configure.

ビルド時に固定される設定プロパティ - それ以外の設定プロパティは実行時に上書き可能

Configuration property

タイプ

デフォルト

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

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

long

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

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

Duration

Environment variable: QUARKUS_CACHE_CAFFEINE__CACHE_NAME__METRICS_ENABLED

boolean

false

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

期間のフォーマットは標準の java.time.Duration フォーマットを使用します。詳細は Duration#parse() javadoc を参照してください。

数値で始まる期間の値を指定することもできます。この場合、値が数値のみで構成されている場合、コンバーターは値を秒として扱います。そうでない場合は、 PT が暗黙的に値の前に付加され、標準の java.time.Duration 形式が得られます。

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

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 キャッシュの設定を行っています。

Enabling Micrometer metrics

Each cache declared using the annotations caching API can be monitored using Micrometer metrics.

The cache metrics collection will only work if your application depends on a quarkus-micrometer-registry-* extension. See the Micrometer metrics guide to learn how to use Micrometer in Quarkus.

The cache metrics collection is disabled by default. It can be enabled from the application.properties file:

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

Like all instrumentation methods, collecting metrics comes with a small overhead that can impact the application performances.

The collected metrics contain cache statistics such as:

  • the approximate current number of entries in the cache

  • the number of entries that were added to the cache

  • the number of times a cache lookup has been performed, including information about hits and misses

  • the number of evictions and the weight of the evicted entries

Here is an example of cache metrics available for an application that depends on the quarkus-micrometer-registry-prometheus extension:

# 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 javax.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 javax.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 javax.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 javax.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 キャッシュからのすべてのエントリーを無効にします。

Clear all application caches

package org.acme.cache;

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

import io.quarkus.cache.CacheManager;

@ApplicationScoped
public class CacheClearer {

    @Inject
    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() への後続の呼び出しがキャッシュから応答されることはありません。その代わりに、リモートへの呼び出しを再度試みることになります。

ネイティブ化

The Cache extension supports building native executables.

However, to optimize runtime speed, Caffeine embarks many cache implementation classes that are selected depending on the cache configuration. We are not registering all of them for reflection (and the ones not registered are not included into the native executables) as registering all of them would be very costly.

We are registering the most common implementations but, depending on your cache configuration, you might encounter errors like:

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 is one of the many cache implementation classes of Caffeine so this part may vary.

When you encounter this error, you can easily fix it by adding the following annotation to any of your application classes (or you can create a new class such as Reflections just to host this annotation if you prefer):

@RegisterForReflection(classNames = { "com.github.benmanes.caffeine.cache.PSAMS" }) (1)
1 It is an array, so you can register several cache implementations in one go if your configuration requires several of them.

This annotation will register the cache implementation classes for reflection and this will include the classes into the native executable.