Hatena::ブログ(Diary)

CLOVER

2017-02-19

Spring BootのAutoConfigurationを自分で作ってみる

Spring Bootの勉強として、自分でAutoConfigurationを作ってみようかなと思いまして。

AutoConfigurationの内容を読んだりはしますが、自分で作ってみた方がより理解が深まりますよね。

というわけで、お題を以下のように設定してみました。

  • Spring Boot 1.5.1.RELEASEを使用
  • Ehcache3のAutoConfigurationを作る(元Spring Bootにはない)
  • SpringのCache Abstractionには絡めない
  • 単純に、CacheManagerをAutoConfigurationするだけ
  • 作成したstarterを、別のプロジェクトで読み込んで動かしてみる

参考にしたのは、まずは以下のドキュメント。

44. Creating your own auto-configuration

そして、Spring BootのAutoConfiguration自体も参考にしています。

https://github.com/spring-projects/spring-boot/tree/v1.5.1.RELEASE/spring-boot-autoconfigure

というか、これが1番の教材ではないでしょうか…。

それでは、始めてみます。

Mavenプロジェクトを作る

まずは、Mavenプロジェクトを作成します。

以下に習うと、「autoconfigure」と「starter」という2つのモジュールを作るのがよいそうです。

Creating your own starter

Namingについては、「xxxxx-spring-boot-autoconfigure」とか「xxxxx--spring-boot-starter」みたいな名前に
するべきだそうです。サードパーティ製のものが、「spring-boot」で始めるのは避けるようにしましょう、と。

「autoconfigure」には起動に必要な設定、コンポーネント定義などを実装し、「starger」は依存関係を
管理するだけのモジュールとします。

というわけで、Mavenプロジェクトは以下のようなマルチプロジェクト構成とします。

$ find ehcache3-spring-boot -maxdepth 2 | grep pom.xml
ehcache3-spring-boot/pom.xml
ehcache3-spring-boot/ehcache3-spring-boot-autoconfigure/pom.xml
ehcache3-spring-boot/ehcache3-spring-boot-starter/pom.xml

以降は、「ehcache3-spring-boot」ディレクトリ内で作業をします。

$ cd ehcache3-spring-boot

親となる「ehcache3-spring-boot」プロジェクトの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>ehcache3-spring-boot</artifactId>
    <packaging>pom</packaging>
    <version>0.0.1</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <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>

            <dependency>
                <groupId>org.ehcache</groupId>
                <artifactId>ehcache</artifactId>
                <version>3.2.0</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring.boot.version}</version>
            </plugin>
        </plugins>
    </build>

    <modules>
        <module>ehcache3-spring-boot-autoconfigure</module>
        <module>ehcache3-spring-boot-starter</module>
    </modules>
</project>

今回は、dependencyManagementに必要な依存関係を定義しておきます。

以降、「autoconfigure」と「starter」を見ていきましょう。

autoconfigureの作成

まずはautoconfigureを作成します。

pom.xmlの定義

pom.xmlの定義は、こんな感じにしました。
ehcache3-spring-boot-autoconfigure/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">
    <parent>
        <artifactId>ehcache3-spring-boot</artifactId>
        <groupId>org.littlewings</groupId>
        <version>0.0.1</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>ehcache3-spring-boot-autoconfigure</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.ehcache</groupId>
            <artifactId>ehcache</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
</project>

依存関係にはSpring Bootを使うのに最低限必要な「spring-boot-starter」を含め、Ehcache3はoptionalに設定。

AutoConfigurationを作成する

以下の2つの内容に習い、AutoConfigurationを作成します。

Understanding auto-configured beans

Condition annotations

各種@Conditinalなアノテーションを付与した@Configurationなクラスを作成し、Beanの定義をしていきます。

@Conditionalアノテーション自体は、各種@Conditionalなアノテーションに付与するアノテーションみたいですね。
以下のような各種条件で、設定が可能なようです。

  • Class conditions(@ConditionalOnClass、@ConditionalOnMissingClass) … クラスパス上に指定のクラスの有無
  • Bean conditions(@ConditionalOnBean、@ConditionalOnMissingBean) … ApplicationContextへのBean定義の有無
  • Property conditions(@ConditionalOnProperty) … プロパティ定義の有無
  • Resource conditions(@ConditionalOnResource) … リソースファイルの有無
  • Web application conditions(@ConditionalOnWebApplication、@ConditionalOnNotWebApplication) … Webアプリケーションかどうか
  • SpEL expression conditions(@ConditionalOnExpression) … SpELによる条件

今回は、シンプルにいってみます。こんなクラスを作成。
ehcache3-spring-boot-autoconfigure/src/main/java/org/littlewings/spring/boot/ehcache3/autoconfigure/Ehcache3AutoConfiguration.java

package org.littlewings.spring.boot.ehcache3.autoconfigure;

import java.io.IOException;
import java.net.URL;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.ehcache.CacheManager;
import org.ehcache.config.builders.CacheConfigurationBuilder;
import org.ehcache.config.builders.CacheManagerBuilder;
import org.ehcache.config.builders.ResourcePoolsBuilder;
import org.ehcache.expiry.Duration;
import org.ehcache.expiry.Expirations;
import org.ehcache.xml.XmlConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConditionalOnClass(CacheManager.class)
@ConditionalOnMissingBean(CacheManager.class)
@EnableConfigurationProperties(Ehcache3Properties.class)
public class Ehcache3AutoConfiguration {
    @Bean(destroyMethod = "close")
    public CacheManager ehcache3CacheManager(Ehcache3Properties ehcache3Properties) throws IOException {
        if (ehcache3Properties.getConfig() != null) {
            // Ehcache3の設定ファイルがある場合
            URL configUrl = ehcache3Properties.getConfig().getURL();
            org.ehcache.config.Configuration configuration = new XmlConfiguration(configUrl);
            CacheManager cacheManager = CacheManagerBuilder.newCacheManager(configuration);
            cacheManager.init();
            return cacheManager;
        } else {
            // 設定からCacheManager作成
            Set<String> cacheNames = ehcache3Properties.getCacheNames();
            Long heapEntries = ehcache3Properties.getHeapEntries();
            Integer timeToLiveExpirationSeconds = ehcache3Properties.getTimeToLiveExpirationSeconds();
            Integer timeToIdleExpirationSeconds = ehcache3Properties.getTimeToIdleExpirationSeconds();

            CacheManagerBuilder<CacheManager> cacheManagerBuilder =
                    CacheManagerBuilder
                            .newCacheManagerBuilder();

            if (cacheNames != null) {
                for (String cacheName : cacheNames) {
                    CacheConfigurationBuilder<Object, Object> cacheConfigurationBuilder =
                            CacheConfigurationBuilder
                                    .newCacheConfigurationBuilder(Object.class, Object.class, ResourcePoolsBuilder.heap(heapEntries));

                    // ttl
                    if (timeToLiveExpirationSeconds != null) {
                        cacheConfigurationBuilder =
                                cacheConfigurationBuilder.withExpiry(Expirations.timeToLiveExpiration(Duration.of(timeToLiveExpirationSeconds, TimeUnit.SECONDS)));
                    }

                    // idle timeout
                    if (timeToIdleExpirationSeconds != null) {
                        cacheConfigurationBuilder =
                                cacheConfigurationBuilder.withExpiry(Expirations.timeToIdleExpiration(Duration.of(timeToIdleExpirationSeconds, TimeUnit.SECONDS)));
                    }

                    cacheManagerBuilder = cacheManagerBuilder.withCache(cacheName, cacheConfigurationBuilder.build());
                }
            }

            CacheManager cacheManager = cacheManagerBuilder.build();
            cacheManager.init();
            return cacheManager;
        }
    }
}

@Configurationを付与したクラスを作成して、

@Configuration
@ConditionalOnClass(CacheManager.class)
@ConditionalOnMissingBean(CacheManager.class)
@EnableConfigurationProperties(Ehcache3Properties.class)
public class Ehcache3AutoConfiguration {

CacheManagerがクラスパス上にあり

@ConditionalOnClass(CacheManager.class)

CacheManagerがBean定義になければ有効ですと。

@ConditionalOnMissingBean(CacheManager.class)

あと、設定情報としてEhcache3Propertiesというクラスを使用するようにします。

@EnableConfigurationProperties(Ehcache3Properties.class)

Ehcache3Propertiesクラスの定義は、こんな感じです。
ehcache3-spring-boot-autoconfigure/src/main/java/org/littlewings/spring/boot/ehcache3/autoconfigure/Ehcache3Properties.java

package org.littlewings.spring.boot.ehcache3.autoconfigure;

import java.util.Set;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.io.Resource;

import static org.littlewings.spring.boot.ehcache3.autoconfigure.Ehcache3Properties.EHCACHE3_PREFIX;

@ConfigurationProperties(prefix = EHCACHE3_PREFIX)
public class Ehcache3Properties {
    public static final String EHCACHE3_PREFIX = "ehcache3";

    private Set<String> cacheNames;

    private Integer timeToLiveExpirationSeconds;

    private Integer timeToIdleExpirationSeconds;

    private Long heapEntries;

    private Resource config;

    // getter/setterは省略
}

これで、application.propertiesにこんな感じとか(これは設定ファイルの指定)

ehcache3.config=ehcache3.xml

こんな感じに書けるようになります。

ehcache3.cache-names=fooCache,barCache
ehcache3.heap-entries=10
ehcache3.time-to-live-expiration-seconds=3

これを使って、Ehcache3の設定ファイルが指定されていればそれを使い、そうでなければその他の
プロパティからCacheManagerとCacheを定義するようにしています。

    @Bean(destroyMethod = "close")
    public CacheManager ehcache3CacheManager(Ehcache3Properties ehcache3Properties) throws IOException {
        if (ehcache3Properties.getConfig() != null) {
            // Ehcache3の設定ファイルがある場合
            URL configUrl = ehcache3Properties.getConfig().getURL();
            org.ehcache.config.Configuration configuration = new XmlConfiguration(configUrl);
            CacheManager cacheManager = CacheManagerBuilder.newCacheManager(configuration);
            cacheManager.init();
            return cacheManager;
        } else {
            // 設定からCacheManager作成
            Set<String> cacheNames = ehcache3Properties.getCacheNames();
            Long heapEntries = ehcache3Properties.getHeapEntries();
            Integer timeToLiveExpirationSeconds = ehcache3Properties.getTimeToLiveExpirationSeconds();
            Integer timeToIdleExpirationSeconds = ehcache3Properties.getTimeToIdleExpirationSeconds();

            CacheManagerBuilder<CacheManager> cacheManagerBuilder =
                    CacheManagerBuilder
                            .newCacheManagerBuilder();

            if (cacheNames != null) {
                for (String cacheName : cacheNames) {
                    CacheConfigurationBuilder<Object, Object> cacheConfigurationBuilder =
                            CacheConfigurationBuilder
                                    .newCacheConfigurationBuilder(Object.class, Object.class, ResourcePoolsBuilder.heap(heapEntries));

                    // ttl
                    if (timeToLiveExpirationSeconds != null) {
                        cacheConfigurationBuilder =
                                cacheConfigurationBuilder.withExpiry(Expirations.timeToLiveExpiration(Duration.of(timeToLiveExpirationSeconds, TimeUnit.SECONDS)));
                    }

                    // idle timeout
                    if (timeToIdleExpirationSeconds != null) {
                        cacheConfigurationBuilder =
                                cacheConfigurationBuilder.withExpiry(Expirations.timeToIdleExpiration(Duration.of(timeToIdleExpirationSeconds, TimeUnit.SECONDS)));
                    }

                    cacheManagerBuilder = cacheManagerBuilder.withCache(cacheName, cacheConfigurationBuilder.build());
                }
            }

            CacheManager cacheManager = cacheManagerBuilder.build();
            cacheManager.init();
            return cacheManager;
        }
    }

@ConditionalOnMissingBeanなどは今回クラス定義に付与していますが、もうちょっと別のBean定義も制御したいなどと
いった場合は、メソッドにも付与することができます。Spring Boot自身のAutoConfigurationでも多用されているので、
確認してみるとよいでしょう。
※これでもCacheManagerがBean定義されていなかったら、という意味になります

    @Bean(destroyMethod = "close")
    @ConditionalOnMissingBean
    public CacheManager ehcache3CacheManager(Ehcache3Properties ehcache3Properties) throws IOException {

spring.factoriesの作成

META-INFディレクトリに、spring.factoriesというファイルを作成します。EnableAutoConfigurationに対する指定として、
先ほど作成した@Configurationなクラスを指定すればOKです。
ehcache3-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.littlewings.spring.boot.ehcache3.autoconfigure.Ehcache3AutoConfiguration

ドキュメントに記載のあるように、複数ある場合は「,」区切りで記述すればよさそうです。

Locating auto-configuration candidates

starterを作成する

starterはpom.xmlのみで、依存関係を定義したものだけを作成します。
ehcache3-spring-boot-starter/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">
    <parent>
        <artifactId>ehcache3-spring-boot</artifactId>
        <groupId>org.littlewings</groupId>
        <version>0.0.1</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>ehcache3-spring-boot-starter</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.littlewings</groupId>
            <artifactId>ehcache3-spring-boot-autoconfigure</artifactId>
            <version>${project.version}</version>
        </dependency>

        <dependency>
            <groupId>org.ehcache</groupId>
            <artifactId>ehcache</artifactId>
        </dependency>
    </dependencies>
</project>

とりあえず、「mvn install」

このできあがったAutoConfigurationを別のプロジェクトから使うために、「mvn install」しておきます。

$ mvn install

使ってみる

それでは、この作成したAutoConfigurationを使ったプロジェクトを作成してみます。

用意した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>ehcache3-spring-boot-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <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.littlewings</groupId>
            <artifactId>ehcache3-spring-boot-starter</artifactId>
            <version>0.0.1</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>

</project>

先ほど作成したAutoConfigurationを指定します。

        <dependency>
            <groupId>org.littlewings</groupId>
            <artifactId>ehcache3-spring-boot-starter</artifactId>
            <version>0.0.1</version>
        </dependency>

これで、作成したAutoConfigurationとEhcache3が依存関係に引き込まれます。

起動用のクラス作成。
src/main/java/demo/App.java

package demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {
    public static void main(String... args) {
        SpringApplication.run(App.class, args);
    }
}

設定も、簡単にしてみます。
src/main/resources/application.properties

ehcache3.cache-names=fooCache,barCache
ehcache3.heap-entries=10
ehcache3.time-to-live-expiration-seconds=3

確認は、テストコードで行ってみましょう。こんな感じで作成。
src/test/java/demo/DemoAutoConfigurationTest.java

package demo;

import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;
import java.util.stream.StreamSupport;

import org.ehcache.Cache;
import org.ehcache.CacheManager;
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.test.context.junit4.SpringRunner;

import static org.assertj.core.api.Assertions.assertThat;

@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoAutoConfigurationTest {
    @Autowired
    CacheManager cacheManager;

    @Test
    public void gettingStarted() {
        assertThat(cacheManager.getCache("fooCache", Object.class, Object.class))
                .isNotNull();
        assertThat(cacheManager.getCache("barCache", Object.class, Object.class))
                .isNotNull();

        assertThat(cacheManager.getCache("hogeCache", Object.class, Object.class))
                .isNull();
    }

    @Test
    public void expire() throws InterruptedException {
        Cache<Object, Object> cache = cacheManager.getCache("fooCache", Object.class, Object.class);

        cache.put("key", "value");
        assertThat(cache.get("key"))
                .isEqualTo("value");

        TimeUnit.SECONDS.sleep(5L);

        assertThat(cache.get("key"))
                .isNull();

        IntStream.rangeClosed(1, 20).forEach(i -> cache.put("key" + i, "value" + i));

        long size = StreamSupport
                .stream(cache.spliterator(), false)
                .count();
        assertThat(size)
                .isEqualTo(10L);
    }
}

これで動作します。AutoConfigurationで作成されたCacheManagerがインジェクション可能になっています。

    @Autowired
    CacheManager cacheManager;

次に、Ehcache3の設定ファイルベースに切り替えてみましょう。
src/main/resources/application.properties

ehcache3.config=ehcache3.xml

Ehcache3の設定は、こんな感じ。先ほどとはCacheの名前を変えておきます。
src/main/resources/ehcache3.xml

<?xml version="1.0" encoding="UTF-8"?>
<config
        xmlns='http://www.ehcache.org/v3'>
    <cache-template name="configureCacheTemplate">
        <key-type>java.lang.Object</key-type>
        <value-type>java.lang.Object</value-type>
        <expiry>
            <tti unit="seconds">5</tti>
        </expiry>
        <resources>
            <heap unit="entries">10</heap>
        </resources>
    </cache-template>

    <cache alias="configureCache" uses-template="configureCacheTemplate"/>
</config>

テストコードは、こんな感じのものに変更。

    @Test
    public void configureCache() throws InterruptedException {
        Cache<Object, Object> cache = cacheManager.getCache("configureCache", Object.class, Object.class);

        cache.put("key", "value");
        assertThat(cache.get("key"))
                .isEqualTo("value");

        TimeUnit.SECONDS.sleep(5L);

        assertThat(cache.get("key"))
                .isNull();

        IntStream.rangeClosed(1, 20).forEach(i -> cache.put("key" + i, "value" + i));

        long size = StreamSupport
                .stream(cache.spliterator(), false)
                .count();
        assertThat(size)
                .isEqualTo(10L);
    }

これでも動作します。

最後は、自分でCacheManagerを自分でBean定義してAutoConfigurationの内容を使わないようにしてみます。

起動クラスに、Bean定義を含めてみましょう。
src/main/java/demo/App.java

package demo;

import java.util.concurrent.TimeUnit;

import org.ehcache.CacheManager;
import org.ehcache.config.builders.CacheConfigurationBuilder;
import org.ehcache.config.builders.CacheManagerBuilder;
import org.ehcache.config.builders.ResourcePoolsBuilder;
import org.ehcache.expiry.Duration;
import org.ehcache.expiry.Expirations;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class App {
    public static void main(String... args) {
        SpringApplication.run(App.class, args);
    }

    @Bean(destroyMethod = "close")
    public CacheManager cacheManager() {
        CacheManagerBuilder<CacheManager> cacheManagerBuilder =
                CacheManagerBuilder
                        .newCacheManagerBuilder();

        CacheConfigurationBuilder<Object, Object> cacheConfigurationBuilder =
                CacheConfigurationBuilder
                        .newCacheConfigurationBuilder(Object.class, Object.class, ResourcePoolsBuilder.heap(10));

        // ttl
        cacheConfigurationBuilder =
                cacheConfigurationBuilder.withExpiry(Expirations.timeToLiveExpiration(Duration.of(3, TimeUnit.SECONDS)));

        cacheManagerBuilder = cacheManagerBuilder.withCache("myCache", cacheConfigurationBuilder.build());

        CacheManager cacheManager = cacheManagerBuilder.build();
        cacheManager.init();
        return cacheManager;
    }
}

今度は、「myCache」にしてみました。

        cacheManagerBuilder = cacheManagerBuilder.withCache("myCache", cacheConfigurationBuilder.build());

テストコードはこんな感じで。

    @Test
    public void myCache() throws InterruptedException {
        Cache<Object, Object> cache = cacheManager.getCache("myCache", Object.class, Object.class);

        cache.put("key", "value");
        assertThat(cache.get("key"))
                .isEqualTo("value");

        TimeUnit.SECONDS.sleep(5L);

        assertThat(cache.get("key"))
                .isNull();

        IntStream.rangeClosed(1, 20).forEach(i -> cache.put("key" + i, "value" + i));

        long size = StreamSupport
                .stream(cache.spliterator(), false)
                .count();
        assertThat(size)
                .isEqualTo(10L);
    }

AutoConfigurationで定義されたBeanではなく、アプリケーション側でBean定義されたCacheManagerが使用されたことが
確認できました。

とりあえず、こんな感じで動かすことができましたよ、と。

まとめ

簡単にではありますが、Spring BootのAutoConfigurationを自分でも作ってみました。

極めて初歩的な内容だと思いますが、あとはSpring BootのAutoConfigurationのソースコードを見たりしてパターンを覚えて
いけばいいのかな?と思います。

依存関係とかをもうちょっとまとめたい場合は、BOMを作った方がいいのかもしれません。
Spring Bootもこのスタイルですね。

https://github.com/spring-projects/spring-boot/tree/v1.5.1.RELEASE/spring-boot-dependencies

2017-02-18

Infinispan Serverを組み込みでSpring Boot上で動かせるようにしてみた

Spring Bootの勉強を兼ねて、かなり誰特ですがInfinispan Serverの各種モードをSpring Boot上に組み込んで
動かせるようにしてみました。

ソースコードはこちら。
https://github.com/kazuhira-r/infinispan-embedded-server-spring-boot

動かせるプロトコルは、

  • Hot Rod
  • Memcached
  • Rest

で、動かせないのは

  • WebSocket

です。

有効化すると、アプリケーション内部でInfinispan Serverを起動します。この時、サーバーはNetty 4で
動くことになります。

使い方

インストールして

$ git clone https://github.com/kazuhira-r/infinispan-embedded-server-spring-boot.git
$ cd infinispan-embedded-server-spring-boot
$ mvn install -DskipTests=true

BOMを読み込んで

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.littlewings</groupId>
                <artifactId>infinispan-embedded-server-spring-boot-dependencies</artifactId>
                <version>0.1.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

使いたいプロトコルに応じて、以下のいずれかを依存関係に加えます。

Hot Rod

        <dependency>
            <groupId>org.littlewings</groupId>
            <artifactId>infinispan-embedded-server-spring-boot-starter-hotrod</artifactId>
        </dependency>

Memcached

        <dependency>
            <groupId>org.littlewings</groupId>
            <artifactId>infinispan-embedded-server-spring-boot-starter-memcached</artifactId>
        </dependency>

Rest

        <dependency>
            <groupId>org.littlewings</groupId>
            <artifactId>infinispan-embedded-server-spring-boot-starter-rest</artifactId>
        </dependency>

追加したプロトコルに応じて、@Enable〜アノテーションを記述します。

Hot Rod

@EnableInfinispanEmbeddedHotRodServer
@SpringBootApplication
public class App {
    public static void main(String... args) {
        SpringApplication.run(App.class, args);
    }
}

Memcached

@EnableInfinispanEmbeddedMemcachedServer
@SpringBootApplication
public class App {
    public static void main(String... args) {
        SpringApplication.run(App.class, args);
    }
}

Rest

@EnableInfinispanEmbeddedRestServer
@SpringBootApplication
public class App {
    public static void main(String... args) {
        SpringApplication.run(App.class, args);
    }
}

全部合わせて使うこともできますが、EmbeddedCacheManagerは自分でBean定義する必要があります。
定義されたEmbeddedCacheManagerは、各サーバーに共有されます。
※以下のように書かなくても、別に設定ファイルを読み込ませる形でDefaultCacheManagerを作ってあげてもいいです

@EnableInfinispanEmbeddedHotRodServer
@EnableInfinispanEmbeddedMemcachedServer
@EnableInfinispanEmbeddedRestServer
@SpringBootApplication
public class App {
    public static void main(String... args) {
        SpringApplication.run(App.class, args);
    }

    @Bean
    public EmbeddedCacheManager embeddedCacheManager(InfinispanEmbeddedHotRodServerProperties hotRodServerProperties,
                                                     InfinispanEmbeddedMemcachedServerProperties memcachedServerProperties,
                                                     InfinispanEmbeddedRestServerProperties restServerProperties) {
        EmbeddedCacheManager cacheManager = new DefaultCacheManager(new ConfigurationBuilder().build());

        Set<String> cacheNames = new HashSet<>();
        cacheNames.addAll(hotRodServerProperties.getCacheNames() != null ? hotRodServerProperties.getCacheNames() : Collections.emptySet());
        cacheNames.addAll(memcachedServerProperties.getCacheNames() != null ? memcachedServerProperties.getCacheNames() : Collections.emptySet());
        cacheNames.addAll(restServerProperties.getCacheNames() != null ? restServerProperties.getCacheNames() : Collections.emptySet());

        cacheNames.forEach(cacheName -> cacheManager.defineConfiguration(cacheName, new ConfigurationBuilder().build()));

        return cacheManager;
    }
}

この場合Nettyサーバーはプロトコル別に起動します。

一応、設定もほどほどにできるようになっています。SSLや認証以外は、ProtocolServerConfigurationBuilderで設定できるものが
ほぼ設定可能です。

infinispan.embedded.server.hotrod.default-cache-name=
infinispan.embedded.server.hotrod.port=
...

infinispan.embedded.server.memcached.default-cache-name=
infinispan.embedded.server.memcached.port=
...

infinispan.embedded.server.rest.cache-names=
infinispan.embedded.server.rest.port=
...

参考にしたもの

いくつかのGitHubリポジトリを参考にしました。

Infinispan Serverの起動/停止

SmartLifecycleというインターフェースを実装することで、実現しています。このあたりは、Spring Cloud Netflixの
Eureka Serverの使い方を参考にしました。

あとは、@ConditionalOnMissingBeanなどを使いつつ、Infinispan Serverをある程度自動で組み上げて実行するようにしています。

なお、使っているInfinispanのバージョンは、クラスの階層関係の都合上しれっと9.0.0.CR1を使っています…。

まとめ

実は、はじめはAutoConfigurationを作るつもりだったのですが、途中で@Enable〜なアノテーションをつける形にするように
気が変わったので、こんな感じになりました。

いろいろ勉強になりました。けっこう面白かったです。

2017-02-12

Spring Data Geodeで、SpringのCache Abstractionを使う

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に慣れていれば。

2017-02-11

JAX-RS(RESTEasy/Undertow)で、Swaggerを使ってREST APIのドキュメントを生成する

Swaggerというものがあるのはなんとなく知っていたのですが、使ったことがなかったので
試してみます。

Swagger ? The World's Most Popular Framework for APIs.

Swaggerで始めるモデルファーストなAPI開発

あんまりちゃんと調べたことなかったのですが、JSON/YAMLを使ってRESTful APIのドキュメンテーションと
コード、実際のAPIの間をもつ仕組みみたいですね。



今回は、SwaggerのJAX-RS用のモジュールを使ってSwagger Spec(JSON)を生成し、Swagger UIで見るところまで
やってみます。

参考)
JAX-RS+Glassfish+SwaggerでシンプルにはじめるAPIドキュメンテーション - Qiita

JAX-RS の REST API ドキュメントを Swagger を使って生成する - なにか作る

また、構成は

  • JAX-RS(RESTEasy)
  • Servlet Container(Undertow)
  • Scala + Jackson Scala Module

とします。

Swagger Coreおよび、SwaggerのJAX-RS用のモジュールはこちら。

GitHub - swagger-api/swagger-core: Examples and server integrations for generating the Swagger API Specification, which enables easy access to your REST API

swagger-core/modules/swagger-jaxrs at master ? swagger-api/swagger-core ? GitHub

Swagger JAX-RSについては、こちらの情報とサンプルを参考にしています。

Swagger Core JAX RS Project Setup 1.5.X ? swagger-api/swagger-core Wiki ? GitHub

swagger-samples/java/java-resteasy at master ? swagger-api/swagger-samples ? GitHub

準備

ビルド定義。
build.sbt

name := "resteasy-swagger"

version := "0.0.1-SNAPSHOT"

organization := "org.littlewings"

scalaVersion := "2.12.1"

scalacOptions ++= Seq("-Xlint", "-deprecation", "-unchecked", "-feature")

updateOptions := updateOptions.value.withCachedResolution(true)

libraryDependencies ++= Seq(
  "org.jboss.resteasy" % "resteasy-undertow" % "3.0.19.Final",
  "org.jboss.resteasy" % "resteasy-jackson2-provider" % "3.0.19.Final",
  "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.8.6",
  "io.undertow" % "undertow-core" % "1.4.10.Final",
  "io.undertow" % "undertow-servlet" % "1.4.10.Final",
  "io.swagger" % "swagger-jaxrs" % "1.5.12" exclude("javax.ws.rs", "jsr311-api")
)

Swagger関係のものは「swagger-jaxrs」ですが、JAX-RS 1系の依存が入っているようなので除去しています。
Undertowで動かす時は、これが残っていると困ったことになりました。

あとは、RESTEasyでJackson 2、Jackson 2のScala用のモジュール、Undertowに燗する依存関係です。

JAX-RSリソースクラス(REST API)の作成

まずは、対象となるJAX-RSリソースクラス(REST API)を作っていきます。

特にJSONにはこだわらない足し算、掛け算を行うリソースクラス。
src/main/scala/org/littlewings/javaee7/rest/CalcResource.scala

package org.littlewings.javaee7.rest

import javax.ws.rs.core.MediaType
import javax.ws.rs.{GET, Path, Produces, QueryParam}

import io.swagger.annotations.{Api, ApiOperation}

@Path("calc")
@Api("calc")
class CalcResource {
  @GET
  @Path("add")
  @Produces(Array(MediaType.TEXT_PLAIN))
  @ApiOperation(value = "calc add")
  def add(@QueryParam("a") a: Int, @QueryParam("b") b: Int): Int =
    a + b

  @GET
  @Path("multiply")
  @Produces(Array(MediaType.TEXT_PLAIN))
  @ApiOperation(value = "calc multiply")
  def multiply(@QueryParam("a") a: Int, @QueryParam("b") b: Int): Int =
    a * b
}

Swaggerに燗する@Api、@ApiOperationを付与しています。@ProducesでMediaTypeをきっちり指定しておくと
Swaggerが生成するSpecにも反映してくれます。

@Apiで指定しているのは、APIに対するタグになります。

@Api("calc")

続いて、JSONを扱うリソースクラス。お題は書籍とし、データはインメモリで持つこととします。
src/main/scala/org/littlewings/javaee7/rest/BookResource.scala

package org.littlewings.javaee7.rest

import javax.ws.rs.core.{Context, MediaType, Response, UriInfo}
import javax.ws.rs._

import io.swagger.annotations.{Api, ApiOperation}

import scala.collection.JavaConverters._

object BookResource {
  private[rest] val books: scala.collection.mutable.Map[String, Book] =
    new java.util.concurrent.ConcurrentHashMap[String, Book]().asScala
}

@Path("book")
@Api("book")
class BookResource {
  @GET
  @Produces(Array(MediaType.APPLICATION_JSON))
  @ApiOperation(value = "find all books", response = classOf[Seq[Book]])
  def fildAll: Seq[Book] =
    BookResource.books.values.toVector

  @GET
  @Path("{isbn}")
  @Produces(Array(MediaType.APPLICATION_JSON))
  @ApiOperation(value = "find book", response = classOf[Book])
  def find(@PathParam("isbn") isbn: String): Book =
    BookResource.books.get(isbn).orNull

  @PUT
  @Path("{isbn}")
  @Consumes(Array(MediaType.APPLICATION_JSON))
  @Produces(Array(MediaType.APPLICATION_JSON))
  @ApiOperation("register book")
  def register(book: Book, @Context uriInfo: UriInfo): Response = {
    BookResource.books.put(book.isbn, book)
    Response.created(uriInfo.getRequestUriBuilder.build(book.isbn)).build
  }
}

先ほどと少し変えているのは@ApiOperationのresponseにメソッドの戻り値の型を指定しているところで、ここを指定して
おくとSwaggerが生成するJSONの方にもこの情報が伝わるようになります。

  @GET
  @Path("{isbn}")
  @Produces(Array(MediaType.APPLICATION_JSON))
  @ApiOperation(value = "find book", response = classOf[Book])
  def find(@PathParam("isbn") isbn: String): Book =
    BookResource.books.get(isbn).orNull

また、BookクラスはCase Classとして作成しましたが、JavaBeans(getter/setter)でないとSwaggerが型を理解してくれない
みたいなので、@BeanPropertyを付与しています。

src/main/scala/org/littlewings/javaee7/rest/Book.scala 
package org.littlewings.javaee7.rest

import scala.beans.BeanProperty

case class Book(@BeanProperty isbn: String, @BeanProperty title: String, @BeanProperty price: Int)

ここまでで、JAX-RSリソースクラスの作成は完了です。

Swaggerの設定をする

SwaggerのJAX-RS用のモジュールでは、JAX-RSのApplicationクラスのサブクラス内にSwagger関係の設定を
実装します。

今回作成したのは、こちら。
src/main/scala/org/littlewings/javaee7/rest/JaxrsActivator.scala

package org.littlewings.javaee7.rest

import javax.ws.rs.{ApplicationPath, Path}
import javax.ws.rs.core.Application

import io.swagger.jaxrs.config.BeanConfig
import io.swagger.jaxrs.listing.{ApiListingResource, SwaggerSerializers}
import org.reflections.Reflections

import scala.collection.JavaConverters._

@ApplicationPath("api")
class JaxrsActivator extends Application {
  val beanConfig = new BeanConfig
  beanConfig.setVersion("1.0.0");
  beanConfig.setSchemes(Array("http"))
  beanConfig.setHost("localhost:8080")
  beanConfig.setBasePath(getClass.getAnnotation(classOf[ApplicationPath]).value)
  beanConfig.setResourcePackage(classOf[JaxrsActivator].getPackage.getName)
  beanConfig.setScan(true)

  override def getClasses: java.util.Set[Class[_]] = {
    val resourceClasses: Set[Class[_]] =
      Set.empty ++
        new Reflections(classOf[JaxrsActivator].getPackage.getName)
          .getTypesAnnotatedWith(classOf[Path])
          .asScala
    val swaggerClasses = Set[Class[_]](
      classOf[ApiListingResource],
      classOf[SwaggerSerializers]
    )

    (resourceClasses ++ swaggerClasses).asJava
  }
}

Swaggerが生成する対象のAPI群に燗する基本的な設定は、こちらで行います。

  val beanConfig = new BeanConfig
  beanConfig.setVersion("1.0.0");
  beanConfig.setSchemes(Array("http"))
  beanConfig.setHost("localhost:8080")
  beanConfig.setBasePath(getClass.getAnnotation(classOf[ApplicationPath]).value)
  beanConfig.setResourcePackage(classOf[JaxrsActivator].getPackage.getName)
  beanConfig.setScan(true)

basePathとresourcePackageは、ハードコードしてもよかったのですが、今回は@ApplicationPathの値を引っこ抜いたのと
作成したクラスを全部同じパッケージに置いているのでこんな感じではしょりました。

Application#getClassesでは、SwaggerのApiListingResourceクラスとSwaggerSerializersクラスを入れて返す
必要があります。

  override def getClasses: java.util.Set[Class[_]] = {
    val resourceClasses: Set[Class[_]] =
      Set.empty ++
        new Reflections(classOf[JaxrsActivator].getPackage.getName)
          .getTypesAnnotatedWith(classOf[Path])
          .asScala
    val swaggerClasses = Set[Class[_]](
      classOf[ApiListingResource],
      classOf[SwaggerSerializers]
    )

    (resourceClasses ++ swaggerClasses).asJava
  }

こうなると、自分が作成したリソースクラスも手動で登録することになるので、Reflectionsで
引っこ抜きました。

Reflectionsは、Swagger JAX-RSの依存関係に含まれています。

Jackson Scala Moduleの設定

Case Classを使ったり、SeqをJAX-RSリソースクラスで使ってしまっているので、Jackson 2に対するScala用のモジュールの
設定が必要になります。

こんなクラスを実装して
src/main/scala/org/littlewings/javaee7/rest/ScalaObjectMapperProvider.scala

package org.littlewings.javaee7.rest

import javax.ws.rs.{Consumes, Produces}
import javax.ws.rs.core.MediaType
import javax.ws.rs.ext.{ContextResolver, Provider}

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.scala.DefaultScalaModule

@Provider
@Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON))
class ScalaObjectMapperProvider extends ContextResolver[ObjectMapper] {
  override def getContext(typ: Class[_]): ObjectMapper = {
    val objectMapper = new ObjectMapper
    objectMapper.registerModule(DefaultScalaModule)
    objectMapper
  }
}

Service Providerの設定を用意しておきます。
src/main/resources/META-INF/services/javax.ws.rs.ext.Providers

org.littlewings.javaee7.rest.ScalaObjectMapperProvider

起動クラス

最後に、起動用のクラスを作成します。UndertowにJAX-RSアプリケーションをデプロイします、と。
src/main/scala/org/littlewings/javaee7/rest/Server.scala

package org.littlewings.javaee7.rest

import java.time.LocalDateTime
import javax.servlet.DispatcherType

import io.undertow.Undertow
import io.undertow.servlet.Servlets
import io.undertow.servlet.api.FilterInfo
import org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer

import scala.io.StdIn

object Server {
  def main(args: Array[String]): Unit = {
    val server = new UndertowJaxrsServer
    val deployment = server.undertowDeployment(classOf[JaxrsActivator])
    deployment.setContextPath("")
    deployment.setDeploymentName("resteasy-swagger")
    server.deploy(deployment)

    server.start(Undertow
            .builder
            .addHttpListener(8080, "localhost"))

    StdIn.readLine(s"[${LocalDateTime.now}] Server startup. enter, stop.")

    server.stop()
  }
}

起動すると、Enterを入力するまで浮いているサーバーになります。

確認

それでは、確認してみましょう。Undertowで作ったサーバーを起動します。

> run
[info] Running org.littlewings.javaee7.rest.Server 
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
2 11, 2017 9:13:32 午後 org.jboss.resteasy.spi.ResteasyDeployment processApplication
INFO: RESTEASY002225: Deploying javax.ws.rs.core.Application: class org.littlewings.javaee7.rest.JaxrsActivator
2 11, 2017 9:13:32 午後 org.jboss.resteasy.spi.ResteasyDeployment processApplication
INFO: RESTEASY002200: Adding class resource org.littlewings.javaee7.rest.CalcResource from Application class org.littlewings.javaee7.rest.JaxrsActivator
2 11, 2017 9:13:32 午後 org.jboss.resteasy.spi.ResteasyDeployment processApplication
INFO: RESTEASY002200: Adding class resource org.littlewings.javaee7.rest.BookResource from Application class org.littlewings.javaee7.rest.JaxrsActivator
2 11, 2017 9:13:32 午後 org.jboss.resteasy.spi.ResteasyDeployment processApplication
INFO: RESTEASY002200: Adding class resource io.swagger.jaxrs.listing.ApiListingResource from Application class org.littlewings.javaee7.rest.JaxrsActivator
2 11, 2017 9:13:32 午後 org.jboss.resteasy.spi.ResteasyDeployment processApplication
INFO: RESTEASY002205: Adding provider class io.swagger.jaxrs.listing.SwaggerSerializers from Application class org.littlewings.javaee7.rest.JaxrsActivator
2 11, 2017 9:13:32 午後 org.xnio.Xnio <clinit>
INFO: XNIO version 3.3.6.Final
2 11, 2017 9:13:32 午後 org.xnio.nio.NioXnio <clinit>
INFO: XNIO NIO Implementation Version 3.3.6.Final
[2017-02-11T21:13:33.221] Server startup. enter, stop.

curlで「http://localhost:8080/api/swagger.json」にアクセスすると、生成されたSwagger Specを確認することができます。
URLは、「コンテキストパスまで〜/[BeanConfig#basePath]/swagger.json」みたいですね。

$ curl -i 'http://localhost:8080/api/swagger.json'
HTTP/1.1 200 OK
Connection: keep-alive
Content-Type: application/json
Content-Length: 2087
Date: Sat, 11 Feb 2017 12:13:59 GMT

{"swagger":"2.0","info":{"version":"1.0.0"},"host":"localhost:8080","basePath":"/api","tags":[{"name":"book"},{"name":"calc"}],"schemes":["http"],"paths":{"/book":{"get":{"tags":["book"],"summary":"find all books","description":"","operationId":"fildAll","produces":["application/json"],"parameters":[],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/Seq"}}}}},"/book/{isbn}":{"get":{"tags":["book"],"summary":"find book","description":"","operationId":"find","produces":["application/json"],"parameters":[{"name":"isbn","in":"path","required":true,"type":"string"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/Book"}}}},"put":{"tags":["book"],"summary":"register book","description":"","operationId":"register","consumes":["application/json"],"produces":["application/json"],"parameters":[{"in":"body","name":"body","required":false,"schema":{"$ref":"#/definitions/Book"}}],"responses":{"default":{"description":"successful operation"}}}},"/calc/multiply":{"get":{"tags":["calc"],"summary":"calc multiply","description":"","operationId":"multiply","produces":["text/plain"],"parameters":[{"name":"a","in":"query","required":false,"type":"integer","format":"int32"},{"name":"b","in":"query","required":false,"type":"integer","format":"int32"}],"responses":{"200":{"description":"successful operation","schema":{"type":"integer","format":"int32"}}}}},"/calc/add":{"get":{"tags":["calc"],"summary":"calc add","description":"","operationId":"add","produces":["text/plain"],"parameters":[{"name":"a","in":"query","required":false,"type":"integer","format":"int32"},{"name":"b","in":"query","required":false,"type":"integer","format":"int32"}],"responses":{"200":{"description":"successful operation","schema":{"type":"integer","format":"int32"}}}}}},"definitions":{"Seq":{"type":"object","properties":{"traversableAgain":{"type":"boolean"},"empty":{"type":"boolean"}}},"Book":{"type":"object","properties":{"isbn":{"type":"string"},"title":{"type":"string"},"price":{"type":"integer","format":"int32"}}}}}

Swagger UIを使う

でも、これだとよくわからないのでSwagger UIを使ってビジュアルに見てみます。

GitHub - swagger-api/swagger-ui: Swagger UI is a dependency-free collection of HTML, Javascript, and CSS assets that dynamically generate beautiful documentation from a Swagger-compliant API.

Swagger UIを使う方法はいくつかあるみたいですが、今回はWildFly Swarmに組み込まれたものを使いました。

Swagger UI Server

ダウンロードして、起動。ポートは9000としました。

$ wget http://repo2.maven.org/maven2/org/wildfly/swarm/servers/swagger-ui/2017.2.0/swagger-ui-2017.2.0-swarm.jar
$ java -Dswarm.http.port=9000 -jar swagger-ui-2017.2.0-swarm.jar

この状態で、http://localhost:9000/swagger-ui/にアクセスすると、Swagger UIの画面を見ることができます。

f:id:Kazuhira:20170211212157p:image

ここで、画面上部のテキストフィールドに先ほど生成したSwagger SpecのURL(http://localhost:8080/api/swagger.json)を指定すれば
いいのですが、そのままだと動きません。

f:id:Kazuhira:20170211212529p:image

「CORSの設定してないんじゃない?」って言われているので、設定しましょう。

以下のサンプルにもあったCORSの設定を、ほぼそのまま流用。

swagger-samples/java/java-resteasy at master ? swagger-api/swagger-samples ? GitHub

こうなりました。
src/main/scala/org/littlewings/javaee7/rest/CorsFilter.scala

package org.littlewings.javaee7.rest

import javax.servlet._
import javax.servlet.http.HttpServletResponse

class CorsFilter extends Filter {
  override def init(filterConfig: FilterConfig): Unit = ()

  override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain): Unit = {
    val res = response.asInstanceOf[HttpServletResponse]
    res.addHeader("Access-Control-Allow-Origin", "*")
    res.addHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT")
    res.addHeader("Access-Control-Allow-Headers", "Content-Type")
    chain.doFilter(request, response)
  }

  override def destroy(): Unit = ()
}

UndertowのDeploymentInfoを設定するところで、ServletFilterとして追加します。

    val server = new UndertowJaxrsServer
    val deployment = server.undertowDeployment(classOf[JaxrsActivator])
    deployment.setContextPath("")
    deployment.setDeploymentName("resteasy-swagger")
    deployment.addFilter(Servlets.filter("corsFilter", classOf[CorsFilter]))
    deployment.addFilterUrlMapping("corsFilter", "/*", DispatcherType.REQUEST)
    server.deploy(deployment)

これで気を取り直して確認すると、アクセスできることが確認できます。

f:id:Kazuhira:20170211212919p:image

もう少し確認

表示されているAPIのURLを展開して、もうちょっと確認してみましょう。

例えば、「GET /calc/add」を展開するとこんな表示になるので、テキストフィールドに値を入れて
「Try it out!」を押すと結果の確認(=APIの呼び出し)もできます。

f:id:Kazuhira:20170211213222p:image

また、JSONを扱うようなAPIの場合、ちゃんとアノテーションで設定を入れていると「Model Schema」に構造が表示されるので、
これをクリックすることで入力パラメーターのテンプレートとしても使うことができます。

今回は、これを編集してデータ登録をしてみました。
f:id:Kazuhira:20170211213835p:image

まとめ

SwaggerのJAX-RS用のモジュールを使ってSwagger Specを生成し、Swagger UIで見るということをRESTEasy+Undertowで
行ってみました。

正直、Undertowでてこずるところが多く素直にJava EEサーバーにデプロイしていればもっと簡単だったのかな?と思うところも
ありますが、とりあえず目標達成、と。

今回作成したソースコードは、こちらに置いています。
https://github.com/kazuhira-r/javaee7-scala-examples/tree/master/resteasy-swagger

UndertowでWebSocketを使って遊ぶ

WebSocketを軽く触ってみようと思いまして。

WebSocketのAPIは、Java EE 7にJSR-356があるので、こちらを使って試すことを前提に
考えたいと思います。

The Java Community Process(SM) Program - JSRs: Java Specification Requests - detail JSR# 356

また、実装にはUndertowを使います。これでかなりてこずりましたが…。

Undertow ? JBoss Community

お題は、echo、クライアントとサーバーそれぞれをUndertowとJSR-356のAPIを使って実装します。

準備

プロジェクトは、クライアントとサーバーでそれぞれ別にしました。
build.sbt

name := "undertow-websocket"

lazy val commonSettings = Seq(
  version := "0.0.1-SNAPSHOT",
  organization := "org.littlewings",
  scalaVersion := "2.12.1",
  scalacOptions ++= Seq("-Xlint", "-deprecation", "-unchecked", "-feature"),
  updateOptions := updateOptions.value.withCachedResolution(true)
)

lazy val server = (project in file("server")).
  settings(
    name := "undertow-websocket-server",
    commonSettings,
    libraryDependencies ++= Seq(
      "io.undertow" % "undertow-websockets-jsr" % "1.4.10.Final"
    )
  )

lazy val client = (project in file("client")).
  settings(
    name := "undertow-websocket-client",
    commonSettings,
    libraryDependencies ++= Seq(
      "io.undertow" % "undertow-websockets-jsr" % "1.4.10.Final"
    )
  )

ドキュメントは、Undertowのものを参考に…したかったのですが、ほとんどなにも書いていません。

Undertow

仕方がないので、このあたりを参考に。

java - How to do websockets in embedded undertow? - Stack Overflow

undertow/JSRWebSocketServer.java at 1.4.10.Final ? undertow-io/undertow ? GitHub

https://github.com/undertow-io/undertow/blob/1.4.10.Final/websockets-jsr/src/test/java/io/undertow/websockets/jsr/test/TestMessagesReceivedInOrder.java

そういえば、ドキュメントは1.3系ですけど、今の1.x系は1.4が最新なんですね。

サーバー側の実装

JSR-356のAPIを使ったサーバー側のエンドポイントの実装サンプルは調べるとけっこう出てくるので、簡単に。
server/src/main/scala/org/littlewings/javaee7/websocket/EchoServer.scala

package org.littlewings.javaee7.websocket

import javax.websocket._
import javax.websocket.server.ServerEndpoint

import org.jboss.logging.Logger

@ServerEndpoint("/echo")
class EchoServer {
  val logger: Logger = Logger.getLogger(getClass)

  @OnMessage
  def onMessage(session: Session, msg: String): Unit = {
    logger.infof("receive message = %s", msg)
    session.getBasicRemote.sendText(s"[$msg]")
  }

  @OnOpen
  def onOpen(session: Session, config: EndpointConfig): Unit =
    logger.infof("session open")

  @OnClose
  def onClose(session: Session, reason: CloseReason): Unit =
    logger.infof("close, reason = %s", reason.getReasonPhrase)

  @OnError
  def onError(session: Session, cause: Throwable): Unit =
    logger.errorf(cause, "error")
}

@ServerEndpointで、WebSocketのエンドポイントであることと、パスを指定します。

@ServerEndpoint("/echo")
class EchoServer {

@OnOpen、@OnClose、@OnErrorで、接続時、クローズ時、エラー発生時のイベントを受け取ります。

メッセージは@OnMessageで受け付け、今回はテキストメッセージを「[]」をくっつけて送り返すようにしています。

  @OnMessage
  def onMessage(session: Session, msg: String): Unit = {
    logger.infof("receive message = %s", msg)
    session.getBasicRemote.sendText(s"[$msg]")
  }

続いて、起動クラス。ここが1番てこずったというか、情報がなかったというか。
server/src/main/scala/org/littlewings/javaee7/websocket/WebSocketServer.scala

package org.littlewings.javaee7.websocket

import java.time.LocalDateTime

import io.undertow.servlet.Servlets
import io.undertow.servlet.api.DeploymentInfo
import io.undertow.websockets.jsr.WebSocketDeploymentInfo
import io.undertow.{Handlers, Undertow}

import scala.io.StdIn

object WebSocketServer {
  def main(args: Array[String]): Unit = {
    val webSocketDeploymentInfo =
      new WebSocketDeploymentInfo()
        .addEndpoint(classOf[EchoServer])

    val builder =
      new DeploymentInfo()
        .setClassLoader(getClass.getClassLoader)
        .setContextPath("/")
        .setDeploymentName("myapp.war")
        .addServletContextAttribute(WebSocketDeploymentInfo.ATTRIBUTE_NAME, webSocketDeploymentInfo)

    val manager = Servlets.defaultContainer.addDeployment(builder)
    manager.deploy()

    val path =
      Handlers
        .path
        .addPrefixPath("/", manager.start())

    val undertow =
      Undertow
        .builder
        .addHttpListener(8080, "localhost")
        .setHandler(path)
        .build()

    undertow.start()

    StdIn.readLine(s"[${LocalDateTime.now}] Please Enter, Stop.")

    undertow.stop()
  }
}

JSR-356のAPIを使って実装したWebSocketエンドポイントを、Undertowにどうデプロイすればいいのかがわからずに
だいぶ困っていましたが、最終的にこんな感じになりました。

    val webSocketDeploymentInfo =
      new WebSocketDeploymentInfo()
        .addEndpoint(classOf[EchoServer])

    val builder =
      new DeploymentInfo()
        .setClassLoader(getClass.getClassLoader)
        .setContextPath("/")
        .setDeploymentName("myapp.war")
        .addServletContextAttribute(WebSocketDeploymentInfo.ATTRIBUTE_NAME, webSocketDeploymentInfo)

WebSocketDeploymentInfoを作成してエンドポイントを追加し、DeploymentInfoのServletContextの属性として設定すれば
OKみたいです。

あとはServletを使っていた時みたいにHandlerとして設定して、起動するだけですね。

    val manager = Servlets.defaultContainer.addDeployment(builder)
    manager.deploy()

    val path =
      Handlers
        .path
        .addPrefixPath("/", manager.start())

    val undertow =
      Undertow
        .builder
        .addHttpListener(8080, "localhost")
        .setHandler(path)
        .build()

    undertow.start()

ここまでで、サーバー側はおしまい。

クライアント側

あんまり情報がなさそうな、JSR-356のクライアント側。

こちらは、こんな感じの実装に。
client/src/main/scala/org/littlewings/javaee7/websocket/EchoClient.scala

package org.littlewings.javaee7.websocket

import javax.websocket._

import org.jboss.logging.Logger

@ClientEndpoint
class EchoClient {
  val logger: Logger = Logger.getLogger(classOf[EchoClient])

  @OnMessage
  def onMessage(session: Session, msg: String): Unit = {
    logger.infof("received from server: %s", msg)
  }

  @OnOpen
  def onOpen(session: Session, config: EndpointConfig): Unit =
    logger.infof("session open")

  @OnClose
  def onClose(session: Session, reason: CloseReason): Unit =
    logger.infof("close, reason = %s", reason.getReasonPhrase)

  @OnError
  def onError(session: Session, cause: Throwable): Unit =
    logger.errorf(cause, "error")
}

@ClientEndpointをクラスに付与している以外は、ほとんどサーバー側のエンドポイントと変わりません。

起動用のクラスは、こちら。
client/src/main/scala/org/littlewings/javaee7/websocket/WebSocketClient.scala

package org.littlewings.javaee7.websocket

import java.net.URI
import javax.websocket.{CloseReason, ContainerProvider}

import scala.io.StdIn

object WebSocketClient {
  def main(args: Array[String]): Unit = {
    val container = ContainerProvider.getWebSocketContainer
    val session = container.connectToServer(classOf[EchoClient], URI.create("ws://localhost:8080/echo"))

    Iterator
      .continually(StdIn.readLine("enter text> "))
      .takeWhile(_.trim != "exit")
      .filter(!_.trim.isEmpty)
      .foreach { message => session.getBasicRemote.sendText(message) }

    session.close(new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE, "byebye!!"))
  }
}

ContainerProviderからWebSocketContainerを取得し、あとは作成したクライアントのクラスとサーバー側のエンドポイントを指定して、
WebSocketのSessionを取得します。

あとはプロンプトっぽいものを簡単に作って、入力された文字列をサーバーに送信、「exit」と入力されると
終了するようにしています。

動作確認

それでは、試してみましょう。

まずはサーバー側を起動。

> run
[info] Running org.littlewings.javaee7.websocket.WebSocketServer 
2 11, 2017 4:22:50 午後 org.xnio.Xnio <clinit>
INFO: XNIO version 3.3.6.Final
2 11, 2017 4:22:50 午後 org.xnio.nio.NioXnio <clinit>
INFO: XNIO NIO Implementation Version 3.3.6.Final
2 11, 2017 4:22:50 午後 io.undertow.websockets.jsr.Bootstrap handleDeployment
WARN: UT026009: XNIO worker was not set on WebSocketDeploymentInfo, the default worker will be used
2 11, 2017 4:22:50 午後 io.undertow.websockets.jsr.Bootstrap handleDeployment
WARN: UT026010: Buffer pool was not set on WebSocketDeploymentInfo, the default pool will be used
2 11, 2017 4:22:50 午後 io.undertow.websockets.jsr.ServerWebSocketContainer addEndpointInternal
INFO: UT026003: Adding annotated server endpoint class org.littlewings.javaee7.websocket.EchoServer for path /echo
[2017-02-11T16:22:51.123] Please Enter, Stop.

クライアント側も起動。

> run
[info] Running org.littlewings.javaee7.websocket.WebSocketClient 
2 11, 2017 4:23:09 午後 org.xnio.Xnio <clinit>
INFO: XNIO version 3.3.6.Final
2 11, 2017 4:23:09 午後 org.xnio.nio.NioXnio <clinit>
INFO: XNIO NIO Implementation Version 3.3.6.Final
2 11, 2017 4:23:09 午後 io.undertow.websockets.jsr.ServerWebSocketContainer addEndpointInternal
INFO: UT026004: Adding annotated client endpoint class org.littlewings.javaee7.websocket.EchoClient
2 11, 2017 4:23:10 午後 org.littlewings.javaee7.websocket.EchoClient onOpen
INFO: session open
enter text> 

サーバー側にも、セッションをオープンしたログが出ます。

[2017-02-11T16:22:51.123] Please Enter, Stop.2 11, 2017 4:23:10 午後 org.littlewings.javaee7.websocket.EchoServer onOpen
INFO: session open

クライアント側で、いくつか入力。

enter text> Hello WebSocket!!
enter text> 2 11, 2017 4:23:53 午後 org.littlewings.javaee7.websocket.EchoClient onMessage
INFO: received from server: [Hello WebSocket!!]

enter text> こんにちは、世界
enter text> 2 11, 2017 4:24:00 午後 org.littlewings.javaee7.websocket.EchoClient onMessage
INFO: received from server: [こんにちは、世界]

サーバー側の様子。

2 11, 2017 4:23:53 午後 org.littlewings.javaee7.websocket.EchoServer onMessage
INFO: receive message = Hello WebSocket!!
2 11, 2017 4:23:59 午後 org.littlewings.javaee7.websocket.EchoServer onMessage
INFO: receive message = こんにちは、世界

終了。

enter text> exit
2 11, 2017 4:24:26 午後 org.littlewings.javaee7.websocket.EchoClient onClose
INFO: close, reason = byebye!!

サーバー側も、セッション完了。

2 11, 2017 4:24:26 午後 org.littlewings.javaee7.websocket.EchoServer onClose
INFO: close, reason = byebye!!

OKそうです。

まとめ

Undertowを使って、Java EEのWebSocket API(JSR-356)を試してみました。

Echoくらいなら、JSR-356自体は簡単ですが、Undertowでの設定にだいぶてこずりました…。
まあ、動いたので良しとしましょう。

今回作成したソースコードは、こちらに置いています。
https://github.com/kazuhira-r/javaee7-scala-examples/tree/master/undertow-websocket

2017-02-09

Apache GeodeのServerにJARファイルをデプロイする

Client/Server ModeのApache Geodeでは、Queryなどを実行する時にはJARファイルをServer側にデプロイする
必要があります。

デプロイには、gfshで「deploy」コマンドを実行するのですが、複数のServerに一気にデプロイ
できるみたいだったので、確認してみました。

Deploying Application JARs to Apache Geode Members | Geode Docs

今回は、Locatorひとつ、Server 3つの構成とします。

Serverのひとつでgfshを起動します。

$ bin/gfsh
    _________________________     __
   / _____/ ______/ ______/ /____/ /
  / /  __/ /___  /_____  / _____  / 
 / /__/ / ____/  _____/ / /    / /  
/______/_/      /______/_/    /_/    1.0.0-incubating

Monitor and Manage Apache Geode (incubating)

Locatorに接続。
※「172.19.0.2」は、自分の手元の環境のLocatorのIPアドレスです

gfsh>connect --jmx-manager=172.19.0.2[1099]
Connecting to Manager at [host=172.19.0.2, port=1099] ..
Successfully connected to: [host=172.19.0.2, port=1099]

クラスタメンバー一覧。

gfsh>list members
        Name         | Id
-------------------- | --------------------------------------------------------
locator-geodelocator | 172.19.0.2(locator-geodelocator:36:locator)<ec><v0>:1024
server-a550bf494fc2  | 172.19.0.3(server-a550bf494fc2:149)<v3>:1024
server-7f12f0c9df78  | 172.19.0.4(server-7f12f0c9df78:39)<v4>:1024
server-61b596c14335  | 172.19.0.5(server-61b596c14335:39)<v5>:1024

デプロイされているJARファイルの確認。

gfsh>list deployed
No JAR Files Found

現時点では、なにもありません。

ここに、deployコマンドでJARファイルをデプロイしてみます。デプロイするJARファイルは、「entity.jar」とします。

gfsh>deploy --jar=/path/to/library-dir/entity.jar
      Member        | Deployed JAR | Deployed JAR Location
------------------- | ------------ | --------------------------------------------------------
server-61b596c14335 | entity.jar   | /opt/apache-geode/server-61b596c14335/vf.gf#entity.jar#1
server-7f12f0c9df78 | entity.jar   | /opt/apache-geode/server-7f12f0c9df78/vf.gf#entity.jar#1
server-a550bf494fc2 | entity.jar   | /opt/apache-geode/server-a550bf494fc2/vf.gf#entity.jar#1

一気に3つのServerにデプロイできたようです。

gfsh>list deployed
      Member        |    JAR     | JAR Location
------------------- | ---------- | --------------------------------------------------------
server-61b596c14335 | entity.jar | /opt/apache-geode/server-61b596c14335/vf.gf#entity.jar#1
server-7f12f0c9df78 | entity.jar | /opt/apache-geode/server-7f12f0c9df78/vf.gf#entity.jar#1
server-a550bf494fc2 | entity.jar | /opt/apache-geode/server-a550bf494fc2/vf.gf#entity.jar#1

しかも、各ServerにJARファイルそのものはなくてもよさそうです。
※コマンドを実行するServerにJARファイルがあればよさそう

また、「--dir」オプションでディレクトリ内のJARファイルを一気にデプロイもできるようです。

gfsh>deploy --dir=/path/to/library-dir

Deploying files: entity.jar
Total file size is: 0.00MB

Continue?  (Y/n): y
      Member        | Deployed JAR | Deployed JAR Location
------------------- | ------------ | --------------------------------------------------------
server-42f3246e5a27 | entity.jar   | /opt/apache-geode/server-42f3246e5a27/vf.gf#entity.jar#1
server-a0d45f9f070f | entity.jar   | /opt/apache-geode/server-a0d45f9f070f/vf.gf#entity.jar#1
server-aa3fdc045291 | entity.jar   | /opt/apache-geode/server-aa3fdc045291/vf.gf#entity.jar#1

一気に複数のServerにデプロイできるのは便利ですね、覚えておきましょう。

なお、アンデプロイもできるようです。

途中でServerを増やした場合は?

ちょっと気になったのが、JARファイルをデプロイした後にServerを追加した場合。この場合はどうなるのかなと
思ったので、確認してみました。

今度は、Locatorひとつ、Serverひとつをスタートとします。

gfsh>list members
        Name         | Id
-------------------- | --------------------------------------------------------
locator-geodelocator | 172.19.0.2(locator-geodelocator:36:locator)<ec><v0>:1024
server-e5c9ddb5489f  | 172.19.0.3(server-e5c9ddb5489f:162)<v3>:1024

JARファイルをデプロイします。

gfsh>deploy --jar=./deploy-libraries/entity.jar
      Member        | Deployed JAR | Deployed JAR Location
------------------- | ------------ | --------------------------------------------------------
server-e5c9ddb5489f | entity.jar   | /opt/apache-geode/server-e5c9ddb5489f/vf.gf#entity.jar#1

gfsh>list deployed
      Member        |    JAR     | JAR Location
------------------- | ---------- | --------------------------------------------------------
server-e5c9ddb5489f | entity.jar | /opt/apache-geode/server-e5c9ddb5489f/vf.gf#entity.jar#1

ここで、Serverをもうひとつ追加します。

gfsh>list members
        Name         | Id
-------------------- | --------------------------------------------------------
locator-geodelocator | 172.19.0.2(locator-geodelocator:36:locator)<ec><v0>:1024
server-e5c9ddb5489f  | 172.19.0.3(server-e5c9ddb5489f:162)<v3>:1024
server-4ffac3317ab3  | 172.19.0.4(server-4ffac3317ab3:39)<v4>:1024

JARファイルのデプロイ状況を確認してみます。

gfsh>list deployed
      Member        |    JAR     | JAR Location
------------------- | ---------- | --------------------------------------------------------
server-4ffac3317ab3 | entity.jar | /opt/apache-geode/server-4ffac3317ab3/vf.gf#entity.jar#1
server-e5c9ddb5489f | entity.jar | /opt/apache-geode/server-e5c9ddb5489f/vf.gf#entity.jar#1

追加されたメンバーにも、デプロイされたJARが伝播するみたいですね。

別解)--classpathを使う

デプロイとは少し異なりますが、Serverの起動時に「--classpath」オプションでServerに読み込ませるJARファイルを
追加することができます。

こんな感じです。

gfsh>start server --name=server --classpath=/path/to/library.jar

JARファイルまでのパスは、deployコマンドと異なり絶対パスで記述する必要があります。

複数ある場合は、「:」で区切ります(Windowsだと「;」とかになるのかな?)

gfsh>start server --name=server --classpath=/path/to/library1.jar:/path/to/library2.jar

「*」も使うことができます。

gfsh>start server --name=server --classpath=/path/to/*

deployコマンドと異なり、途中でアンデプロイなどはできないと思いますが、deployコマンドだとServerの
起動とデプロイが別々になってしまうので、ケースによってはこちらを使うのもありでしょう。

1.0.0.INCUBATINGになったSpring Data Geodeを軽く試す

Apache Geodeが1.0.0-incubatingとしてリリースされていますが、合わせてSpring Data Geodeも
1.0.0.INCUBATING-RELEASEになっています。

とはいえ、Spring Data Geode用のページはなくて、相変わらずSpring Data Gemfireな
わけですが。

Spring Data Gemfire

Spring Data Geodeは、Spring Data GemfireのGitHubリポジトリ内で、別ブランチとして
管理されています。

GitHub - spring-projects/spring-data-gemfire at apache-geode

ドキュメント(Spring Data Gemfire)はこちら。

Spring Data GemFire Reference Guide

今回、Apache Geodeが1.0.0-incubatingになって初めてSpring Data Geode含めて扱うので、
まずは軽く試してみたいと思います。

本エントリとしてのお題は、

  • Apache GeodeはClient/Server Modeで動かす
  • Spring Data GeodeをSpring Bootを含めて起動できる環境を作る
  • Spring Data Geodeで、GemfireRepositoryを使ってみる(Queryは含まない)

とします。要するに、Hello Worldレベルですね。

めっちゃハマりましたが。

では、いってみたいと思います。

準備

アプリケーション側(Maven

Mavenの依存関係などは、こんな感じ。

    <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.4.4.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</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.4.4.RELEASEとしました。最新の1.5.1.RELEASEではありません。

また、Spring Data Geodeにはstarterがないので、直接Spring Data Geodeを依存関係に足しています。

Apache GeodeのLocator/Serverの準備

Locatorと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="myRegion" refid="PARTITION_REDUNDANT">
  </region>
</cache>

Locator、Server、そしてRegion。

gfsh>list members
        Name         | Id
-------------------- | --------------------------------------------------------
locator-geodelocator | 172.19.0.2(locator-geodelocator:35:locator)<ec><v0>:1024
server-1fcdc39cf249  | 172.19.0.3(server-1fcdc39cf249:154)<v3>:1024

gfsh>list regions
List of regions
---------------
myRegion

アプリケーションの作成

では、Spring Data Geodeで作成するアプリケーションを作っていきます。

最初に、Apache Geodeに保存するエンティティを作成。お題は書籍とします。
src/main/java/org/littlewings/geode/spring/Book.java

package org.littlewings.geode.spring;

import java.io.Serializable;

import org.springframework.data.annotation.Id;

public class Book implements Serializable {
    private static final long serialVersionUID = 1L;

    @Id
    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は省略
}

Serializableを実装し、Spring Dataの@Idを付与している以外はふつうのJavaBeansです。

次に、このBookクラスを使うRepositoryを作成。今回は、GemfireRepositoryインターフェースを継承しているだけで、
追加メソッドは作成しません。また、@Regionでエンティティの保存先のRegionを指定しておきます。
src/main/java/org/littlewings/geode/spring/BookRepository.java

package org.littlewings.geode.spring;

import org.springframework.data.gemfire.mapping.Region;
import org.springframework.data.gemfire.repository.GemfireRepository;

@Region("myRegion")
public interface BookRepository extends GemfireRepository<Book, String> {
}

Spring Data Geodeの設定。@EnableGemfireRepositoriesアノテーションを付与するとともに、ClientCacheとRegionの
Bean定義を行います。
src/main/java/org/littlewings/geode/spring/GeodeConfig.java

package org.littlewings.geode.spring;

import org.apache.geode.cache.client.ClientCache;
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.client.ClientRegionFactoryBean;
import org.springframework.data.gemfire.repository.config.EnableGemfireRepositories;

@Configuration
@EnableGemfireRepositories
public class GeodeConfig {
    @Bean
    public ClientCacheFactoryBean geodeCache() throws Exception {
        ClientCacheFactoryBean clientCacheFactory = new ClientCacheFactoryBean();
        clientCacheFactory.setCacheXml(new ClassPathResource("client-cache.xml"));
        clientCacheFactory.afterPropertiesSet();
        return clientCacheFactory;
    }

    @Bean
    public ClientRegionFactoryBean<String, Book> region(ClientCache cache) throws Exception {
        ClientRegionFactoryBean<String, Book> clientRegionFactory = new ClientRegionFactoryBean<>();
        clientRegionFactory.setCache(cache);
        clientRegionFactory.setRegionName("myRegion");
        clientRegionFactory.afterPropertiesSet();
        return clientRegionFactory;
    }
}

Cacheの設定は、Cache XMLで指定するようにしました。

        clientCacheFactory.setCacheXml(new ClassPathResource("client-cache.xml"));

また、RegionのBean定義はしておかないと、RepositoryがRegionを参照できなくてエラーになります…。
Cache XMLを使用しているからといって、Region自体のBeanの定義は省略できず行っておく必要があります、と。

ところで、Apache Geodeは1.0.0-incubatingになるに伴いパッケージ名が「org.apache.geode」になったの
ですが、Spring Data Geodeは未だにいろいろとGemfireなので混乱しますね?

Spring Boot有効化のためのクラス。特にすることはありません。
src/main/java/org/littlewings/geode/spring/App.java

package org.littlewings.geode.spring;

import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.gemfire.repository.config.EnableGemfireRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@SpringBootApplication
public class App {
}

ClientCacheの設定

先ほどBean定義で参照していた、Cache XMLは以下のように定義しました。
src/test/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="myRegion" refid="PROXY">
        <region-attributes pool-name="client-pool"/>
    </region>
</client-cache>

単純にServerに接続しにいくRegion定義です。

テストコード

では、テストコードを書いて確認してみます。

テストコードの雛形としては、以下のようなものを用意。
src/test/java/org/littlewings/geode/spring/SimpleDataGeodeTest.java

package org.littlewings.geode.spring;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

import org.apache.geode.cache.client.ServerOperationException;
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.dao.DataAccessResourceFailureException;
import org.springframework.test.context.junit4.SpringRunner;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

@RunWith(SpringRunner.class)
@SpringBootTest
public class SimpleDataGeodeTest {
    @Autowired
    BookRepository bookRepository;

    // ここに、テストを書く!
}

作成したRepositoryは、@Autowiredでインジェクションしています。

まずは基本的なCRUD

    @Test
    public void gettingStarted() {
        Book book = new Book("978-4798142470", "Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発", 4320);

        // 保存
        bookRepository.save(book);

        // 1件取得
        Book foundBook = bookRepository.findOne("978-4798142470");
        assertThat(foundBook.getTitle())
                .isEqualTo("Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発");
        assertThat(foundBook.getPrice())
                .isEqualTo(4320);

        // 削除
        bookRepository.delete("978-4798142470");

        // 確認
        assertThat(bookRepository.findOne("978-4798142470"))
                .isNull();
        assertThat(bookRepository.count())
                .isZero();
    }

findAllなどの、Iterableを使うもの。

    @Test
    public void iterable() {
        List<Book> books =
                Arrays.asList(
                        new Book("978-4798142470", "Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発", 4320),
                        new Book("978-4774182179", "[改訂新版]Spring入門 ――Javaフレームワーク・より良い設計とアーキテクチャ", 4104),
                        new Book("978-4777519699", "はじめてのSpring Boot―スプリング・フレームワークで簡単Javaアプリ開発", 2700)
                );

        // 複数保存
        bookRepository.save(books);

        // 全件保存
        Iterable<Book> foundBooks = bookRepository.findAll();
        List<Book> foundBooksAsList = (List<Book>) foundBooks;

        assertThat(foundBooksAsList)
                .hasSize(3);
        assertThat(foundBooksAsList.stream().map(b -> b.getIsbn()).collect(Collectors.toList()))
                .containsAll(books.stream().map(b -> b.getIsbn()).collect(Collectors.toList()));

        // 削除
        bookRepository.delete(books);

        // 確認
        assertThat(bookRepository.count())
                .isZero();
    }

deleteAllはサポートしていなかったりします。

    @Test
    public void deleteAllUnsupported() {
        assertThatThrownBy(() -> bookRepository.deleteAll())
                .isInstanceOf(DataAccessResourceFailureException.class)
                .hasCauseInstanceOf(ServerOperationException.class);  // さらにcauseを見るとUnsupportedOperationException
    }

とりあえず、OKそうですね。

ハマったこと

それはもうハマりました。

まあ、ハマったといっても1点だけで、Spring Bootを1.5.1.RELEASEでSpring Data Geodeを合わせようとするとRepositoryにうまく
DIできなくなるようで、こんな感じでコケてしまいます…。

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'region' defined in class path resource [org/littlewings/geode/spring/GeodeConfig.class]: Unsatisfied dependency expressed through method 'region' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'bookRepository': Could not resolve matching constructor (hint: specify index/type/name arguments for simple parameters to avoid type ambiguities)

Spring Bootのバージョンを下げるとうまく動くのですが、それに気づくまでにかなり時間がかかりました…。
なお、原因自体はちゃんと追えてません…。

というか

Spring Data GeodeをSpring Bootのstarterに加えて欲しいんですけど、どうやらそうもいかない様子。

Add auto-configuration support for Spring Data Geode Repositories. by jxblum ? Pull Request #6952 ? spring-projects/spring-boot ? GitHub

Add auto-configuration support for Apache Geode as a caching provider. by jxblum ? Pull Request #6967 ? spring-projects/spring-boot ? GitHub

うーん。

ホント、Spring Data Geodeって今後どうなるんでしょ…。

まとめ

とりあえず触りだけですが、Spring Data Geodeの1.0.0.INCUBATING-RELEASEを試してみました。

Spring BootとSpring Data GeodeのRepositoryの関連にだいぶハマりましたが、最低限動いたので…とは言いきりにくい
ところがあります。

SpringのCache Abstractionとも、そのうち組み合わせてみようかな?(こっちは、Spring Boot 1.5.1.RELEASEで
動いて欲しい)
あと、Queryとかも試しておきたいですね。

まずはこんなところで。