Spring Data Geodeには、SpringのCache Abstractionで使うCacheManagerの実装が含まれています。
※もとはもちろん、Spring Data Gemfireのものですが
Support for Spring Cache Abstraction
今回、こちらを試してみたいと思います。
なお、構成はClient/Server Modeで行うものとします。
準備
Maven依存関係の定義。Spring Bootを使いつつやります。
<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.5.1.RELEASE</spring.boot.version> </properties> <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-cache</artifactId> </dependency> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-geode</artifactId> <version>1.0.0.INCUBATING-RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <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>
Spring Bootのバージョンは、1.5.1としました。
Spring Data Geode 1.0.0.INCUBATING-RELEASEとSpring Boot 1.5.xの組み合わせはRepositoryがうまく動かない
感じなのですが、CacheManagerなら問題なかろうと…。
で、実際問題ありませんでした。
サンプルアプリケーションの作成
SpringのCache Abstractionを試す、サンプルアプリケーションを作成します。
簡単に、こんなクラスを作成。それぞれ、動作がわかりやすいように3秒間のスリープを入れています。
引数を2倍、足し算を行うService。
src/main/java/org/littlewings/geode/spring/CalcService.java
package org.littlewings.geode.spring; import java.util.concurrent.TimeUnit; import org.springframework.cache.annotation.CacheConfig; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; @Service @CacheConfig(cacheNames = "calcRegion") public class CalcService { @Cacheable public int doubling(int x) { try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { // ignore } return x * 2; } @Cacheable public int add(int a, int b) { try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { // ignore } return a + b; } }
書籍の登録と取得を行うService。
src/main/java/org/littlewings/geode/spring/BookService.java
package org.littlewings.geode.spring; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import org.springframework.cache.annotation.CacheConfig; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; @Service @CacheConfig(cacheNames = "bookRegion") public class BookService { Map<String, Book> books = new ConcurrentHashMap<>(); @Cacheable public Book find(String isbn) { try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { // ignore } return books.get(isbn); } public void put(Book book) { books.put(book.getIsbn(), book); } }
Bookクラスの定義は、ふつうのSerializableなJavaBeansです。
src/main/java/org/littlewings/geode/spring/Book.java
package org.littlewings.geode.spring; import java.io.Serializable; public class Book implements Serializable { private static final long serialVersionUID = 1L; private String isbn; private String title; private Integer price; public Book(String isbn, String title, Integer price) { this.isbn = isbn; this.title = title; this.price = price; } public Book() { } // getter/setterは省略 }
Cacheの定義(Client側)
Client側、Spring Data Geode側で使うCacheの定義は、こんな感じにしました。
src/main/resources/client-cache.xml
<?xml version="1.0" encoding="UTF-8"?> <client-cache xmlns="http://geode.apache.org/schema/cache" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://geode.apache.org/schema/cache http://geode.apache.org/schema/cache/cache-1.0.xsd" version="1.0"> <pool name="client-pool" subscription-enabled="true"> <locator host="localhost" port="10334"/> </pool> <region name="calcRegion" refid="PROXY"> <region-attributes pool-name="client-pool"/> </region> <region name="bookRegion" refid="PROXY"> <region-attributes pool-name="client-pool"/> </region> </client-cache>
2つのRegionを定義しているだけですね。
Spring Data GeodeとCacheManagerの設定
用意した設定を読み込んで使うように、Spring Data GeodeのCacheの設定を行います。
src/main/java/org/littlewings/geode/spring/CachingConfig.java
package org.littlewings.geode.spring; import org.apache.geode.cache.client.ClientCache; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; import org.springframework.data.gemfire.client.ClientCacheFactoryBean; import org.springframework.data.gemfire.support.GemfireCacheManager; @Configuration @EnableCaching public class CachingConfig { @Bean public ClientCacheFactoryBean geodeCache() throws Exception { ClientCacheFactoryBean clientCacheFactory = new ClientCacheFactoryBean(); clientCacheFactory.setCacheXml(new ClassPathResource("client-cache.xml")); clientCacheFactory.afterPropertiesSet(); return clientCacheFactory; } @Bean public CacheManager cacheManager(ClientCache cache) { GemfireCacheManager cacheManager = new GemfireCacheManager(); cacheManager.setCache(cache); cacheManager.afterPropertiesSet(); return cacheManager; } }
@EnableCachingは付与しておきます。
Spring Data GeodeでRepositoryを使う時のように、Region自体をBean定義する必要はありません。
※Region自体はCache XMLに定義されているので
Regionも、Cacheの定義に含まれて入れば特にCacheManagerに指定する必要はありませんが、Cache定義してあるものの中から
CacheManagerで使えるRegionを絞りたい場合は、GemfireCacheManager#setCacheNames、
もしくはGemfireCacheManager#setRegionsで使えるRegionを絞ることができます
@Bean public CacheManager cacheManager(ClientCache cache) { GemfireCacheManager cacheManager = new GemfireCacheManager(); cacheManager.setCache(cache); // Region名を指定 cacheManager.setCacheNames(new HashSet<>(Arrays.asList("calcRegion", "bookRegion"))); /* もしくは cacheManager.setRegions(new HashSet<>( Arrays.asList(cache.getRegion("calcRegion"), cache.getRegion("bookRegion")) )); */ cacheManager.afterPropertiesSet(); return cacheManager; }
これらを指定しなかった場合は、ClientCacheから取得可能なRegionがすべてCacheManagerで使用可能に
なります(ClientCache#getRegionで定義があれば使います)。
Spring Bootの有効化
実行自体ははテストコードで行いますが、Spring Boot有効化のために、@SpringBootApplicationアノテーションを
付与したクラスを用意しておきます。
src/main/java/org/littlewings/geode/spring/App.java package org.littlewings.geode.spring; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class App { }
Server側のCacheの設定
Client/Server Modeになるので、Server側にCacheの定義が必要です。
今回は、このような定義にしました。
cache.xml
<?xml version="1.0" encoding="UTF-8"?> <cache xmlns="http://geode.apache.org/schema/cache" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://geode.apache.org/schema/cache http://geode.apache.org/schema/cache/cache-1.0.xsd" version="1.0"> <region name="calcRegion" refid="PARTITION_REDUNDANT"> <region-attributes> <entry-time-to-live> <expiration-attributes timeout="5"/> </entry-time-to-live> </region-attributes> </region> <region name="bookRegion" refid="PARTITION_REDUNDANT"> <region-attributes> <entry-time-to-live> <expiration-attributes timeout="5"/> </entry-time-to-live> </region-attributes> </region> </cache>
Regionに有効期限を設定していますが、この確認自体はこのあとに出てくるテストコードでは行いません。
gfshでの「start server」時に、「--cache-xml-file」オプションでCache XMLを指定してServerを起動しましょう。
テストコードの作成と確認
それでは、確認を兼ねてテストコードを書いていきます。
テストコードの雛形は、こちら。
src/test/java/org/littlewings/geode/spring/GeodeCacheTest.java
package org.littlewings.geode.spring; import org.apache.geode.cache.Region; import org.apache.geode.cache.client.ClientCache; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cache.interceptor.SimpleKey; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.util.StopWatch; import static org.assertj.core.api.Assertions.assertThat; @RunWith(SpringRunner.class) @SpringBootTest public class GeodeCacheTest { // ここに、テストを書く! }
シンプルなパターンで確認
まずは、CalcServiceの単純に引数を2倍するメソッドをテストしてみます。
@Autowired ClientCache cache; @Autowired CalcService calcService; @Test public void calcCacheDoubling() { StopWatch stopWatch = new StopWatch(); // 初回は低速 stopWatch.start(); assertThat(calcService.doubling(4)) .isEqualTo(8); stopWatch.stop(); assertThat((long) stopWatch.getLastTaskInfo().getTimeSeconds()) .isGreaterThanOrEqualTo(3L); // 2回目は高速 stopWatch.start(); assertThat(calcService.doubling(4)) .isEqualTo(8); stopWatch.stop(); assertThat((long) stopWatch.getLastTaskInfo().getTimeSeconds()) .isEqualTo(0L); // 別のキーにすると、キャッシュに乗っていないので低速 stopWatch.start(); assertThat(calcService.doubling(15)) .isEqualTo(30); stopWatch.stop(); assertThat((long) stopWatch.getLastTaskInfo().getTimeSeconds()) .isGreaterThanOrEqualTo(3L); // Regionで確認もできる Region<Object, Object> region = cache.getRegion("calcRegion"); assertThat(region.get(4)) .isEqualTo(8); }
1回目は低速、2回目は高速と、特に問題なく動作していてCacheManagerを介して登録した値がRegionを
使って取得できることも確認できます。
キーが複数となるメソッドを使用する
続いては、CalcServiceの足し算のメソッドをテストしましょう。
こんなコードを用意します。
@Test public void calcCacheAdd() { StopWatch stopWatch = new StopWatch(); // 初回は低速 stopWatch.start(); assertThat(calcService.add(1, 5)) .isEqualTo(6); stopWatch.stop(); assertThat((long) stopWatch.getLastTaskInfo().getTimeSeconds()) .isGreaterThanOrEqualTo(3L); // 2回目は高速 stopWatch.start(); assertThat(calcService.add(1, 5)) .isEqualTo(6); stopWatch.stop(); assertThat((long) stopWatch.getLastTaskInfo().getTimeSeconds()) .isEqualTo(0L); // SimpleKeyを指定して、Regionより確認 Region<Object, Object> region = cache.getRegion("calcRegion"); assertThat(region.get(new SimpleKey(1, 5))) .isEqualTo(6); }
ところがこのコードは、そのまま動かすとエラーになります。Server側でこんな感じでコケて、Client側にも
例外が飛んできます。
Caused by: java.lang.ClassNotFoundException: org.springframework.cache.interceptor.SimpleKey at java.net.URLClassLoader.findClass(URLClassLoader.java:381) at java.lang.ClassLoader.loadClass(ClassLoader.java:424) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331) at java.lang.ClassLoader.loadClass(ClassLoader.java:357) at java.lang.Class.forName0(Native Method) at java.lang.Class.forName(Class.java:348) at java.io.ObjectInputStream.resolveClass(ObjectInputStream.java:677) at org.apache.geode.internal.InternalDataSerializer$DSObjectInputStream.resolveClass(InternalDataSerializer.java:3599) at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1819) at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1713) at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1986) at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1535) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:422) at org.apache.geode.internal.InternalDataSerializer.basicReadObject(InternalDataSerializer.java:2996) at org.apache.geode.DataSerializer.readObject(DataSerializer.java:3281) at org.apache.geode.internal.util.BlobHelper.deserializeBlob(BlobHelper.java:103) at org.apache.geode.internal.cache.tier.sockets.CacheServerHelper.deserialize(CacheServerHelper.java:82) at org.apache.geode.internal.cache.tier.sockets.Part.getObject(Part.java:273) at org.apache.geode.internal.cache.tier.sockets.Part.getObject(Part.java:282) at org.apache.geode.internal.cache.tier.sockets.Part.getStringOrObject(Part.java:287) at org.apache.geode.internal.cache.tier.sockets.command.Get70.cmdExecute(Get70.java:95) at org.apache.geode.internal.cache.tier.sockets.BaseCommand.execute(BaseCommand.java:147) at org.apache.geode.internal.cache.tier.sockets.ServerConnection.doNormalMsg(ServerConnection.java:783) at org.apache.geode.internal.cache.tier.sockets.ServerConnection.doOneMessage(ServerConnection.java:913) at org.apache.geode.internal.cache.tier.sockets.ServerConnection.run(ServerConnection.java:1180) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at org.apache.geode.internal.cache.tier.sockets.AcceptorImpl$1$1.run(AcceptorImpl.java:546) at java.lang.Thread.run(Thread.java:745)
デシリアライズしようとして、SimpleKeyが見つからないと言われております…。
仕方がないので、SpringのJARファイルをServer側にデプロイします。対象は、とりあえず「spring-core-4.3.6.RELEASE.jar」、
「spring-context-4.3.6.RELEASE.jar」の2つとします。
※SimpleKeyがクラス定義として見れればいいのかな?という安易な発想
gfsh>deploy --dir=/path/to/deploy-targetdir Deploying files: spring-core-4.3.6.RELEASE.jar, spring-context-4.3.6.RELEASE.jar Total file size is: 2.15MB Continue? (Y/n): y Member | Deployed JAR | Deployed JAR Location ------------------- | ------------ | ---------------------------------------------------------------- server-d67e9da1dd69 | | ERROR: java.lang.NoClassDefFoundError: org/apache/tools/ant/Task
なんか、エラーになりましたけど。依存関係が足りません、ってことですねぇ…。
ちなみに、この状態でもJARファイル自体は認識しているようで、先ほどのテストコードはパスするように
なります。
アンデプロイも可能です。
gfsh>undeploy --jar=spring-context-4.3.6.RELEASE.jar Member | Un-Deployed JAR | Un-Deployed From JAR Location ------------------- | -------------------------------- | ------------------------------------------------------------------------------ server-d67e9da1dd69 | spring-context-4.3.6.RELEASE.jar | /opt/apache-geode/server-d67e9da1dd69/vf.gf#spring-context-4.3.6.RELEASE.jar#1 gfsh>undeploy --jar=spring-core-4.3.6.RELEASE.jar Member | Un-Deployed JAR | Un-Deployed From JAR Location ------------------- | ----------------------------- | --------------------------------------------------------------------------- server-d67e9da1dd69 | spring-core-4.3.6.RELEASE.jar | /opt/apache-geode/server-d67e9da1dd69/vf.gf#spring-core-4.3.6.RELEASE.jar#1
まあ、ふつうにやる時は依存関係ごとデプロイする感じでしょうねぇ…。
ユーザー定義のクラスを値として登録する
最後は、BookServiceを使ったテストコードです。このコードの場合は、Cacheに登録する値がユーザー定義のものになります。
テストコードはこんな感じ。
@Autowired BookService bookService; @Test public void bookCache() { StopWatch stopWatch = new StopWatch(); Book book = new Book("978-4798142470", "Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発", 4320); // データの登録 bookService.put(book); // 初回の取得は低速 stopWatch.start(); assertThat(bookService.find("978-4798142470").getTitle()) .isEqualTo("Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発"); stopWatch.stop(); assertThat((long) stopWatch.getLastTaskInfo().getTimeSeconds()) .isGreaterThanOrEqualTo(3L); // 2回目は高速 stopWatch.start(); assertThat(bookService.find("978-4798142470").getTitle()) .isEqualTo("Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発"); stopWatch.stop(); assertThat((long) stopWatch.getLastTaskInfo().getTimeSeconds()) .isEqualTo(0L); // Regionでも確認できる Region<String, Book> region = cache.getRegion("bookRegion"); Book foundBook = region.get("978-4798142470"); assertThat(foundBook.getTitle()) .isEqualTo("Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発"); }
先ほどSimpleKeyを使ったテストコードがエラーになったので、こちらもNGになるかと思いきやふつうにパスします。
Apache GeodeのServer側から見ればSpringのSimpleKeyクラスも今回作ったBookクラスも未知の値ですが、
キーに対して使ってしまうとデシリアライズしないと比較できないので困ります、ということなのでしょうね。
※値はキーで取得時には、中身がわからなくてもよい
@Cacheableを付与したメソッドの引数が単一であれば(key属性でSpELで調整しなければ)そのままキーとなり、
複数のものであればSimpleKeyになるので、キーは単一にできるだけした方がいいよーってことでしょう。
まとめ
Spring Data Geodeを使って、SpringのCache Abstractionを試してみました。
こちらはそれほどハマることなく、割と簡単に使えたと思います。…ある程度Apache Geodeに慣れていれば。