- Spring
Spring Boot キャッシュ|@Cacheableの属性一覧と設定例
Spring Boot のキャッシュとは、メソッドに @Cacheable などのアノテーションを付けるだけで、同じ引数の2回目以降の呼び出しを保存済みの結果で返す仕組みです。 保存先(Caffeine、Redis など)は依存関係と spring.cache.* の設定で切り替えられ、メソッド側のコードは変わりません。
この記事では、5つのアノテーションの属性、SpEL で使える変数、プロバイダーの検出順と設定プロパティを一覧にし、Caffeine 版と Redis 版のコピペ用一式を載せます。「キャッシュが効かない」ときの原因早見表と、同じクラス内の呼び出しで効かない理由を Java 単体のデモ(出力つき)でも確かめます。
確認環境:Spring Boot 4.1.1・Spring Framework 7.0.9・Spring Data Redis 4.1.1 の公式リファレンスと API ドキュメントで確認(確認日 2026年10月7日)。Java 単体で再現できる部分(プロキシとキーのデモ)は Ubuntu 24.04・OpenJDK 21.0.10 で実行し、YAML は Python の yaml で構文を確認しています。
Spring Boot キャッシュの早見表
| アノテーション | メソッド本体 | キャッシュへの操作 | 主な用途 |
|---|---|---|---|
@Cacheable |
ヒットしたら実行しない | 無ければ結果を保存 | 読み取り |
@CachePut |
必ず実行する | 結果で上書き | 更新 |
@CacheEvict |
必ず実行する | エントリを削除 | 削除・無効化 |
@Caching |
中身による | 複数の操作をまとめる | 1メソッドで複数操作 |
@CacheConfig |
— | クラス共通の設定 | キャッシュ名の共通化 |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Spring Boot キャッシュ|@Cacheableの属性一覧と設定例」
最小構成で動かす
必要なものは3つです。スターターを入れる、@EnableCaching を付ける、メソッドに @Cacheable を付ける。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
package com.example.demo;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
@EnableCaching
public class CacheConfig {
}
package com.example.demo;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;
@Component
public class MathService {
@Cacheable("piDecimals")
public int computePiDecimal(int precision) {
// 時間のかかる計算
return 0;
}
}
キャッシュ用のライブラリを何も足さない場合は、ConcurrentHashMap を使う Simple プロバイダーが選ばれます。有効期限や件数の上限は持たないので、長く動かすアプリでは次の章の Caffeine や Redis を選びます。
公式リファレンスは、@EnableCaching を main メソッドのあるアプリケーションクラスに付けないよう案内しています。付けるとキャッシュが必須の機能になり、テスト実行時にも常に有効になるためです。上のように専用の設定クラスに分けておきます。
アノテーションと属性の一覧
| 属性 | Cacheable | CachePut | CacheEvict | 意味 |
|---|---|---|---|---|
value/cacheNames |
○ | ○ | ○ | 使うキャッシュの名前(複数可) |
key |
○ | ○ | ○ | キーを決める SpEL 式 |
keyGenerator |
○ | ○ | ○ | キー生成用 Bean の名前(key と同時指定不可) |
cacheManager |
○ | ○ | ○ | 使う CacheManager の Bean 名 |
cacheResolver |
○ | ○ | ○ | 使う CacheResolver の Bean 名(cacheManager と同時指定不可) |
condition |
○ | ○ | ○ | true のときだけ操作する(実行前に評価) |
unless |
○ | ○ | — | true のとき保存しない(実行後に評価) |
sync |
○ | — | — | 同じキーの読み込みを1スレッドにまとめる |
allEntries |
— | — | ○ | キャッシュ内の全エントリを削除する |
beforeInvocation |
— | — | ○ | メソッド実行前に削除する |
既定値は、文字列の属性が空文字、sync・allEntries・beforeInvocation が false です。残りの2つのアノテーションの属性は次のとおりです。
| アノテーション | 属性 | 内容 |
|---|---|---|
@Caching |
cacheable |
@Cacheable の配列 |
@Caching |
put |
@CachePut の配列 |
@Caching |
evict |
@CacheEvict の配列 |
@CacheConfig |
value/cacheNames |
クラス共通のキャッシュ名 |
@CacheConfig |
keyGenerator |
クラス共通のキー生成 Bean |
@CacheConfig |
cacheManager |
クラス共通の CacheManager |
@CacheConfig |
cacheResolver |
クラス共通の CacheResolver |
設定の優先順位は、全体の設定(CachingConfigurer など)< クラスの @CacheConfig < メソッドのアノテーションです。メソッドに書いたものが常に優先されます。
キーの既定の決まり方
key を書かない場合、引数から次のようにキーが作られます。
| 引数 | キー |
|---|---|
| なし | SimpleKey.EMPTY |
| 1つ | その引数そのもの |
| 2つ以上 | 全引数を持つ SimpleKey |
キーに使う引数は、正しい hashCode() と equals() を実装している必要があります。検索条件の一部だけをキーにしたいときは key を書きます。
@Cacheable(cacheNames = "books", key = "#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
syncを使うときの制約
sync = true は、複数のスレッドが同じキーを同時に読み込もうとしたとき、1つだけがメソッドを実行し、ほかは待つ動きにします。API ドキュメントには次の制約が書かれています。
unlessと併用できない- 指定できるキャッシュは1つだけ
- ほかのキャッシュ操作と組み合わせられない
- プロバイダーが同期に対応しているとは限らない(ヒントとして扱われる)
SpELで使える変数の一覧
key・condition・unless の式の中で使える変数です。
| 変数 | 内容 | 例 |
|---|---|---|
#root.methodName |
呼び出されたメソッドの名前 | #root.methodName |
#root.method |
呼び出されたメソッド | #root.method.name |
#root.target |
呼び出し対象のオブジェクト | #root.target |
#root.targetClass |
呼び出し対象のクラス | #root.targetClass |
#root.args |
引数の配列 | #root.args[0] |
#root.caches |
対象のキャッシュの一覧 | #root.caches[0].name |
| 引数名 | 引数を名前で参照 | #isbn |
#a0/#p0 |
引数を位置で参照(0 始まり) | #a0、#p1 |
#result |
メソッドの戻り値 | #result |
- 引数名は、コンパイル時に
-parametersを付けていない場合は使えません。その場合は#a0/#p0の形で参照します。 #resultが使えるのは、unless、@CachePutのkey、@CacheEvict(beforeInvocation = falseのとき)です。- 戻り値が
Optionalのとき、#resultは中身のオブジェクトを指します。空の可能性があるので#result?.hardbackのように?.を使います。
conditionとunlessの使い分け
condition は実行前に引数で判断し、unless は実行後に結果で判断します。
| 観点 | condition |
unless |
|---|---|---|
| 評価のタイミング | メソッド実行の前 | メソッド実行の後 |
| 使える情報 | 引数 | 引数と #result |
| true のとき | キャッシュを使う | 保存しない |
| 向く用途 | 対象の引数を絞る | 結果によって保存を見送る |
// 名前が 32 文字未満のときだけキャッシュを使い、ハードカバーの本は保存しない
@Cacheable(cacheNames = "book", condition = "#name.length() < 32", unless = "#result.hardback")
public Book findBook(String name)
「見つからなかった結果(null)を保存したくない」場合は unless = "#result == null" を使います。
CachePutとCacheEvictの判断基準
どちらもメソッド本体を必ず実行します。違いは、実行後にキャッシュを新しい値で上書きするか、消すかです。
| 状況 | 選ぶもの | 理由 |
|---|---|---|
| 更新メソッドが更新後の値を返す | @CachePut |
次の読み取りがそのままヒットする |
更新メソッドが void や件数を返す |
@CacheEvict |
保存する値が手元に無い |
| 削除した | @CacheEvict |
エントリを消す |
| 一覧のキャッシュに影響する変更 | @CacheEvict(allEntries = true) |
どのキーが影響を受けるか特定しにくい |
| 一括取り込みのあと | @CacheEvict(allEntries = true) |
まとめて作り直す |
| メソッドが失敗しても必ず消したい | @CacheEvict(beforeInvocation = true) |
実行前に削除される |
@CacheEvict の既定(beforeInvocation = false)は、メソッドが正常に終わったあとで削除します。公式リファレンスは、同じメソッドに @CachePut と @Cacheable を一緒に付けないよう案内しています。前者は必ず実行し、後者は実行を省こうとするので、動きが食い違うためです。
コピペ用一式(Caffeine版)
1つのアプリの中で完結するインメモリのキャッシュです。件数の上限と有効期限を設定で指定できます。
依存関係
Maven:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache-test</artifactId>
<scope>test</scope>
</dependency>
Gradle(Kotlin DSL):
dependencies {
implementation("org.springframework.boot:spring-boot-starter-cache")
implementation("com.github.ben-manes.caffeine:caffeine")
testImplementation("org.springframework.boot:spring-boot-starter-cache-test")
}
spring-boot-starter-cache は spring-context-support を含みます。Caffeine や JCache のサポートにはこのモジュールが必要です。
application.yml
spring:
cache:
cache-names: "items,itemLists"
caffeine:
spec: "maximumSize=500,expireAfterWrite=10m"
spec に書けるキーは initialCapacity、maximumSize、maximumWeight、expireAfterAccess、expireAfterWrite、refreshAfterWrite、weakKeys、weakValues、softValues、recordStats です。期間は 50s、11m、6h、3d のように書けます。expireAfterWrite は書き込みからの時間、expireAfterAccess は最後のアクセスからの時間で期限を決めます。
Caffeine の設定は、次の順で適用されます。
spring.cache.caffeine.specに書いた specCaffeineSpecの BeanCaffeineの Bean
設定クラス
package com.example.demo.item;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
@EnableCaching
public class CacheConfig {
}
モデルとリポジトリ
package com.example.demo.item;
import java.io.Serializable;
public record Item(Long id, String name, int stock) implements Serializable {
}
package com.example.demo.item;
import java.util.List;
import java.util.Optional;
public interface ItemRepository {
Optional<Item> findById(Long id);
List<Item> findAll();
Item save(Item item);
void deleteById(Long id);
}
サービス全文
package com.example.demo.item;
import java.util.ArrayList;
import java.util.List;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.stereotype.Service;
@Service
@CacheConfig("items")
public class ItemService {
private final ItemRepository repository;
public ItemService(ItemRepository repository) {
this.repository = repository;
}
// 読み取り:id をキーに保存する。見つからなかった結果は保存しない
@Cacheable(key = "#id", unless = "#result == null")
public Item find(Long id) {
return repository.findById(id).orElse(null);
}
// 一覧:引数が無いのでキーは SimpleKey.EMPTY になる
@Cacheable("itemLists")
public List<Item> findAll() {
return new ArrayList<>(repository.findAll());
}
// 更新:実行して結果で上書きし、一覧のキャッシュは消す
@Caching(
put = @CachePut(key = "#result.id()"),
evict = @CacheEvict(cacheNames = "itemLists", allEntries = true))
public Item save(Item item) {
return repository.save(item);
}
// 削除:対象のエントリと一覧のキャッシュを消す
@Caching(evict = {
@CacheEvict(key = "#id"),
@CacheEvict(cacheNames = "itemLists", allEntries = true)})
public void delete(Long id) {
repository.deleteById(id);
}
// 一括取り込みのあとなどに全消去する
@CacheEvict(cacheNames = {"items", "itemLists"}, allEntries = true)
public void clearAll() {
}
}
@CacheConfig("items") でクラス共通のキャッシュ名を決めているので、find・save・delete では名前を省いています。一覧だけ別のキャッシュ(itemLists)にしているのは、1件の更新で一覧全体を作り直せるようにするためです。
コピペ用一式(Redis版)
複数のインスタンスでキャッシュを共有したいときは Redis を使います。サービスのコードは Caffeine 版と同じで、変わるのは依存関係と設定だけです。
依存関係
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
dependencies {
implementation("org.springframework.boot:spring-boot-starter-cache")
implementation("org.springframework.boot:spring-boot-starter-data-redis")
}
spring-boot-starter-data-redis の既定のクライアントは Lettuce で、接続先の既定は localhost:6379 です。
application.yml
spring:
data:
redis:
host: "localhost"
port: 6379
cache:
cache-names: "items,itemLists"
redis:
time-to-live: "10m"
cache-null-values: false
キャッシュ名ごとにTTLを変える
spring.cache.redis.time-to-live は全キャッシュ共通の既定値です。キャッシュごとに変えるときは RedisCacheManagerBuilderCustomizer を Bean にします。
package com.example.demo.item;
import java.time.Duration;
import org.springframework.boot.cache.autoconfigure.RedisCacheManagerBuilderCustomizer;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
@Configuration(proxyBeanMethods = false)
@EnableCaching
public class CacheConfig {
@Bean
public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer() {
return (builder) -> builder
.withCacheConfiguration("items", RedisCacheConfiguration
.defaultCacheConfig().entryTtl(Duration.ofMinutes(30)))
.withCacheConfiguration("itemLists", RedisCacheConfiguration
.defaultCacheConfig().entryTtl(Duration.ofSeconds(60)));
}
}
Redisの既定の動き
Spring Data Redis の RedisCacheConfiguration の既定値です。
| 項目 | 既定 |
|---|---|
| 有効期限 | なし |
| null の保存 | する |
| キーの接頭辞 | 付ける(キャッシュ名) |
| キーのシリアライザー | StringRedisSerializer |
| 値のシリアライザー | JdkSerializationRedisSerializer |
- 値は JDK のシリアライズで保存されるので、キャッシュする型は
Serializableを実装します(上のItemはそのためにimplements Serializableを付けています)。JSON などで保存したい場合は、RedisCacheConfigurationのserializeValuesWithでシリアライザーを差し替えられます。既定の設定をまるごと置き換えるには、自分のRedisCacheConfigurationを Bean として定義します。 - キーの接頭辞は、別々のキャッシュが同じキーを使っても Redis 上で重ならないようにするためのものです。公式リファレンスは、この設定を有効のままにしておくことを強く勧めています。
- 最後のアクセスから数えて期限を延ばしたい(TTI)ときは
enableTimeToIdle()を使います。Redis 6.2.0 以上が必要です。
プロバイダーの自動検出順と設定プロパティ
CacheManager や CacheResolver の Bean を自分で定義していない場合、Spring Boot は次の順でプロバイダーを探し、最初に見つかったものを使います。
| 順 | 種類 | 選ばれる条件 | spring.cache.type |
|---|---|---|---|
| 1 | Generic | Cache 型の Bean が1つ以上ある |
generic |
| 2 | JCache(JSR-107) | CachingProvider がクラスパスにある |
jcache |
| 3 | Hazelcast | HazelcastInstance が自動構成され hazelcast-spring がある |
hazelcast |
| 4 | Infinispan | Infinispan がある | infinispan |
| 5 | Couchbase | Spring Data Couchbase があり設定済み | couchbase |
| 6 | Redis | Redis があり設定済み | redis |
| 7 | Caffeine | Caffeine がある | caffeine |
| 8 | Cache2k | Cache2k の Spring 連携がある | cache2k |
| 9 | Simple | ほかに何も無い | simple |
Redis は Caffeine より先に検出されます。 両方がクラスパスにあって Caffeine を使いたいときは、spring.cache.type=caffeine で明示します。キャッシュを使わない環境では spring.cache.type=none にします。
spring.cache.* の一覧(4.1.1)
| プロパティ | 既定 | 内容 |
|---|---|---|
spring.cache.type |
自動検出 | 使うプロバイダーを明示する |
spring.cache.cache-names |
空 | 起動時に作るキャッシュ名(カンマ区切り) |
spring.cache.caffeine.spec |
なし | Caffeine の spec 文字列 |
spring.cache.redis.time-to-live |
なし | エントリの有効期限 |
spring.cache.redis.cache-null-values |
true |
null を保存するか |
spring.cache.redis.key-prefix |
なし | キーの接頭辞 |
spring.cache.redis.use-key-prefix |
true |
接頭辞を付けるか |
spring.cache.redis.enable-statistics |
false |
統計を有効にするか |
spring.cache.couchbase.expiration |
なし | エントリの有効期限 |
spring.cache.infinispan.config |
なし | 設定ファイルの場所 |
spring.cache.jcache.config |
なし | 設定ファイルの場所 |
spring.cache.jcache.provider |
なし | CachingProvider の完全修飾名 |
cache-names の扱いはプロバイダーで少し違います。Simple では、cache-names を書くとそこに無い名前のキャッシュは使えなくなり、必要になった時点(起動時ではなく実行時)でエラーになります。書かなければ、必要に応じて作られます。
細かく調整するためのカスタマイザー
| 対象 | Bean の型 |
|---|---|
自動構成された CacheManager 全般 |
CacheManagerCustomizer<T> |
| Redis | RedisCacheManagerBuilderCustomizer |
| Couchbase | CouchbaseCacheManagerBuilderCustomizer |
| Cache2k | Cache2kBuilderCustomizer |
いずれも org.springframework.boot.cache.autoconfigure パッケージにあります。たとえば Simple で null を保存しないようにするには、次のように書きます。
package com.example.demo;
import org.springframework.boot.cache.autoconfigure.CacheManagerCustomizer;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class SimpleCacheCustomizerConfig {
@Bean
public CacheManagerCustomizer<ConcurrentMapCacheManager> cacheManagerCustomizer() {
return (cacheManager) -> cacheManager.setAllowNullValues(false);
}
}
キャッシュが効かないときの原因早見表
| 症状 | 原因 | 直し方 |
|---|---|---|
| 毎回メソッドが実行される | 同じクラス内から呼んでいる | 別の Bean に分ける、またはプロキシ経由で呼ぶ |
| 毎回メソッドが実行される | @EnableCaching が無い |
設定クラスに付ける |
| 毎回メソッドが実行される | メソッドが public ではない | public にする(エラーは出ず設定が無視される) |
| 毎回メソッドが実行される | spring.cache.type=none になっている |
対象の環境の設定を確認する |
| 毎回メソッドが実行される | キーの型に equals/hashCode が無い |
record にするか実装する |
| 同じデータなのにヒットしない | 既定のキーが全引数の組になっている | key でキーにする引数を絞る |
key = "#id" が効かない |
引数名の情報が無い | -parameters を付けるか #a0 を使う |
| 更新したのに前の値が返る | 更新メソッドにキャッシュ操作が無い | @CachePut か @CacheEvict を付ける |
| 更新したのに前の値が返る | 読み取りと更新でキーが違う | 両方の key をそろえる |
| null が返り続ける | null が保存されている | unless = "#result == null" を付ける |
| 起動時の処理で効かない | @PostConstruct から呼んでいる |
初期化後に呼ぶ(プロキシの準備が終わっていない) |
| キャッシュ名のエラーになる | cache-names に無い名前を使った |
cache-names に追加する |
| Redis で保存時にエラーになる | 型が Serializable ではない |
Serializable を実装するかシリアライザーを替える |
アノテーションは、インターフェースではなく具象クラス(とそのメソッド)に付けることを公式は勧めています。
同じクラス内の呼び出しで効かない理由をデモで確かめる
既定の proxy モードでは、キャッシュの処理はBean を包むプロキシが行います。外からの呼び出しはプロキシを通りますが、同じクラスの中から this.find(…) と呼ぶとプロキシを通らず、本体が直接実行されます。
この動きは Spring が無くても再現できます。ConcurrentHashMap と JDK の動的プロキシで作った小さなデモです(@Cached・@Put・@Evict は、それぞれ @Cacheable・@CachePut・@CacheEvict に当たる自作の目印です)。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class SelfInvocationDemo {
// @Cacheable / @CachePut / @CacheEvict に当たる目印
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @interface Cached {}
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @interface Put {}
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @interface Evict {}
interface ItemService {
@Cached String find(int id);
String findViaThis(int id);
@Put String rename(int id, String name);
@Evict void delete(int id);
void setSelf(ItemService self);
String findViaSelf(int id);
}
static class ItemServiceImpl implements ItemService {
private final Map<Integer, String> table = new ConcurrentHashMap<>(Map.of(1, "ノート", 2, "ペン"));
private ItemService self = this;
int loads = 0;
public String find(int id) {
loads++;
System.out.println(" [本体] find(" + id + ") を実行(" + loads + "回目)");
return table.get(id);
}
public String findViaThis(int id) { return this.find(id); } // 同じクラス内の呼び出し
public String findViaSelf(int id) { return self.find(id); } // プロキシ経由の呼び出し
public String rename(int id, String name) { table.put(id, name); return name; }
public void delete(int id) { table.remove(id); }
public void setSelf(ItemService self) { this.self = self; }
}
static final Object NULL = "(nullの目印)"; // ConcurrentHashMap は null を入れられないので目印に置き換える
// キャッシュ用プロキシ:外から来た呼び出しだけがここを通る
static ItemService withCache(ItemService target, Map<Object, Object> cache) {
return (ItemService) Proxy.newProxyInstance(
ItemService.class.getClassLoader(), new Class<?>[] {ItemService.class},
(proxy, method, args) -> {
Object key = args == null ? "EMPTY" : args[0];
if (method.isAnnotationPresent(Cached.class)) {
if (cache.containsKey(key)) {
System.out.println(" [プロキシ] キー " + key + " はキャッシュから返す");
Object hit = cache.get(key);
return hit == NULL ? null : hit;
}
Object value = method.invoke(target, args);
cache.put(key, value == null ? NULL : value);
return value;
}
Object value = method.invoke(target, args);
if (method.isAnnotationPresent(Put.class)) cache.put(key, value); // 実行して上書き
if (method.isAnnotationPresent(Evict.class)) cache.remove(key); // 実行して削除
return value;
});
}
public static void main(String[] args) {
Map<Object, Object> cache = new ConcurrentHashMap<>();
ItemServiceImpl impl = new ItemServiceImpl();
ItemService service = withCache(impl, cache);
System.out.println("1) 外から find(1) を2回");
System.out.println(" -> " + service.find(1));
System.out.println(" -> " + service.find(1));
System.out.println("2) 同じクラス内(this.find)で find(2) を2回");
System.out.println(" -> " + service.findViaThis(2));
System.out.println(" -> " + service.findViaThis(2));
System.out.println("3) 自分自身のプロキシを持たせて find(2) を2回");
service.setSelf(service);
System.out.println(" -> " + service.findViaSelf(2));
System.out.println(" -> " + service.findViaSelf(2));
System.out.println("4) rename(1)(Put:実行して上書き)のあとに find(1)");
service.rename(1, "方眼ノート");
System.out.println(" -> " + service.find(1));
System.out.println("5) delete(2)(Evict:実行して削除)のあとに find(2)");
service.delete(2);
System.out.println(" -> " + service.find(2));
System.out.println(" -> " + service.find(2));
System.out.println("本体の find が動いた回数: " + impl.loads + " / キャッシュの中身: " + cache);
}
}
OpenJDK 21.0.10 での実行結果です。
$ java SelfInvocationDemo.java
1) 外から find(1) を2回
[本体] find(1) を実行(1回目)
-> ノート
[プロキシ] キー 1 はキャッシュから返す
-> ノート
2) 同じクラス内(this.find)で find(2) を2回
[本体] find(2) を実行(2回目)
-> ペン
[本体] find(2) を実行(3回目)
-> ペン
3) 自分自身のプロキシを持たせて find(2) を2回
[本体] find(2) を実行(4回目)
-> ペン
[プロキシ] キー 2 はキャッシュから返す
-> ペン
4) rename(1)(Put:実行して上書き)のあとに find(1)
[プロキシ] キー 1 はキャッシュから返す
-> 方眼ノート
5) delete(2)(Evict:実行して削除)のあとに find(2)
[本体] find(2) を実行(5回目)
-> null
[プロキシ] キー 2 はキャッシュから返す
-> null
本体の find が動いた回数: 5 / キャッシュの中身: {1=方眼ノート, 2=(nullの目印)}
読み取れることは4つです。
- 外からの呼び出しは、2回目がプロキシでキャッシュから返り、本体は1回しか動いていません。
this.find(2)での呼び出しは、2回とも本体が動いています。プロキシを通っていないためです。- プロキシ経由(
self.find(2))に変えると、2回目がキャッシュから返ります。 - Put は実行して上書きするので次の読み取りがヒットし、Evict は実行して削除するので次の読み取りで本体が動きます。削除後の
find(2)は null を返し、その null も保存されて2回目はキャッシュから返っています。
4つ目の後半が「null が返り続ける」の正体です。ConcurrentHashMap は null を値にできないため、デモでは目印のオブジェクトに置き換えて保存しています。Spring のキャッシュでも、Simple や Redis は既定で null を保存します。
同じクラス内の呼び出しを直す
| 方法 | 内容 | 向く場面 |
|---|---|---|
| Bean を分ける | キャッシュするメソッドを別のクラスに移す | 基本の方法 |
| プロキシ経由で呼ぶ | 自分自身の Bean を注入して呼ぶ | クラスを分けにくいとき |
aspectj モード |
@EnableCaching(mode = AdviceMode.ASPECTJ) でクラス自体を織り込む |
public 以外のメソッドにも付けたいとき |
Bean を分ける形は次のようになります。
package com.example.demo.item;
import org.springframework.stereotype.Service;
@Service
public class ItemReportService {
private final ItemService itemService; // キャッシュ付きのメソッドを持つ別の Bean
public ItemReportService(ItemService itemService) {
this.itemService = itemService;
}
public String label(Long id) {
Item item = itemService.find(id); // プロキシを通るのでキャッシュが使われる
return item == null ? "(なし)" : item.name() + " x" + item.stock();
}
}
aspectj モードを使うには、クラスパスに spring-aspects.jar があり、ロード時またはコンパイル時のウィービングが有効になっている必要があります。
キーが一致しない例をデモで確かめる
キャッシュは「キーが等しいか」で引き当てます。ConcurrentHashMap で同じことを確かめます。
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class KeyDemo {
// equals / hashCode を実装していないクラス
static class PlainCriteria {
final String word;
PlainCriteria(String word) { this.word = word; }
}
// record は equals / hashCode が自動で用意される
record RecordCriteria(String word) {}
public static void main(String[] args) {
Map<Object, String> cache = new ConcurrentHashMap<>();
cache.put(new PlainCriteria("ノート"), "結果A");
System.out.println("equals なしのクラス : " + cache.get(new PlainCriteria("ノート")));
cache.put(new RecordCriteria("ノート"), "結果B");
System.out.println("record : " + cache.get(new RecordCriteria("ノート")));
// 引数が複数あるときは「全引数の組」がキーになる
cache.put(List.of(1, true), "結果C");
System.out.println("引数 (1, true) : " + cache.get(List.of(1, true)));
System.out.println("引数 (1, false) : " + cache.get(List.of(1, false)));
}
}
$ java KeyDemo.java
equals なしのクラス : null
record : 結果B
引数 (1, true) : 結果C
引数 (1, false) : null
equals/hashCode の無いクラスをキーにすると、中身が同じでも別のキーとして扱われます。また、引数が複数あるメソッドでは全引数の組がキーになるので、結果に影響しない引数(フラグなど)が違うだけでヒットしなくなります。 そのときは key = "#id" のようにキーにする引数を絞ります。
テスト時にキャッシュを無効にする
キャッシュが有効なままだと、前のテストで保存された値が次のテストに影響することがあります。自動構成のキャッシュを使っている場合、方法は2つです。
方法1:テスト用の設定で none にする
src/test/resources/application.yml:
spring:
cache:
type: "none"
方法2:@AutoConfigureCache を付ける
package com.example.demo.item;
import org.springframework.boot.cache.test.autoconfigure.AutoConfigureCache;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
@AutoConfigureCache
class ItemServiceTests {
// 自動構成の CacheManager が何もしない実装に置き換わる
}
@AutoConfigureCache は spring-boot-starter-cache-test に含まれ、既定で NoOpCacheManager を構成します。cacheProvider 属性(spring.cache.type に対応)で、テスト用に別のプロバイダーを指定することもできます。
| 状況 | 方法 |
|---|---|
| 自動構成のキャッシュを使っている | spring.cache.type=none または @AutoConfigureCache |
CacheManager を自分で定義している |
キャッシュの設定を独立した @Configuration に分け、テストで上書きする |
スライステスト(@WebMvcTest など) |
キャッシュの設定を独立したクラスにしておけば読み込まれない |
CacheManager を自分で定義している場合、@AutoConfigureCache は効きません。@EnableCaching を専用の設定クラスに分けておくことが、ここでも役に立ちます。
よくある質問
Q. @Cacheable を付けたのに毎回 SQL が実行されます。最初に見るところは?
A. 呼び出し元が同じクラスの中かどうかを確認します。既定の proxy モードでは、同じクラス内からの呼び出しはプロキシを通らないのでキャッシュされません。次に @EnableCaching の有無、メソッドが public かどうかを見ます。
Q. @CachePut と @CacheEvict の違いは何ですか?
A. どちらもメソッドを必ず実行します。@CachePut は戻り値でキャッシュを上書きし、@CacheEvict はエントリを削除します。更新後の値を返すメソッドには @CachePut、void のメソッドや一覧に影響する変更には @CacheEvict が合います。
Q. 有効期限(TTL)はアノテーションで指定できますか?
A. アノテーションの属性に有効期限はありません。Caffeine は spring.cache.caffeine.spec の expireAfterWrite/expireAfterAccess、Redis は spring.cache.redis.time-to-live や RedisCacheManagerBuilderCustomizer の entryTtl のように、プロバイダー側の設定で指定します。
Q. Caffeine と Redis はどう選べばよいですか? A. 1つのインスタンスの中で完結してよいなら Caffeine、複数のインスタンスで同じキャッシュを共有したいなら Redis が合います。どちらを選んでもアノテーションを付けたコードは変わらないので、あとから切り替えられます。
まとめ
- 3つで動く:
spring-boot-starter-cache、設定クラスの@EnableCaching、メソッドの@Cacheable。 @Cacheableは実行を省き、@CachePutは実行して上書き、@CacheEvictは実行して削除する。conditionは実行前に引数で、unlessは実行後に#resultで判断する。syncはunlessと併用できない。- 既定のキーは全引数の組。キーにする引数を絞るときは
key、引数名が使えないときは#a0/#p0。 - プロバイダーは9種を順に検出し、Redis は Caffeine より先。明示するなら
spring.cache.type。 - 同じクラス内の呼び出しはプロキシを通らない。Bean を分けるのが基本。
- テストでは
spring.cache.type=noneか@AutoConfigureCacheで無効にできる。
参考・出典
確認日はいずれも 2026年10月7日です。
- Spring Boot リファレンス Caching(4.1.1): https://docs.spring.io/spring-boot/reference/io/caching.html
- Spring Framework リファレンス Declarative Annotation-based Caching(7.0.9): https://docs.spring.io/spring-framework/reference/integration/cache/annotations.html
- Cacheable API: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/cache/annotation/Cacheable.html
- CachePut API: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/cache/annotation/CachePut.html
- CacheEvict API: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/cache/annotation/CacheEvict.html
- Caching API: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/cache/annotation/Caching.html
- CacheConfig API: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/cache/annotation/CacheConfig.html
- AutoConfigureCache API(4.1.1): https://docs.spring.io/spring-boot/4.1.1/api/java/org/springframework/boot/cache/test/autoconfigure/AutoConfigureCache.html
- CacheProperties のソース(v4.1.1): https://raw.githubusercontent.com/spring-projects/spring-boot/v4.1.1/module/spring-boot-cache/src/main/java/org/springframework/boot/cache/autoconfigure/CacheProperties.java
- Spring Data Redis リファレンス Redis Cache(4.1.1): https://docs.spring.io/spring-data/redis/reference/redis/redis-cache.html
- Spring Boot リファレンス NoSQL(Redis、4.1.1): https://docs.spring.io/spring-boot/reference/data/nosql.html
- Caffeine Wiki Specification: https://github.com/ben-manes/caffeine/wiki/Specification
- Spring Boot 4.0 Migration Guide(スターター一覧): https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Migration-Guide
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Spring Boot キャッシュ|@Cacheableの属性一覧と設定例」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。