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

Spring Cache APIのためのQuarkusエクステンション

ユーザーは キャッシュのQuarkusアノテーションを使用することが推奨されていますが、Quarkusは、 spring-cache エクステンションの形式でSpring Cacheアノテーションのための互換性レイヤーを提供しています。

このガイドでは、Quarkusアプリケーションが、よく知られているSpring Cacheアノテーションを活用して、Spring Beansのアプリケーションデータのキャッシングを可能にする方法を説明します。

前提条件

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

  • 約15分

  • IDE

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

  • Apache Maven 3.8.6

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

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

  • Spring DI エクステンションにある程度慣れている

Mavenプロジェクトの作成

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

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

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

Quarkus CLIのインストール方法や使用方法については、<a href="cli-tooling.html">Quarkus CLIガイド</a> を参照してください。

Maven
mvn io.quarkus.platform:quarkus-maven-plugin:2.14.2.Final:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=spring-cache-quickstart \
    -Dextensions='resteasy-reactive,spring-di,spring-cache' \
    -DnoCode
cd spring-cache-quickstart

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

このコマンドは、 spring-cachespring-di のエクステンションをインポートするプロジェクトを生成します。

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

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

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

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

REST APIの作成

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

package org.acme.spring.cache;

import java.time.LocalDate;

import org.springframework.stereotype.Component;

@Component
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/spring/cache/WeatherForecast.java をこのように作成します。

package org.acme.spring.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;
    }
}

あとは、サービスとレスポンスを使用するために WeatherForecastResource クラスを作成する必要があります。

package org.acme.spring.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 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 の値を加えたものから始まります。

全て終わりました!正常に動作しているか確認してみましょう。

まず、アプリケーションを実行します。

コマンドラインインタフェース
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 org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;

@Component
public class WeatherForecastService {

    @Cacheable("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は、以下のSpring Cacheアノテーションとの互換性を提供しています。

  • @Cacheable

  • @CachePut

  • @CacheEvict

この最初のバージョンの Spring Cache アノテーションエクステンションでは、これらのアノテーションのすべての機能がサポートされているわけではないことに注意してください (サポートされていない機能を使用しようとすると、適切なエラーがログに記録されます)。しかし、今後のリリースでは追加機能が計画されています。