Spring でデータキャッシング

このガイドでは、Spring 管理の Bean でキャッシュを有効にするプロセスについて説明します。

構築するもの

簡単な本リポジトリでキャッシュを有効にするアプリケーションを構築します。

必要なもの

本ガイドの完成までの流れ

ほとんどの Spring 入門ガイドと同様に、最初から始めて各ステップを完了するか、すでに慣れている場合は基本的なセットアップステップをバイパスできます。いずれにしても、最終的に動作するコードになります。

最初から始めるには、Spring Initializr から開始に進みます。

基本スキップするには、次の手順を実行します。

完了したときは、gs-caching/complete のコードに対して結果を確認できます。

Spring Initializr から開始

IDE を使用する場合はプロジェクト作成ウィザードを使用します。IDE を使用せずにコマンドラインなどで開発する場合は、この事前に初期化されたプロジェクトからプロジェクトを ZIP ファイルとしてダウンロードできます。このプロジェクトは、このチュートリアルの例に合うように構成されています。

プロジェクトを手動で初期化するには:

  1. IDE のメニューまたはブラウザーから Spring Initializr を開きます。アプリケーションに必要なすべての依存関係を取り込み、ほとんどのセットアップを行います。

  2. Gradle または Maven のいずれかと、使用する言語を選択してください。

  3. 依存関係をクリックして、Spring キャッシュ抽象化を選択します。

  4. 生成をクリックします。

  5. 結果の ZIP ファイルをダウンロードします。これは、選択して構成された Web アプリケーションのアーカイブです。

EclipseIntelliJ のような IDE は新規プロジェクト作成ウィザードから Spring Initializr の機能が使用できるため、手動での ZIP ファイルのダウンロードやインポートは不要です。
プロジェクトを GitHub からフォークして、IDE または他のエディターで開くこともできます。

ブックモデルを作成する

まず、書籍のシンプルなモデルを作成する必要があります。以下にその方法を示します。

Java
package com.example.caching;

public record Book(String isbn, String title) {

}
Kotlin
package com.example.caching

data class Book(
    val isbn: String,
    val title: String
)

ブックリポジトリを作成する

また、そのモデル用のリポジトリも必要です。以下のリストは、そのようなリポジトリを示しています。

Java
package com.example.caching;

public interface BookRepository {

	Book getByIsbn(String isbn);

}
Kotlin
package com.example.caching

interface BookRepository {
    fun getByIsbn(isbn: String): Book
}

Spring Data を使用すれば、幅広い SQL または NoSQL ストアに対応したリポジトリの実装を提供できます。しかし、このガイドでは、ネットワークサービス、低速な遅延、その他の課題など、レイテンシをシミュレートする単純な実装を使用します。以下のリストは、そのようなリポジトリを示しています。

Java
package com.example.caching;

import org.springframework.stereotype.Component;

@Component
public class SimpleBookRepository implements BookRepository {

	@Override
	public Book getByIsbn(String isbn) {
		simulateSlowService();
		return new Book(isbn, "Some book");
	}

	// Don't do this at home
	private void simulateSlowService() {
		try {
			long time = 3000L;
			Thread.sleep(time);
		} catch (InterruptedException e) {
			throw new IllegalStateException(e);
		}
	}

}
Kotlin
package com.example.caching

import org.springframework.stereotype.Component

@Component
class SimpleBookRepository : BookRepository {

    override fun getByIsbn(isbn: String): Book {
        simulateSlowService()
        return Book(isbn, "Some book")
    }

    // Don't do this at home
    private fun simulateSlowService() {
        Thread.sleep(3000L)
    }
}

simulateSlowService は、各 getByIsbn 呼び出しに意図的に 3 秒の遅延を挿入します。後で、この例をキャッシュで高速化します。

リポジトリの使用

次に、リポジトリを接続して、それを使って書籍にアクセスできるようにする必要があります。以下のリストはその方法を示しています。

Java
package com.example.caching;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class CachingApplication {

	public static void main(String[] args) {
		SpringApplication.run(CachingApplication.class, args);
	}

}
Kotlin
package com.example.caching

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class CachingApplication

fun main(args: Array<String>) {
    runApplication<CachingApplication>(*args)
}

@SpringBootApplication は、次のすべてを追加する便利なアノテーションです。

  • @Configuration: アプリケーションコンテキストの Bean 定義のソースとしてクラスにタグを付けます。

  • @EnableAutoConfiguration: クラスパス設定、他の Bean、さまざまなプロパティ設定に基づいて Bean の追加を開始するよう Spring Boot に指示します。例: spring-webmvc がクラスパスにある場合、このアノテーションはアプリケーションに Web アプリケーションとしてフラグを立て、DispatcherServlet のセットアップなどの主要な動作をアクティブにします。

  • @ComponentScan: Spring に、com/example パッケージ内の他のコンポーネント、構成、サービスを探して、コントローラーを検出させるように指示します。

main() メソッドは、Spring Boot の SpringApplication.run() メソッドを使用してアプリケーションを起動します。XML が 1 行もないことに気付きましたか? web.xml ファイルもありません。この Web アプリケーションは 100% 純粋な Java であり、接続機能やインフラストラクチャの構成に対処する必要はありませんでした。

また、BookRepository を注入し、異なる引数で複数回呼び出す CommandLineRunner も必要です。以下のリストはそのクラスを示しています。

Java
package com.example.caching;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class AppRunner implements CommandLineRunner {

	private static final Logger logger = LoggerFactory.getLogger(AppRunner.class);

	private final BookRepository bookRepository;

	public AppRunner(BookRepository bookRepository) {
		this.bookRepository = bookRepository;
	}

	@Override
	public void run(String... args) throws Exception {
		logger.info(".... Fetching books");
		logger.info("isbn-1234 -->" + bookRepository.getByIsbn("isbn-1234"));
		logger.info("isbn-4567 -->" + bookRepository.getByIsbn("isbn-4567"));
		logger.info("isbn-1234 -->" + bookRepository.getByIsbn("isbn-1234"));
		logger.info("isbn-4567 -->" + bookRepository.getByIsbn("isbn-4567"));
		logger.info("isbn-1234 -->" + bookRepository.getByIsbn("isbn-1234"));
		logger.info("isbn-1234 -->" + bookRepository.getByIsbn("isbn-1234"));
	}

}
Kotlin
package com.example.caching

import org.slf4j.LoggerFactory
import org.springframework.boot.CommandLineRunner
import org.springframework.stereotype.Component

@Component
class AppRunner(private val bookRepository: BookRepository) : CommandLineRunner {

    private val logger = LoggerFactory.getLogger(javaClass)

    override fun run(vararg args: String) {
        logger.info(".... Fetching books")
        logger.info("isbn-1234 -->${bookRepository.getByIsbn("isbn-1234")}")
        logger.info("isbn-4567 -->${bookRepository.getByIsbn("isbn-4567")}")
        logger.info("isbn-1234 -->${bookRepository.getByIsbn("isbn-1234")}")
        logger.info("isbn-4567 -->${bookRepository.getByIsbn("isbn-4567")}")
        logger.info("isbn-1234 -->${bookRepository.getByIsbn("isbn-1234")}")
        logger.info("isbn-1234 -->${bookRepository.getByIsbn("isbn-1234")}")
    }
}

この時点でアプリケーションを実行してみると、全く同じ書籍を複数回取得しているにもかかわらず、非常に動作が遅いことに気づくでしょう。以下のサンプル出力(Java の Book レコードを使用)は、意図的にひどいコードによって発生した 3 秒の遅延を示しています。

2014-06-05 12:15:35.783  ... : .... Fetching books
2014-06-05 12:15:40.783  ... : isbn-1234 -->Book[isbn=isbn-1234, title=Some book]
2014-06-05 12:15:43.784  ... : isbn-1234 -->Book[isbn=isbn-1234, title=Some book]
2014-06-05 12:15:46.786  ... : isbn-1234 -->Book[isbn=isbn-1234, title=Some book]

キャッシュを有効にすることで状況を改善できます。

キャッシュを使用可能にする

これで、SimpleBookRepository でキャッシュを有効にして、書籍を books のキャッシュ内に保存できるようになります。リポジトリ定義は以下のとおりです。

Java
package com.example.caching;

import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;

@Component
public class SimpleBookRepository implements BookRepository {

	@Override
	@Cacheable("books")
	public Book getByIsbn(String isbn) {
		simulateSlowService();
		return new Book(isbn, "Some book");
	}

	// Don't do this at home
	private void simulateSlowService() {
		try {
			long time = 3000L;
			Thread.sleep(time);
		} catch (InterruptedException e) {
			throw new IllegalStateException(e);
		}
	}

}
Kotlin
package com.example.caching

import org.springframework.cache.annotation.Cacheable
import org.springframework.stereotype.Component

@Component
class SimpleBookRepository : BookRepository {

    @Cacheable("books")
    override fun getByIsbn(isbn: String): Book {
        simulateSlowService()
        return Book(isbn, "Some book")
    }

    // Don't do this at home
    private fun simulateSlowService() {
        Thread.sleep(3000L)
    }
}

次に、キャッシュアノテーションの処理を有効にする必要があります。以下の例がその方法を示しています。

Java
package com.example.caching;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching
public class CachingApplication {

	public static void main(String[] args) {
		SpringApplication.run(CachingApplication.class, args);
	}

}
Kotlin
package com.example.caching

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.cache.annotation.EnableCaching

@SpringBootApplication
@EnableCaching
class CachingApplication

fun main(args: Array<String>) {
    runApplication<CachingApplication>(*args)
}

@EnableCaching (Javadoc) アノテーションは、public メソッドのキャッシュアノテーションの存在についてすべての Spring Bean をインスペクションするポストプロセッサーをトリガーします。そのようなアノテーションが見つかった場合、メソッド呼び出しをインターセプトし、それに応じてキャッシュ動作を処理するプロキシが自動的に作成されます。

ポストプロセッサーは、@Cacheable (Javadoc) @CachePut (Javadoc) @CacheEvict (Javadoc) アノテーションを処理します。詳細については、Javadoc およびリファレンスガイドを参照してください

Spring Boot は、適切な CacheManager (Javadoc) を自動的に構成して、関連するキャッシュのプロバイダーとして機能します。詳細については、Spring Boot のドキュメントを参照してください。

サンプルでは特定のキャッシュライブラリを使用しないため、キャッシュストアは ConcurrentHashMap を使用する単純なフォールバックです。キャッシング抽象化は、幅広いキャッシュライブラリをサポートし、JSR-107(JCache)に完全に準拠しています。

実行可能 JAR を構築する

コマンドラインから Gradle または Maven を使用してアプリケーションを実行できます。必要なすべての依存関係、クラス、リソースを含む単一の実行可能 JAR ファイルを構築して実行することもできます。実行可能な jar を構築すると、開発ライフサイクル全体、さまざまな環境などで、アプリケーションとしてサービスを簡単に提供、バージョン管理、デプロイできます。

Gradle を使用する場合、./gradlew bootRun を使用してアプリケーションを実行できます。または、次のように、./gradlew build を使用して JAR ファイルをビルドしてから、JAR ファイルを実行できます。

java -jar build/libs/gs-caching-0.0.1-SNAPSHOT.jar

Maven を使用する場合、./mvnw spring-boot:run を使用してアプリケーションを実行できます。または、次のように、./mvnw clean package で JAR ファイルをビルドしてから、JAR ファイルを実行できます。

java -jar target/gs-caching-0.0.1-SNAPSHOT.jar

アプリケーションをテストする

キャッシュが有効になったため、アプリケーションを再度実行し、同じ ISBN を使用する場合と使用しない場合で呼び出しを追加することで、その違いを確認できます。大きな違いが現れるはずです。以下のリストは、キャッシュを有効にした場合の出力例です(Java の Book レコードを使用)。

2016-09-01 11:12:47.033  .. : .... Fetching books
2016-09-01 11:12:50.039  .. : isbn-1234 -->Book[isbn=isbn-1234, title=Some book]
2016-09-01 11:12:53.044  .. : isbn-4567 -->Book[isbn=isbn-4567, title=Some book]
2016-09-01 11:12:53.045  .. : isbn-1234 -->Book[isbn=isbn-1234, title=Some book]
2016-09-01 11:12:53.045  .. : isbn-4567 -->Book[isbn=isbn-4567, title=Some book]
2016-09-01 11:12:53.045  .. : isbn-1234 -->Book[isbn=isbn-1234, title=Some book]
2016-09-01 11:12:53.045  .. : isbn-1234 -->Book[isbn=isbn-1234, title=Some book]

上記の出力例では、書籍の最初の取得にはまだ 3 秒かかります。ただし、同じ本の 2 回目以降ははるかに高速であり、キャッシュが機能していることを示しています。

要約

おめでとう! Spring が管理する Bean でキャッシュを有効にしました。

関連事項

次のガイドも役立つかもしれません:

新しいガイドを作成したり、既存のガイドに貢献したいですか? 投稿ガイドラインを参照してください [GitHub] (英語)

すべてのガイドは、コード用の ASLv2 ライセンス、およびドキュメント用の帰属表示、NoDerivatives クリエイティブコモンズライセンス (英語) でリリースされています。

コードを入手する