CLOVER🍀

That was when it all began.

HazelcastCacheManager or JCacheCacheManager

最近、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の実装を使う、という分けでいいような印象を持ちました。