便利だけれどあまり知られていない、Springがもつキャッシュ抽象化機構について説明します。
Springのキャッシュ機能では
org.springframework.cache.CacheManager
によるキャッシュ製品共通API- AOP(
@Cacheable
)による透過的キャッシュ
がサポートされています。
CacheManagerの使い方
org.springframework.cache.CacheManager
インターフェースを通じて、様々なキャッシュ(ConcurrentHashMap
や EhCacheなどなど)を同じAPIでアクセスすることができます。
使い方は
// キャッシュ取得
Cache cache = cacheManager.getCache("foo");
// キャシュにキー=値を追加
cache.put("hoge", "test");
// キャシュから値を取得
System.out.println(cache.get("hoge", String.class)); // => test
// キャシュから値を削除
cache.evict("hoge");
System.out.println(cache.get("hoge", String.class)); // => null
// キャシュをクリア
cache.clear();
このAPIを使う事で製品ごとのAPIを使う必要がなくなるので、製品を入れ替えが容易になります。
例えば開発・テスト中はConcurrentHashMap
を使い、本番はキャッシュ製品を使うというような使い方が可能です。
Spring Bootから使う
Spring Bootで動くプログラム全文を載せておきます。
package demo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.concurrent.ConcurrentMapCache;
import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.context.annotation.Bean;
import java.util.Arrays;
@SpringBootApplication
public class DemoApplication implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
// Bean定義
@Bean // CacheManagerをBean定義する必要がある
CacheManager cacheManager() {
// org.springframework.cache.Cacheの実装をマニュアルで登録するCacheManagerクラス
SimpleCacheManager cacheManager = new SimpleCacheManager();
// ConcurrentHashMapを使ったorg.springframework.cache.Cacheの実装を登録する
cacheManager.setCaches(Arrays.asList(new ConcurrentMapCache("foo")));
return cacheManager;
}
// ここから主プログラム
@Autowired
CacheManager cacheManager;
@Override
public void run(String... strings) throws Exception {
Cache cache = cacheManager.getCache("foo");
cache.put("hoge", "test");
System.out.println(cache.get("hoge", String.class));
cache.evict("hoge");
System.out.println(cache.get("hoge", String.class));
}
}
様々なCacheManager実装
先の例では、単純なSimpleCacheManager
を紹介しましたが、これ以外にもたくさんのCacheManager
実装があります。
対応製品(Springまたは製品側にCacheManagerの実装があるもの)は以下の通りです。
- ConcurrentMap
- EhCache
- Guava
- JCache
- Redis
- Gemfire, Apache Geode
- Hazelcast
- Apache Ignite
- Infinispan
- ElastiCache (Memcached) (
Cache
の実装のみ) - WebSphere eXtreme Scale (
Cache
の実装のみ)
JCacheManager
はjavax.cache.CacheManager
のアダプターです。したがってJCache対応製品(Oracle Coherenceなど)はこのアダプターを通じてSpringのキャッシュ機構から利用可能です。
そのほか、CompositeCacheManagerは複数のCacheManager
を混合することができますし、AbstractTransactionSupportingCacheManager実装クラスは@Transactional
で(対応していれば)トランザクション管理が可能です。
クラス図貼っておきます。
CacheManger
Cache
AOPによる透過的キャッシュ
AOPを使うことで、先の例のように明示的にCache
を使うことなくキャッシュを利用することができます。
@Cacheable
アノテーションをつけたメソッドの返り値を自動でCache
に登録できます。
まずはキャッシュを使わない例を紹介します。外部Webサービス(Open Weather Map API)にアクセスするプログラムです。
package demo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.time.LocalDateTime;
import java.util.Map;
@SpringBootApplication
public class DemoApplication implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
// Bean定義
@Bean
RestTemplate restTemplate() {
return new RestTemplate();
}
// ここから主プログラム
@Autowired
WeatherService weatherService;
@Override
public void run(String... strings) throws Exception {
perfMon(() -> System.out.println(weatherService.getWeather("Tokyo")));
perfMon(() -> System.out.println(weatherService.getWeather("Osaka")));
perfMon(() -> System.out.println(weatherService.getWeather("Tokyo")));
}
void perfMon(Runnable runnable) {
long start = System.currentTimeMillis();
runnable.run();
long elapsed = System.currentTimeMillis() - start;
System.out.println("took " + elapsed + " [ms]");
}
}
@Service
class WeatherService {
@Autowired
RestTemplate restTemplate;
@SuppressWarnings("unchecked")
public String getWeather(String where) {
Map<String, Object> result = (Map<String, Object>) restTemplate.getForObject("http://api.openweathermap.org/data/2.5/weather?q=" + where, Map.class);
Double temperature = (Double) ((Map<String, Object>) result.get("main")).get("temp") - 273;
Double wind = (Double) ((Map<String, Object>) result.get("wind")).get("speed") * 3.6;
return "The current temperature " + temperature + " degrees and the wind is " + wind + " km/h. (" + LocalDateTime.now() + ")";
}
}
実行すると、
The current temperature 16.12299999999999 degrees and the wind is 22.32 km/h. (2015-05-17T04:23:53.125)
took 374 [ms]
The current temperature 11.12299999999999 degrees and the wind is 7.02 km/h. (2015-05-17T04:23:53.209)
took 82 [ms]
The current temperature 16.12299999999999 degrees and the wind is 22.32 km/h. (2015-05-17T04:23:53.293)
took 84 [ms]
と出力されます。WebサービスへHTTPアクセスしているので遅いです。
天気の情報はそう頻繁に変わらないので、同じ場所の結果はキャッシュさせるようにしましょう。ここで@Cacheable
の登場です。
package demo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.concurrent.ConcurrentMapCache;
import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Map;
@SpringBootApplication
@EnableCaching // AOPによるキャッシュアクセスを有効にする
public class DemoApplication implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
// Bean定義
@Bean // 使用するCacheManagerの定義
CacheManager cacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
cacheManager.setCaches(Arrays.asList(new ConcurrentMapCache("weather")));
return cacheManager;
}
@Bean
RestTemplate restTemplate() {
return new RestTemplate();
}
// ここから主プログラム
@Autowired
WeatherService weatherService;
@Override
public void run(String... strings) throws Exception {
perfMon(() -> System.out.println(weatherService.getWeather("Tokyo")));
perfMon(() -> System.out.println(weatherService.getWeather("Osaka")));
perfMon(() -> System.out.println(weatherService.getWeather("Tokyo")));
}
void perfMon(Runnable runnable) {
long start = System.currentTimeMillis();
runnable.run();
long elapsed = System.currentTimeMillis() - start;
System.out.println("took " + elapsed + " [ms]");
}
}
@CacheConfig(cacheNames = "weather") // キャッシュ名を指定
@Service
class WeatherService {
@Autowired
RestTemplate restTemplate;
@Cacheable // キャッシュさせたいメソッドにつける
@SuppressWarnings("unchecked")
public String getWeather(String where) {
Map<String, Object> result = (Map<String, Object>) restTemplate.getForObject("http://api.openweathermap.org/data/2.5/weather?q=" + where, Map.class);
Double temperature = (Double) ((Map<String, Object>) result.get("main")).get("temp") - 273;
Double wind = (Double) ((Map<String, Object>) result.get("wind")).get("speed") * 3.6;
return "The current temperature " + temperature + " degrees and the wind is " + wind + " km/h. (" + LocalDateTime.now() + ")";
}
}
実行してみます。
The current temperature 16.12299999999999 degrees and the wind is 22.32 km/h. (2015-05-17T04:30:24.011)
took 344 [ms]
The current temperature 11.12299999999999 degrees and the wind is 7.02 km/h. (2015-05-17T04:30:24.346)
took 334 [ms]
The current temperature 16.12299999999999 degrees and the wind is 22.32 km/h. (2015-05-17T04:30:24.011)
took 1 [ms]
2回目のTokyoの結果が速くなっているのがわかります。また、1回目と同じ時刻になっているので、初回の結果がキャッシュされていることもわかります。
既存の処理を簡単に、透過的に高速化できることが実感できたのではないでしょうか。
ちなみに、@CacheConfig
を使わなくても@Cacheable("weather")
というように一つ一つのメソッド毎にキャッシュ名を指定することも可能です。
デフォルトではキャッシュのキーは引数のオブジェクトです。複数ある場合はjava.util.Arrays#deepHashCode
の結果が使用されます。キャッシュのキーはSpEL式を使って柔軟に表現することができます。詳細はマニュアルを参照してください。
キャッシュのサイズや生存期間を指定するAPIはSpringには用意されておらず、キャッシュ製品依存になります。
ここではConcurrentHash
並みに簡単に使えるGoogle Guavaのキャッシュ機構を利用した例を紹介します。
@Bean
CacheManager cacheManager() {
GuavaCacheManager cacheManager = new GuavaCacheManager("weather", "...");
cacheManager.setCacheBuilder(CacheBuilder.newBuilder()
.maximumSize(1000) // 最大1000件キャッシュ
.expireAfterAccess(1, TimeUnit.SECONDS) // 最後のアクセスから1秒後に破棄
.removalListener(e -> System.out.println("==> " + e.getKey() + " has been removed!")));
return cacheManager;
}
この設定を使い、APIにアクセスするプログラムを以下のように修正します。
@Override
public void run(String... strings) throws Exception {
perfMon(() -> System.out.println(weatherService.getWeather("Tokyo")));
perfMon(() -> System.out.println(weatherService.getWeather("Osaka")));
perfMon(() -> System.out.println(weatherService.getWeather("Tokyo")));
TimeUnit.SECONDS.sleep(1);
perfMon(() -> System.out.println(weatherService.getWeather("Tokyo")));
}
結果は以下のようになります。
The current temperature 16.12299999999999 degrees and the wind is 22.32 km/h. (2015-05-17T04:56:03.441)
took 492 [ms]
The current temperature 11.478000000000009 degrees and the wind is 4.716 km/h. (2015-05-17T04:56:03.608)
took 161 [ms]
The current temperature 16.12299999999999 degrees and the wind is 22.32 km/h. (2015-05-17T04:56:03.441)
took 1 [ms]
==> Tokyo has been removed!
The current temperature 16.12299999999999 degrees and the wind is 22.32 km/h. (2015-05-17T04:56:04.706)
took 91 [ms]
最後のアクセス時にはキャッシュがなくなっているので再度Web APIにアクセスしているのがわかります。
明示的なキャッシュの更新、破棄は@CachePut
、@CacheEvict
を使えます。
// import org.springframework.cache.annotation.*;
@CacheConfig(cacheNames = "weather")
@Service
class WeatherService {
@Autowired
RestTemplate restTemplate;
@Cacheable
@SuppressWarnings("unchecked")
public String getWeather(String where) {
Map<String, Object> result = (Map<String, Object>) restTemplate.getForObject("http://api.openweathermap.org/data/2.5/weather?q=" + where, Map.class);
Double temperature = (Double) ((Map<String, Object>) result.get("main")).get("temp") - 273;
Double wind = (Double) ((Map<String, Object>) result.get("wind")).get("speed") * 3.6;
return "The current temperature " + temperature + " degrees and the wind is " + wind + " km/h. (" + LocalDateTime.now() + ")";
}
@CachePut
public String update(String where) {
return getWeather(where);
}
@CacheEvict
public String refresh(String where) {
return getWeather(where);
}
@CacheEvict(allEntries = true)
public void clear() {
}
}
アクセス頻度の高いメソッドは積極的にこの機能の利用を検討してもよいでしょう。特に更新頻度が低い場合に有効です。
一件取得処理なんかはほとんど@Cachebale
つけてもよいと思います。
JCache(JSR-107)アノテーションのサポート
Springではorg.springframework.cache.annotation.Cacheable
の代わりに、Java EE 8から導入されるJCacheのアノテーション(@javax.cache.annotation.CacheResult
など)も使えます。Springではjavax.cache.CacheManager
の実装クラスは存在する必要はありません。
この機能を有効にするにはorg.springframework:spring-context-support
を依存関係に追加する必要があります。
先のWeatherService
の例をJCacheアノテーションを使うと以下のようになります。
// import javax.cache.annotation.*;
@CacheDefaults(cacheName = "weather")
@Service
class WeatherService {
@Autowired
RestTemplate restTemplate;
@CacheResult
@SuppressWarnings("unchecked")
public String getWeather(String where) {
Map<String, Object> result = (Map<String, Object>) restTemplate.getForObject("http://api.openweathermap.org/data/2.5/weather?q=" + where, Map.class);
Double temperature = (Double) ((Map<String, Object>) result.get("main")).get("temp") - 273;
Double wind = (Double) ((Map<String, Object>) result.get("wind")).get("speed") * 3.6;
return "The current temperature " + temperature + " degrees and the wind is " + wind + " km/h. (" + LocalDateTime.now() + ")";
}
// @CachePut
// // 引数から更新値を@CacheValueで指定する必要があるので先の例を表現できない
// public String update(String where) {
// return getWeather(where);
// }
@CacheRemove
public String refresh(String where) {
return getWeather(where);
}
@CacheRemoveAll
public void clear() {
}
}
JCacheアノテーションはSpringのものとほとんど同じように使えることがわかります。比較表はマニュアルに載っています。
@javax.cache.annotation.CachePut
と@org.springframework.cache.annotation.CachePut
の仕様に少し違いがあるのが注意です。
あとはSpringの方がキーの指定のSpELが使える分、柔軟かなと思います。
Springのキャッシュ機構、かなり便利なのでアプリケーションの高速化、DBアクセス負荷低減のために積極的に検討しましょう!
追記
@making cool. In @springboot 1.3 caching will be auto-configured!
— Stéphane Nicoll (@snicoll) 2015, 5月 17
Spring Boot 1.3からAutoConfiguration対応するらしい。