Spring Boot 1.3から、Cacheのauto-configurationが入ったということで。
Cache auto-configuration in Spring Boot 1.3
遊ぼう遊ぼうと思いつつ試せていなかったので、そろそろトライしてみることに。
サポートしているCacheのProviderは、こちら。
- Generic
- JCache
- Ehcache 2
- Hazelcast
- Infinispan
- Redis
- Guava
- Simple
サンプルもあるようです。
このうち、今回はJCache(実装はHazelcast)を使用してみます。
準備
<?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-boot-jcache-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.3.1.RELEASE</spring.boot.version> </properties> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>${spring.boot.version}</version> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </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> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <dependency> <groupId>javax.cache</groupId> <artifactId>cache-api</artifactId> </dependency> <dependency> <groupId>com.hazelcast</groupId> <artifactId>hazelcast</artifactId> </dependency> <dependency> <groupId>com.hazelcast</groupId> <artifactId>hazelcast-spring</artifactId> </dependency> </dependencies> </project>
Spring Bootからは、「spring-boot-starter-cache」を追加。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>
JCacheを使用するので、JCache APIに対する依存関係と
<dependency> <groupId>javax.cache</groupId> <artifactId>cache-api</artifactId> </dependency>
HazelcastとHazelcast Springをサンプルに習い追加。
<dependency> <groupId>com.hazelcast</groupId> <artifactId>hazelcast</artifactId> </dependency> <dependency> <groupId>com.hazelcast</groupId> <artifactId>hazelcast-spring</artifactId> </dependency>
ただ、hazelcast-springは要らない気がしますが??
Hazelcast/JCacheを使ってみる
オーソドックスに、Hazelcast ServerモードでJCacheを使ってみます。
コードの用意
まずはエントリポイントのクラス。
src/main/java/org/littlewings/spring/jcache/App.java
package org.littlewings.spring.jcache; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.annotation.EnableCaching; @SpringBootApplication @EnableCaching public class App { public static void main(String... args) { SpringApplication.run(App.class, args); } }
@EnableCachingアノテーションが付与してあることがポイントです。
RestController。
src/main/java/org/littlewings/spring/jcache/CalcController.java
package org.littlewings.spring.jcache; import org.springframework.beans.factory.annotation.Autowired; 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 { @Autowired protected CalcService calcService; @RequestMapping("add") public int add(@RequestParam int a, @RequestParam int b) { return calcService.add(a, b); } }
@AutowiredしてあるServiceクラスが、Cacheを使用します。
で、Serviceクラスの実装
src/main/java/org/littlewings/spring/jcache/CalcService.java
package org.littlewings.spring.jcache; import java.util.concurrent.TimeUnit; import javax.cache.annotation.CacheDefaults; import javax.cache.annotation.CacheResult; import org.springframework.stereotype.Service; @Service @CacheDefaults(cacheName = "simpleCache") public class CalcService { @CacheResult public int add(int a, int b) { try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { } return a + b; } }
ここでは、JCacheの@CacheDefaultsアノテーションで使用するCacheを指定し、addメソッドに@CacheResultアノテーションを付与して結果をキャッシュするようにしました。
※個人的には、SpringのCache機能を使う時にはJCacheのアノテーションよりもSpringのアノテーションを使った方が好みですが
キャッシュを指定したので、Cacheの定義を行わなくてはいけません。今回は、設定ファイルで指定することにしました。
src/main/resources/application.properties
spring.cache.jcache.config=classpath:hazelcast-jcache.xml
デフォルトの「hazelcast.xml」でもいいような気がしますが、今回はファイル名を変えて明示してみました。
定義ファイルの中身。
src/main/resources/hazelcast-jcache.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>dev</name> <password>dev-pass</password> </group> <network> <port auto-increment="true" port-count="100">5701</port> <join> <multicast enabled="true"> <multicast-group>224.2.2.3</multicast-group> <multicast-port>54327</multicast-port> </multicast> <tcp-ip enabled="false"/> </join> </network> <cache name="simpleCache"> <key-type class-name="java.lang.Object"/> <value-type class-name="java.lang.Object"/> </cache> </hazelcast>
動作確認
では、起動してみます。
$ spring boot:run
1回目は遅いですが…
$ time curl 'http://localhost:8080/calc/add?a=3&b=5' 8 real 0m3.330s user 0m0.020s sys 0m0.000s
2回目以降は高速になります。
$ time curl 'http://localhost:8080/calc/add?a=3&b=5' 8 real 0m0.028s user 0m0.004s sys 0m0.004s
パラメーターを変更するとCacheのキーも変わるので、また時間がかかるようになりますが2回目からは高速になります。
$ time curl 'http://localhost:8080/calc/add?a=6&b=5' 11 real 0m3.030s user 0m0.009s sys 0m0.000s $ time curl 'http://localhost:8080/calc/add?a=6&b=5' 11 real 0m0.019s user 0m0.008s sys 0m0.000s
インスタンスを追加する
せっかくHazelcastがインメモリデータグリッドなので、ここで別のコンソールからもうひとつアプリケーションを起動してみます。
$ mvn spring-boot:run -Drun.arguments='--server.port=8081'
クラスタにNodeが追加されました。
Members [2] { Member [172.17.0.1]:5701 Member [172.17.0.1]:5702 this }
先ほどのcurlコマンドを、追加されたNodeに対して実行してみます。
$ time curl 'http://localhost:8081/calc/add?a=3&b=5' 8 real 0m0.208s user 0m0.000s sys 0m0.007s
高速に動作しますね。
では、新しいNodeに対して、パラメーターを変更して実行。
$ time curl 'http://localhost:8081/calc/add?a=15&b=7' 22 real 0m3.065s user 0m0.016s sys 0m0.016s
この結果を、最初に起動したNodeからも高速に取得することができます。
$ time curl 'http://localhost:8080/calc/add?a=15&b=7' 22 real 0m0.017s user 0m0.007s sys 0m0.000s
ここで、いったんアプリケーションを全部終了
有効期限を設定してみる
Cacheで有効期限を設定したいことはよくあることかと思いますので、HazelcastのJCache実装を使って有効期限を設定してみます。
<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.jcache.MyExpiryFactory"/> </cache>
HazelcastのJCache実装で有効期限を設定するためには、Factoryの実装を作成する必要があります。
src/main/java/org/littlewings/spring/jcache/MyExpiryFactory.java
package org.littlewings.spring.jcache; 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)); } }
今回は、エントリを作成して10秒後に有効期限切れするCache定義としました。
このクラスを、先ほどのexpiry-policy-factoryタグのclass-name属性に指定します。
次に、Cacheを使っていたServiceクラスの@CacheDefaultsアノテーションの設定値を変更して、新しく定義したCacheに変更します。
@Service // @CacheDefaults(cacheName = "simpleCache") @CacheDefaults(cacheName = "expiryCache") public class CalcService {
ここまでやったら、アプリケーション実行。
$ mvn spring-boot:run
この後にアプリケーションにアクセスすると、1回目は遅くて2回目以降は高速になりますが
$ time curl 'http://localhost:8080/calc/add?a=6&b=5' 11 real 0m3.507s user 0m0.007s sys 0m0.000s $ time curl 'http://localhost:8080/calc/add?a=6&b=5' 11 real 0m0.025s user 0m0.008s sys 0m0.000s
10秒経つと、また低速になります。
$ time curl 'http://localhost:8080/calc/add?a=6&b=5' 11 real 0m3.027s user 0m0.005s sys 0m0.006s
そして、しばらく高速になります。
$ time curl 'http://localhost:8080/calc/add?a=6&b=5' 11 real 0m0.016s user 0m0.006s sys 0m0.000s
Hazelcast Client
ドキュメントには載っていませんが、今度はHazelcast ClientでJCacheと統合してみます。
依存関係の変更
pom.xmlで、Hazelcast Clientを使うように修正。
</dependency> <dependency> <groupId>com.hazelcast</groupId> <artifactId>hazelcast-client</artifactId> <version>3.5.4</version> </dependency> <dependency> <groupId>com.hazelcast</groupId> <artifactId>hazelcast-spring</artifactId> </dependency>
hazelcast-clientのバージョンは、Spring Bootが使用しているものに合わせています。
コードの修正
とりあえず、ServiceクラスはsimpleCacheを使うように戻します。
@Service @CacheDefaults(cacheName = "simpleCache") // @CacheDefaults(cacheName = "expiryCache") public class CalcService {
設定ファイルも、それっぽいのを用意しておきます。
src/main/resources/hazelcast-jcache-client.xml
<?xml version="1.0" encoding="UTF-8" ?> <hazelcast-client xsi:schemaLocation="http://www.hazelcast.com/schema/client-config hazelcast-client-config-3.5.xsd" xmlns="http://www.hazelcast.com/schema/client-config" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <group> <name>dev</name> <password>dev-pass</password> </group> </hazelcast-client>
ここでは、JCacheの設定はできませんけれど。
application.propertiesには、設定ファイルを使うようにすることと、Cacheの名前をあらかじめ登録しておきます。
spring.cache.jcache.config=classpath:hazelcast-jcache-client.xml spring.cache.cache-names=simpleCache,expiryCache
Cache名をあらかじめ登録しておかないと、実行時に「そんなCacheないよ」と怒られることになります。
2015-12-19 18:11:52.225 ERROR 30670 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.IllegalArgumentException: Cannot find cache named 'simpleCache' for CacheResultOperation[CacheMethodDetails[method=public int org.littlewings.spring.jcache.CalcService.add(int,int), cacheAnnotation=@javax.cache.annotation.CacheResult(cacheKeyGenerator=interface javax.cache.annotation.CacheKeyGenerator, cacheName=, cachedExceptions=[], skipGet=false, cacheResolverFactory=interface javax.cache.annotation.CacheResolverFactory, exceptionCacheName=, nonCachedExceptions=[]), cacheName='simpleCache']]] with root cause java.lang.IllegalArgumentException: Cannot find cache named 'simpleCache' for CacheResultOperation[CacheMethodDetails[method=public int org.littlewings.spring.jcache.CalcService.add(int,int), cacheAnnotation=@javax.cache.annotation.CacheResult(cacheKeyGenerator=interface javax.cache.annotation.CacheKeyGenerator, cacheName=, cachedExceptions=[], skipGet=false, cacheResolverFactory=interface javax.cache.annotation.CacheResolverFactory, exceptionCacheName=, nonCachedExceptions=[]), cacheName='simpleCache']] at org.springframework.cache.interceptor.AbstractCacheResolver.resolveCaches(AbstractCacheResolver.java:81) ~[spring-context-4.2.4.RELEASE.jar:4.2.4.RELEASE] at org.springframework.cache.jcache.interceptor.AbstractCacheInterceptor.resolveCache(AbstractCacheInterceptor.java:61) ~[spring-context-support-4.2.4.RELEASE.jar:4.2.4.RELEASE]
Serverの起動
Hazelcast Clientを使う場合、Client自身はデータを持たないので対となるServerが必要です。
これをやっていない場合、Client側は起動できません。
Caused by: java.lang.IllegalStateException: Unable to connect to any address in the config! The following addresses were tried:[localhost/127.0.0.1:5701, localhost/127.0.0.1:5702, localhost/127.0.0.1:5703] at com.hazelcast.client.spi.impl.ClusterListenerSupport.connectToOne(ClusterListenerSupport.java:215) at com.hazelcast.client.spi.impl.ClusterListenerSupport.connectToCluster(ClusterListenerSupport.java:148) at com.hazelcast.client.spi.impl.ClientClusterServiceImpl.start(ClientClusterServiceImpl.java:183) at com.hazelcast.client.impl.HazelcastClientInstanceImpl.start(HazelcastClientInstanceImpl.java:262) at com.hazelcast.client.HazelcastClient.newHazelcastClient(HazelcastClient.java:86) at com.hazelcast.client.cache.impl.HazelcastClientCachingProvider.instanceFromProperties(HazelcastClientCachingProvider.java:104) at com.hazelcast.client.cache.impl.HazelcastClientCachingProvider.createHazelcastCacheManager(HazelcastClientCachingProvider.java:70) at com.hazelcast.client.cache.impl.HazelcastClientCachingProvider.createHazelcastCacheManager(HazelcastClientCachingProvider.java:38) at com.hazelcast.cache.impl.AbstractHazelcastCachingProvider.getCacheManager(AbstractHazelcastCachingProvider.java:95)
こちらは、Groovyでさっくり書いてみました。
cache-server.groovy
@Grab('javax.cache:cache-api:1.0.0') @Grab('com.hazelcast:hazelcast:3.5.4') import com.hazelcast.core.* def hazelcast = Hazelcast.newHazelcastInstance()
ポイントは、JCacheへの依存関係を入れておくことです。これがないと、CacheServiceが使えなくなります。
こんな感じで。
重大: [172.17.0.1]:5701 [dev] [3.5.4] While executing request: com.hazelcast.client.impl.client.ClientCreateRequest@483d167d -> No service registered with name: hz:impl:cacheService java.lang.IllegalArgumentException: No service registered with name: hz:impl:cacheService at com.hazelcast.client.impl.ClientEngineImpl$ClientPacketProcessor.initService(ClientEngineImpl.java:511) at com.hazelcast.client.impl.ClientEngineImpl$ClientPacketProcessor.processRequest(ClientEngineImpl.java:461) at com.hazelcast.client.impl.ClientEngineImpl$ClientPacketProcessor.run(ClientEngineImpl.java:384) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at java.lang.Thread.run(Thread.java:745) at com.hazelcast.util.executor.HazelcastManagedThread.executeRun(HazelcastManagedThread.java:76) at com.hazelcast.util.executor.HazelcastManagedThread.run(HazelcastManagedThread.java:92)
で、起動。
$ groovy cache-server.groovy
確認
ここまでやったら、アプリケーションを起動。
$ mvn spring-boot:run
あとは、普通に使えます。
$ time curl 'http://localhost:8080/calc/add?a=6&b=5' 11 real 0m3.432s user 0m0.007s sys 0m0.004s $ time curl 'http://localhost:8080/calc/add?a=6&b=5' 11 real 0m0.032s user 0m0.000s sys 0m0.006s
クライアント側はデータを持っていないので、Spring側のアプリケーションを再起動しても、Hazelcast Serverが存在し続けていればデータを再度取得することができます。
オマケ(Clientを入れつつ、Serverとして使いたい)
こんなシーンがあるかどうかはわかりませんが、Maven依存関係にhazelcast-clientもしくはhazelcast-allを入れると、デフォルトでHazelcast Client側のJCache Providerを使用して動作するようになります。
これをServer側に切り替えるためには、CachingProviderの実装を明示的に指定します。
spring.cache.jcache.provider=com.hazelcast.cache.impl.HazelcastServerCachingProvider
これで、Serverとして動作します。