CLOVER🍀

That was when it all began.

Spring BootのCache auto-configuration×JCache(Hazelcast)で遊ぶ

Spring Boot 1.3から、Cacheのauto-configurationが入ったということで。

Cache auto-configuration in Spring Boot 1.3

遊ぼう遊ぼうと思いつつ試せていなかったので、そろそろトライしてみることに。

Caching


サポートしているCacheのProviderは、こちら。

Supported cache providers

  • Generic
  • JCache
  • Ehcache 2
  • Hazelcast
  • Infinispan
  • Redis
  • Guava
  • Simple

サンプルもあるようです。

Spring Boot Cache Sample

このうち、今回はJCache(実装はHazelcast)を使用してみます。

準備

pom.xmlは、このように用意。
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-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の定義を行わなくてはいけません。今回は、設定ファイルで指定することにしました。

JCache

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として動作します。