1. 序章

この記事では、SpringWebFluxが@Cacheableアノテーションとどのように相互作用するかを説明します。 最初に、いくつかの一般的な問題とそれらを回避する方法について説明します。 次に、利用可能な回避策について説明します。 最後に、いつものように、コード例を提供します。

2. @Cacheableおよびリアクティブタイプ

このトピックはまだ比較的新しいものです。 この記事を書いている時点では、@Cacheableとリアクティブフレームワークの間に流暢な統合はありませんでした。 主な問題は、非ブロッキングキャッシュの実装がないことです(JSR-107キャッシュAPIがブロッキングしています)。 Redisのみがリアクティブドライバーを提供しています。

前の段落で述べた問題にもかかわらず、サービスメソッドで@Cacheableを使用できます。 これにより、ラッパーオブジェクト(MonoまたはFlux)がキャッシュされますが、メソッドの実際の結果はキャッシュされません。

2.1. プロジェクトの設定

これをテストで説明しましょう。 テストの前に、プロジェクトを設定する必要があります。 リアクティブなMongoDBドライバーを使用して単純なSpringWebFluxプロジェクトを作成します。 MongoDBを個別のプロセスとして実行する代わりに、Testcontainersを使用します。

テストクラスには@SpringBootTestの注釈が付けられ、次のものが含まれます。

final static MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:4.0.10"));

@DynamicPropertySource
static void mongoDbProperties(DynamicPropertyRegistry registry) {
    mongoDBContainer.start();
    registry.add("spring.data.mongodb.uri",  mongoDBContainer::getReplicaSetUrl);
}

これらの行は、MongoDBインスタンスを開始し、URIをSpringBootに渡して、Mongoリポジトリーを自動構成します。

このテストでは、saveメソッドとgetItemメソッドを使用してItemServiceクラスを作成します。

@Service
public class ItemService {

    private final ItemRepository repository;

    public ItemService(ItemRepository repository) {
        this.repository = repository;
    }
    @Cacheable("items")
    public Mono<Item> getItem(String id){
        return repository.findById(id);
    }
    public Mono<Item> save(Item item){
        return repository.save(item);
    }
}

application.properties、では、キャッシュとリポジトリにロガーを設定して、テストで何が起こっているかを監視できるようにします。

logging.level.org.springframework.data.mongodb.core.ReactiveMongoTemplate=DEBUG
logging.level.org.springframework.cache=TRACE

2.2. 初期テスト

セットアップ後、テストを実行して結果を分析できます。

@Test
public void givenItem_whenGetItemIsCalled_thenMonoIsCached() {
    Mono<Item> glass = itemService.save(new Item("glass", 1.00));

    String id = glass.block().get_id();

    Mono<Item> mono = itemService.getItem(id);
    Item item = mono.block();

    assertThat(item).isNotNull();
    assertThat(item.getName()).isEqualTo("glass");
    assertThat(item.getPrice()).isEqualTo(1.00);

    Mono<Item> mono2 = itemService.getItem(id);
    Item item2 = mono2.block();

    assertThat(item2).isNotNull();
    assertThat(item2.getName()).isEqualTo("glass");
    assertThat(item2.getPrice()).isEqualTo(1.00);
}

コンソールでは、この出力を確認できます(簡潔にするために重要な部分のみが示されています)。

Inserting Document containing fields: [name, price, _class] in collection: item...
Computed cache key '618817a52bffe4526c60f6c0' for operation Builder[public reactor.core.publisher.Mono...
No cache entry for key '618817a52bffe4526c60f6c0' in cache(s) [items]
Computed cache key '618817a52bffe4526c60f6c0' for operation Builder[public reactor.core.publisher.Mono...
findOne using query: { "_id" : "618817a52bffe4526c60f6c0"} fields: Document{{}} for class: class com.baeldung.caching.Item in collection: item...
findOne using query: { "_id" : { "$oid" : "618817a52bffe4526c60f6c0"}} fields: {} in db.collection: test.item
Computed cache key '618817a52bffe4526c60f6c0' for operation Builder[public reactor.core.publisher.Mono...
Cache entry for key '618817a52bffe4526c60f6c0' found in cache 'items'
findOne using query: { "_id" : { "$oid" : "618817a52bffe4526c60f6c0"}} fields: {} in db.collection: test.item

最初の行に、insertメソッドが表示されています。 その後、 getItem が呼び出されると、Springはこのアイテムのキャッシュをチェックしますが、見つかりません。MongoDBにアクセスしてこのレコードをフェッチします。 2番目のgetItem呼び出しで、Springは再びキャッシュをチェックし、そのキーのエントリを見つけますが、それでもこのレコードをフェッチするためにMongoDBに移動します。

これは、SpringがgetItemメソッドの結果をキャッシュするために発生します。これはMonoラッパーオブジェクトです。 ただし、結果自体については、データベースからレコードをフェッチする必要があります。

次のセクションでは、この問題の回避策を提供します。

3. Mono /Fluxの結果をキャッシュする

MonoおよびFluxには、この状況で回避策として使用できるキャッシュメカニズムが組み込まれています。 前に述べたように、 @Cacheable はラッパーオブジェクトをキャッシュし、組み込みのキャッシュを使用して、サービスメソッドの実際の結果への参照を作成できます。

@Cacheable("items")
public Mono<Item> getItem_withCache(String id) {
    return repository.findById(id).cache();
}

この新しいサービスメソッドを使用して、前の章のテストを実行してみましょう。 出力は次のようになります。

Inserting Document containing fields: [name, price, _class] in collection: item
Computed cache key '6189242609a72e0bacae1787' for operation Builder[public reactor.core.publisher.Mono...
No cache entry for key '6189242609a72e0bacae1787' in cache(s) [items]
Computed cache key '6189242609a72e0bacae1787' for operation Builder[public reactor.core.publisher.Mono...
findOne using query: { "_id" : "6189242609a72e0bacae1787"} fields: Document{{}} for class: class com.baeldung.caching.Item in collection: item
findOne using query: { "_id" : { "$oid" : "6189242609a72e0bacae1787"}} fields: {} in db.collection: test.item
Computed cache key '6189242609a72e0bacae1787' for operation Builder[public reactor.core.publisher.Mono...
Cache entry for key '6189242609a72e0bacae1787' found in cache 'items'

ほぼ同様の出力を見ることができます。 今回のみ、アイテムがキャッシュで見つかったときに追加のデータベースルックアップはありません。 このソリューションでは、キャッシュの有効期限が切れたときに潜在的な問題が発生します。  キャッシュのキャッシュを使用しているため、両方のキャッシュに適切な有効期限を設定する必要があります。 経験則では、FluxキャッシュTTLは@Cacheableより長くする必要があります。

4. ReactorAddonの使用

Reactor 3アドオンを使用すると、CacheMonoクラスとCacheFluxクラスでさまざまなキャッシュ実装を流暢に使用できます。 この例では、Caffeineキャッシュを構成します。

public ItemService(ItemRepository repository) {
    this.repository = repository;
    this.cache = Caffeine.newBuilder().build(this::getItem_withAddons);
}

ItemService コンストラクターでは、最小限の構成でCaffeineキャッシュを初期化し、新しいサービスメソッドではそのキャッシュを使用します。

@Cacheable("items")
public Mono<Item> getItem_withAddons(String id) {
    return CacheMono.lookup(cache.asMap(), id)
      .onCacheMissResume(() -> repository.findById(id).cast(Object.class)).cast(Item.class);
}

CacheMonoは内部でSignalクラスと連携するため、適切なオブジェクトを返すためにキャストを行う必要があります。

以前からテストを再実行すると、前の例と同様の出力が得られます。

5. 結論

この記事では、SpringWebFluxが@Cacheableとどのように相互作用するかについて説明しました。 さらに、それらの使用方法といくつかの一般的な問題について説明しました。 いつものように、この記事のコードはGitHubにあります。