最近、SpringのCache機能とライブラリ側で提供しているCacheManagerの実装を見ていて、ふと気付いたことについて。
SpringのCache機能は、CacheManagerの実装が用意されているか、JCacheに対応した製品であればSpringが提供するJCacheCacheManagerを使うことで、利用することができます。
Cache Abstraction
http://docs.spring.io/spring/docs/current/spring-framework-reference/html/cache.html
で、このうちHazelcastは、Hazelcast自身が提供するHazelcastCacheManagerを使うのか、JCache越しに使うのかで意識するデータ構造が変わるので、メモ的に。
結論を先に書くと、
- HazelcastCacheManagerを使う場合は、Distributed MapがCacheとして利用される
- JCacheとして使う場合は、ICache(?)がCacheとして利用される
ということになります。
このあたりをちょっと見ていってみます。
準備
動作は、Spring Boot(Spring MVC)でSpringのCache機能を使うことにしましょう。今回は、Mavenの設定を以下の様にしました。
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.littlewings</groupId> <artifactId>spring-cache-hazelcast</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <java.version>1.8</java.version> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <spring.boot.version>1.2.5.RELEASE</spring.boot.version> </properties> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>${spring.boot.version}</version> </plugin> </plugins> </build> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring.boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 残りの依存関係は、あとで --> </dependencies> </project>
あとは、利用するCacheManagerの種類に応じて、依存関係を足していきます。
また、共通で利用するクラスを以下のように実装。
まずはアプリケーションのエントリポイント。
src/main/java/org/littlewings/spring/App.java
package org.littlewings.spring; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Import; @SpringBootApplication @Import(Config.class) public class App { public static void main(String... args) { SpringApplication.run(App.class, args); } }
@importしているConfigの中身は、利用するCacheManagerに応じて変えていきます。
RestController。
src/main/java/org/littlewings/spring/CalcController.java
package org.littlewings.spring; import java.util.concurrent.TimeUnit; import org.springframework.cache.annotation.Cacheable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("calc") public class CalcController { @RequestMapping("add") @Cacheable("expiryCache") public int add(@RequestParam int a, @RequestParam int b) { try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { // ignore } return a + b; } }
Cacheの名前に「expiryCache」を指定した@Cacheableアノテーション付きで、3秒間スリープするようにしています。
Hazelcastの設定は、XMLファイルで行うことにします。雛形を以下の様に書いておきました。
src/main/resources/hazelcast.xml
<?xml version="1.0" encoding="UTF-8"?> <hazelcast xsi:schemaLocation="http://www.hazelcast.com/schema/config hazelcast-config-3.5.xsd" xmlns="http://www.hazelcast.com/schema/config" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <group> <name>my-cluster</name> <password>my-cluster-password</password> </group> <!-- あとで --> </hazelcast>
ここからは、これらのコードの未完成の部分を選択したCacheManagerに応じて埋めていきます。
HazelcastCacheManagerを使う場合
Hazelcastを使ってSpringのCache機能を利用する際に読むべきドキュメントは、こちら。
Spring Cache
http://docs.hazelcast.org/docs/3.5/manual/html-single/hazelcast-documentation.html#spring-cache
が、これよくよく見ると、Cacheの保存先にどのデータ構造を利用するかって書いてないですね…。
HazelcastCacheManagerを使う場合、Maven依存関係には以下を追加します。
<dependency> <groupId>com.hazelcast</groupId> <artifactId>hazelcast-spring</artifactId> <version>3.5</version> </dependency>
この場合、JavaConfigとなるConfigクラスは、以下の様に定義しました。
src/main/java/org/littlewings/spring/Config.java
package org.littlewings.spring; import com.hazelcast.core.Hazelcast; import com.hazelcast.core.HazelcastInstance; import com.hazelcast.spring.cache.HazelcastCacheManager; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; @EnableCaching public class Config { @Bean(destroyMethod = "shutdown") public HazelcastInstance hazelcast() { return Hazelcast.newHazelcastInstance(); } @Bean public CacheManager hazelcastCacheManager() { return new HazelcastCacheManager(hazelcast()); } }
なんとなく、HazelcastInstanceもBeanとして定義しています。
あとは、Cacheとして使用されるDistributed Mapの定義です。このようにしました。
<map name="expiryCache"> <time-to-live-seconds>10</time-to-live-seconds> </map>
TTLを10秒にして、Cacheへのエントリ保存後、10秒経過すると有効期限切れするようにしています。
ちなみに、設定ファイルが「hazelcast.xml」の場合は(HazelcastがServer Modeであれば)、勝手に設定ファイルを読み込んでくれます。
※これ以外のファイル名の場合でも、明示的に指定することはできます
では、起動して確認。
## 1回目 $ time curl 'http://localhost:8080/calc/add?a=5&b=3' 8 real 0m3.120s user 0m0.020s sys 0m0.087s ## 2回目 $ time curl 'http://localhost:8080/calc/add?a=5&b=3' 8 real 0m0.016s user 0m0.000s sys 0m0.007s
初回は3秒かかっていますが、2回目からは高速になっています。
10秒後には有効期限切れしてしまうので、また遅くなります。
$ time curl 'http://localhost:8080/calc/add?a=5&b=3' 8 real 0m3.019s user 0m0.004s sys 0m0.004s
OKそうですね。
JCacheCacheManagerを使う場合
HazelcastはJCacheの実装でもあるので、SpringのCache機能が提供するJCacheCacheManager越しにも使うことができます。
Hazelcast JCache
http://docs.hazelcast.org/docs/3.5/manual/html-single/hazelcast-documentation.html#hazelcast-jcache
JCacheCacheManagerを使うには、spring-context-supportが必要なようなので、先ほどのhazelcast-springへの依存関係を削除して、以下のようにします。
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> </dependency> <dependency> <groupId>javax.cache</groupId> <artifactId>cache-api</artifactId> <version>1.0.0</version> </dependency> <dependency> <groupId>com.hazelcast</groupId> <artifactId>hazelcast</artifactId> <version>3.5</version> </dependency>
Configクラスは、以下のように変更します。
import javax.cache.Caching; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.jcache.JCacheCacheManager; import org.springframework.context.annotation.Bean; @EnableCaching public class Config { @Bean public CacheManager jcacheCacheManager() { return new JCacheCacheManager(Caching.getCachingProvider().getCacheManager()); } }
が、このまま起動するとFreeMarkerから「templates」ディレクトリがないと怒られるので、
Caused by: java.lang.IllegalArgumentException: Cannot find template location(s): [classpath:/templates/] (please add some templates, check your FreeMarker configuration, or set spring.freemarker.checkTemplateLocation=false)
今回はFreeMarkerAutoConfigurationをオフにすることにしました(spring.freemarker.checkTemplateLocation=falseは、今回はパス)。
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration; import org.springframework.context.annotation.Import; @SpringBootApplication @Import(Config.class) @EnableAutoConfiguration(exclude = FreeMarkerAutoConfiguration.class) public class App {
※エントリポイントのクラスです
で、このままHazelcastの設定を変えずにアプリケーションを起動してアクセスすると
$ time curl 'http://localhost:8080/calc/add?a=5&b=3' {"timestamp":1437659848051,"status":500,"error":"Internal Server Error","exception":"java.lang.IllegalArgumentException","message":"Cannot find cache named 'expiryCache' for CacheableOperation[public int org.littlewings.spring.CalcController.add(int,int)] caches=[expiryCache] | key='' | keyGenerator='' | cacheManager='' | cacheResolver='' | condition='' | unless=''","path":"/calc/add"}
「expiryCache」がないと怒られます…。
先ほどのDistributed Mapの設定は見ていないということですね。
なので、HazelcastのJCache向けの設定にhazelcast.xmlを修正します。
<cache name="expiryCache"> <key-type class-name="java.lang.Object"/> <value-type class-name="java.lang.Object"/> <expiry-policy-factory class-name="org.littlewings.spring.MyExpiryFactory"/> </cache>
設定ファイルでexpireを決める場合は、Expiry用のクラスを実装する必要があります。
JCache Configuration
http://docs.hazelcast.org/docs/3.5/manual/html-single/hazelcast-documentation.html#jcache-configuration
TTLを10秒にする場合のExpiryの設定は、こちら。
src/main/java/org/littlewings/spring/MyExpiryFactory.java
package org.littlewings.spring; import java.util.concurrent.TimeUnit; import javax.cache.configuration.Factory; import javax.cache.expiry.CreatedExpiryPolicy; import javax.cache.expiry.Duration; import javax.cache.expiry.ExpiryPolicy; public class MyExpiryFactory implements Factory<ExpiryPolicy> { @Override public ExpiryPolicy create() { return new CreatedExpiryPolicy(new Duration(TimeUnit.SECONDS, 10)); } }
まあ、ここまでしなくても、JCacheのConfigurationで定義してもいいのですが…まあHazelcastをいろいろ使う場合は、設定ファイルを書くのかなと思いまして。
なお、この実装でも暗黙的に読み込むファイル名は、「hazelcast.xml」で変わらないようです。
Caching.getCachingProvider().getCacheManager();
※JCacheとして使う場合でも、「hazelcast.xml」以外の名前のファイルを読み込ませることは可能です
ここまですると、先ほどのHazelcastCacheManagerの時と同じ動作をさせることができるようになります。
$ time curl 'http://localhost:8080/calc/add?a=5&b=3' 8 real 0m3.260s user 0m0.008s sys 0m0.000s $ time curl 'http://localhost:8080/calc/add?a=5&b=3' 8 real 0m0.020s user 0m0.007s sys 0m0.000s
10秒後に、Cacheエントリの有効期限が切れるところも同じです。
なのですが、この実装ではHazelcastInstanceが見えないため、Hazelcast自身の機能はあまり使うことができません。
@Bean public CacheManager jcacheCacheManager() { return new JCacheCacheManager(Caching.getCachingProvider().getCacheManager()); }
Hazelcastの機能も使いつつ、JCacheとしても使いたい場合は以下のような実装になるでしょう。
@Bean(destroyMethod = "shutdown") public HazelcastInstance hazelcast() { return Hazelcast.newHazelcastInstance(); } @Bean public CacheManager jcacheCacheManager() { return new JCacheCacheManager( HazelcastServerCachingProvider .createCachingProvider(hazelcast()) .getCacheManager()); }
こうすると、HazelcastInstanceも直接使え、かつJCacheとしても扱えるようになります。
なのですが、この実装方法だとHazelcastのCachingProviderがjavax.cache.Cachingの管理外になってしまうため、直接javax.cache.CachingからCachingProvider/CacheManagerを使うコードが存在すると、問題になるかもしれません。
まとめ
今回は、SpringのCache機能の実装としてHazelcastを使う場合を、HazelcastCacheManagerとHazelcastをJCacheとして使う場合の両方を紹介しました。
HazelcastのJCacheの機能は、Hazelcast 3.3.1から搭載されたまだ新しめの機能であり、以前から存在していたMap、MultiMap、Listなどといったデータ構造とは独立して導入されています(ICache)。
なので、JCache搭載前から知っているとHazelcastCacheManagerの中身はDistributed Mapな予想が立つのですが、JCache搭載後に見るとJCacheと同じものを見るような解釈をする人がいるのでは…と思ったり。なにせ、HazelcastCacheManagerの方にはどのデータ構造を使うか、ドキュメントに記載がないですからね…。
で、どっちを使うの?という話ですが、SpringのCacheとして使うだけなら、どちらでも…と。Hazelcast特有の機能も使うのであれば、素直にHazelcastが提供するHazelcastCacheManagerを使った方がよいのでは?という気がします。HazelcastInstanceを共有したくなると思いますので。
なんとなく、ライブラリ側でSpring Cache機能のCacheManagerの実装、JCacheの実装の両方を提供するケースではライブラリ側が提供するCacheManagerの実装を、JCacheのみの実装がある場合はJCacheの実装を使う、という分けでいいような印象を持ちました。