Spring Cacheとは
Spring Cacheは、Springでキャッシュ機能を使用するためのライブラリです。
Spring Cacheでは抽象的なキャッシュ機能の枠組みを提供します。そのため、内部の具体的な実装を気にせずに簡単にキャッシュ機能を使うことができます。
実装はデフォルト設定のほか、既存のキャッシュライブラリを使用することができます。例えば、CaffeineやJCacheなどをSpring Cache実装として選択することができます。
Spring Cacheのアーキテクチャ
具体的なキャッシュライブラリのことは考えず、まずはSpring Cacheのアーキテクチャを眺めてみます。
このアーキテクチャの概要は、Macchinetta Framework documentationで掲載されている図がとてもわかり易いので、構成だけ引用させていただきました。
青いアイコンが、Spring Cacheで提供される部分になります。
データ読み込み時の流れ
- データを参照するモジュールは、
@Cacheable
アノテーションがついたデータ供給用メソッドを呼び出す。 - Cache AOPが提供する
@Cacheable
にキー値が渡される。キー値はデフォルトで引数値の組み合わせとなる。Cache AOPはキー値をもとにCacheManagerにキャッシュ値の有無を問い合わせる。 - CacheManagerはキー値を元にハッシュテーブルに問い合わせる。キャッシュが存在すればキャッシュ値をCache AOPに返す。
- Cache AOPはキャッシュ値を受け取り、呼び出し元に返す。キャッシュがヒットしなかった場合、通常通りデータ供給用メソッドを実行し、その結果を返す。このとき、供給用メソッドの引数をキー値として(デフォルト設定であれば)、供給用メソッドの結果がハッシュテーブルに保存される。
Spring Cacheを構成するコンポーネント
Cache AOP
@Cacheable
などCacheを有効化するためのアノテーションです。Cache機能の入り口となるインタフェースになります。この層が、キャッシュデータの使用元であるモジュールから、データがキャッシュされたものなのか、最新のデータなのかといった詳細を隠蔽します。
キャッシュ(コンピューティング) - Wikipedia)でも書かれていますが、
キャッシュは通常、隣接する層からは見えないように設計されている抽象化層です。
のように、通常キャッシュは隣接するデータ参照モジュールからはキャッシュの有無は隠蔽されます。 AOPという名がついているのはSpring AOPを使用した実装であるためで、Spring AOPはアプリケーション横断的に処理を挿入することを容易にします。この処理挿入は通常アノテーションで行われ、Spring Cacheもこのアノテーションによる処理挿入を利用しています。
CacheManager
Spring Cacheのキャッシュ処理をコントロールします。上図のキャッシュデータ引き当て処理の場合、CacheManagerはハッシュテーブルからキャッシュキーに対応するキャッシュ値を読み出し返します。CacheManagerはあくまでインタフェースであり、実装はキャッシュ実装によって変わります。
ハッシュテーブル
キャッシュ値をキーに紐付けて保存します。保存されたキャッシュがどこに格納されているのか、どのように管理されるのかはキャッシュ実装によって変わりますが、主にHashMap実装になります。
実装
では、実際にSpring Cacheを使用したキャッシュ処理を実装してみましょう。
Sprint Initializr上でSpring Web StartarとSpring Cacheを導入したプロジェクトを作成しました。言語選定はKotlinとしています。
プロジェクト内の構造は以下の通り。
└── springcachedemo ├── ServletInitializer.kt ├── SpringCacheDemoApplication.kt ├── application │ └── controller │ └── ProductController.kt ├── config │ └── cache │ ├── CacheConfig.kt │ └── MyKeyGenerator.kt └── domain ├── entity │ └── Product.kt ├── repository │ └── ProductRepository.kt └── service └── ProductService.kt
依存関係は下記の通り。
dependencies { implementation("org.springframework.boot:spring-boot-starter-cache") implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.boot:spring-boot-starter-web") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") runtime ("mysql:mysql-connector-java") providedRuntime("org.springframework.boot:spring-boot-starter-tomcat") testImplementation("org.springframework.boot:spring-boot-starter-test") }
キャッシュの検証用アプリとして、商品情報を取得するAPIを実装します。
また検証のためRDB(MySQL)上にテーブル PRODUCTS
を用意し、その中に100000件のレコードを用意しました。
CREATE TABLE `PRODUCTS` ( `ID` int(11) NOT NULL AUTO_INCREMENT, `NAME` varchar(45) COLLATE utf8mb4_general_ci NOT NULL, `PRICE` varchar(45) COLLATE utf8mb4_general_ci NOT NULL, PRIMARY KEY (`ID`) ) ENGINE=InnoDB AUTO_INCREMENT=100001 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
mysql> SELECT count(ID) FROM `cache-example`.PRODUCTS; +-----------+ | count(ID) | +-----------+ | 100000 | +-----------+ 1 row in set (0.01 sec)
次にコード実装に移ります。
まず、レコードをマッピングするオブジェクトのEntityクラスとして Product
を定義します。
IDはAutoIncrementとしたので @GeneratedValue
で明示的に値の自動生成を記述します。
@Entity @Table(name = "PRODUCTS") data class Product( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "ID") val id: Int?, @Column(name = "NAME") val name: String, @Column(name = "PRICE") val price: Int )
Product
テーブルを抽象化するリポジトリ ProductRepository
を用意。
@Repository interface ProductRepository : JpaRepository<Product, Int>
ProductRepository
を参照して商品情報を取得するサービスクラス ProductService
では、商品情報取得メソッド getProducts()
に@Cacheable
アノテーションを付加し、取得結果がキャッシュされるようにしています。 @Cacheable
の引数としてキャッシュを識別するラベルを付与します。
@Service class ProductService( @Autowired private val productRepository: ProductRepository ) { @Cacheable("getProducts") fun getProducts(): List<Product> = productRepository.findAll() }
商品情報取得APIを定義した ProductController
クラスを用意します。
呼び出し先の ProductService::getProducts()
はキャッシュ設定が効いているので、二回目以降の呼び出しでは処理速度が変化するはずです。
@RestController @RequestMapping("/products") class ProductController( @Autowired private val productService: ProductService ) { @GetMapping("/") fun getProducts() = productService.getProducts() }
最後に、 @SpringBootApplication
を付加したクラスに @EnableCaching
アノテーションを付加して、アプリケーションに対するSpring Cache機能を有効化します。
@SpringBootApplication @EnableCaching class SpringCacheDemoApplication fun main(args: Array<String>) { runApplication<SpringCacheDemoApplication>(*args) }
この状態でアプリケーションを起動し、以下のAPIをSpring Cache設定有り・無しの状態で3回ほど実行してそのレスポンスタイム(リクエストを送信してから、応答が返るまで、データ整形を除く)を見ます。なお、キャッシュなしの場合でも別の部分でキャッシュが効いている可能性があるため、複数回試行します。
$ curl http://localhost:8080/api/v1/products/ | jq '.[]' { "id": 1, "name": "prod0", "price": 0 } { "id": 2, "name": "prod1", "price": 100 }
キャッシュ無効
1st | 1307 ms |
2nd | 618 ms |
3nd | 451 ms |
キャッシュ有効
1st | 1277 ms |
2nd | 61 ms |
3nd | 53 ms |
キャッシュ前と比較し、キャッシュ後のレスポンスタイムは約10分の1まで短くなりました。
他の実装でも線形にこの速度短縮が適用されるわけではありませんが、十分キャッシュの効果は現れているといえるでしょう。
Spring Cacheの主な機能
@Cacheable
Cacheable (Spring Framework 5.1.9.RELEASE API)
メソッド(またはクラス内のすべてのメソッド)を呼び出した結果をキャッシュできることを示す注釈。推奨メソッドが呼び出されるたびに、キャッシュ動作が適用され、指定された引数に対してメソッドが既に呼び出されているかどうかがチェックされます。賢明なデフォルトでは、単にメソッドのパラメーターを使用してキーを計算しますが、SpEL式はkey()属性を介して提供するか、カスタム KeyGenerator実装でデフォルトの実装を置き換えることができます(keyGenerator()を参照)。
(Cacheable (Spring Framework 5.1.9.RELEASE API) より)
@Cacheableをメソッドに付加することで、その返戻値の結果をキャッシュすることができます。キャッシュストレージや格納データ構造は後述するキャッシュ実装に任せます。
@Cacheable("getProducts") fun getProducts(): List<Product> = productRepository.findAll()
キャッシュキー
Spring Cacheでは、キャッシュの結果に対し一意のキーを設定します。
このキーの値はデフォルトではキャッシュの引数を元に生成されます。
ただし、引数 key
にSpEL式と呼ばれる形式で独自のキーを設定することもできます。
@Cacheable(value = ["getProduct"], key = "'products/' + #id") fun getProduct(id: Int): Product = productRepository.findById(id).orElseThrow()
上記の例では、引数 key
に 'products/' という文字列と、引数である id
を結合した文字列を設定した例となっています。
keyに渡す文字列内では、静的文字列はシングルクォートで囲む必要があります(これをしないと、変数として扱われてしまいます)。
KeyGenerator
@Cacheableデフォルトキーの生成規則をカスタマイズできます。
KeyGenerator
インタフェースを実装したクラスを定義し、メソッド generate
内でどのようなキーを生成するかを定義します。
class MyKeyGenerator : KeyGenerator { override fun generate(target: Any, method: Method, vararg params: Any?): Any { return target.javaClass.simpleName + "/" + method.name + "/" + params.joinToString() } }
生成したKeyGeneratorはBeanとして登録し、 @Cacheable
の引数 keyGenerator
に設定したBean名を渡せば自前のKeyGeneratorが適用されます。
@Configurationclass CacheConfig : CachingConfigurerSupport() { @Bean("MyKeyGenerator") override fun keyGenerator(): KeyGenerator? = MyKeyGenerator() }
@Service class ProductService(@Autowired private val productRepository: ProductRepository) { @Cacheable("getProducts", keyGenerator = "MyKeyGenerator") fun getProductsWithMyKeyCache(): List<Product> = productRepository.findAll() }
CacheManager
Springのキャッシュ機能を管理するクラスです。キャッシュの引当処理と、キャッシュに紐づくキーの取得機能を提供します。CacheManager実装では、キャッシュの有効期限などより細かい機能を提供します。
CacheManagerを用意せずとも @Cacheable
アノテーションでキャッシュ機能を使うことができますが、@Cacheable
の引数にCacheManagerを渡すことで、引数に渡したCacheManagerの管理の元、キャッシュ機能を使用することができます。
以下の例では、 SimpleCacheManager
クラスをインスタンス化し、それを明示的に @Cacheable
に引き渡しています。
@Configurationclass CacheConfig : CachingConfigurerSupport() { @Bean("MyCacheManager") override fun cacheManager(): CacheManager? = SimpleCacheManager() }
先程の KeyGenerator
同様、定義した CacheManager
Beanの名称を @Cacheable
アノテーション引数に渡します。
@Service class ProductService(@Autowired private val productRepository: ProductRepository) { @Cacheable("getProducts", cacheManager = "MyCacheManager") fun getProducts(): List<Product> = productRepository.findAll() }
上記の SimpleCacheManager
はデフォルトで使用されるCacheManager実装ですが、このキャッシュマネージャを独自にカスタマイズしたり、キャッシュライブラリの実装に基づいたCacheManagerも使用することができます。
@CachePut
キャッシュの値を更新します。
@Cacheable
で指定したラベル値を指定し、それを更新するメソッドに付与します。
CachePut(Spring Framework 5.1.9.RELEASE API)
メソッド(またはクラスのすべてのメソッド)がcache put操作をトリガーすることを示す注釈 。 @Cacheable注釈とは対照的に、この注釈は推奨されるメソッドをスキップさせません。むしろ、常にメソッドが呼び出され、その結果が関連付けられたキャッシュに保存されます。Java8のOptional戻り値の型は自動的に処理され、そのコンテンツはキャッシュに保存されます(存在する場合)。
キャッシュは @CachePut
を付与したメソッドの戻り値によって更新され、引き続き更新後の値がキャッシュされます。
@CachePut("getProduct", key = "'products/' + #product.id") fun putProduct(product: Product) = productRepository.save(product)
@CacheEvict
付加したメソッドを呼び出すと、指定したキーに紐づくキャッシュを削除します。
主に不要なキャッシュを能動的に削除したい場合に使用します。
例えば、外部からキャッシュを削除するAPIを定義し、 @CacheEvict
を付加したServiceメソッドを呼び出していつでも外からキャッシュ値を削除する・・・といったことができます。
@CacheEvict("getProduct", key = "'products/' + #id") fun deleteProducts(id: Int) = productRepository.deleteAll()
参考文献
- Spring Cache
- 4.2. キャッシュの抽象化(Cache Abstraction) — Macchinetta Framework オンライン版クラウド拡張 Development Guideline 1.1.1.RELEASE documentation
- キャッシュ(コンピューティング) - Wikipedia#The_difference_between_buffer_and_cache)
- Cacheable (Spring Framework 5.1.9.RELEASE API)
- CachePut(Spring Framework 5.1.9.RELEASE API)
- Spring Cache - Creating a Custom KeyGenerator | Baeldung
- 6. Spring Expression Language (SpEL)