CLOVER🍀

That was when it all began.

Bean ValidationのConvertGroupを試してみる

そろそろ区切りにしようかなと思う、Bean Validationネタ。今回は、@ConvertGroupを試してみました。

Group conversion
http://docs.jboss.org/hibernate/validator/5.1/reference/en-US/html_single/#section-group-conversion

これを使うと、バリデーション時にGroupの変換ができる(違うGroupを指定したことにできる)みたいです。@Validと一緒に使うのだとか。

試してみましょう。

準備

ビルド定義。
build.sbt

name := "bean-validation-group-conversion"

version := "0.0.1-SNAPSHOT"

scalaVersion := "2.11.7"

organization := "org.littlewings"

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

updateOptions := updateOptions.value.withCachedResolution(true)

fork in Test := true

val tomcatVersion = "8.0.23"
val resteasyVersion = "3.0.11.Final"
val weldServletVersion = "2.2.13.Final"
val scalaTestVersion = "2.2.5"

libraryDependencies ++= Seq(
  "org.apache.tomcat.embed" % "tomcat-embed-core" % tomcatVersion,
  "org.apache.tomcat.embed" % "tomcat-embed-jasper" % tomcatVersion,
  "org.apache.tomcat.embed" % "tomcat-embed-logging-juli" % tomcatVersion,
  "org.jboss.resteasy" % "resteasy-servlet-initializer" % resteasyVersion,
  "org.jboss.resteasy" % "resteasy-cdi" % resteasyVersion,
  "org.jboss.resteasy" % "resteasy-validator-provider-11" % resteasyVersion,
  "org.jboss.resteasy" % "resteasy-jackson2-provider" % resteasyVersion,
  "org.jboss.resteasy" % "resteasy-client" % resteasyVersion % "test",
  "org.jboss.weld.servlet" % "weld-servlet" % weldServletVersion,
  "org.scalatest" %% "scalatest" % scalaTestVersion % "test"
)

今回は、Java SEでBean Validationを使うのではなく、JAX-RS(RESTEasy)と合わせてみました。実行は、組み込みTomcatで行います。…ムダにCDI入り。

beans.xmlも置いておきます。
src/main/resources/META-INF/beans.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                           http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
       bean-discovery-mode="annotated">
</beans>

テストコードとJAX-RSで使うコードの用意

テストに、組み込みTomcatJAX-RSを使います。まずはJAX-RSの有効化。
src/main/scala/org/littlewings/javaee7/rest/JaxrsApplication.scala

package org.littlewings.javaee7.rest

import javax.ws.rs.ApplicationPath
import javax.ws.rs.core.Application

@ApplicationPath("rest")
class JaxrsApplication extends Application

JAX-RSリソースクラス。
src/main/scala/org/littlewings/javaee7/rest/ValidationResource.scala

package org.littlewings.javaee7.rest

import javax.enterprise.context.RequestScoped
import javax.validation.Valid
import javax.validation.groups.{ConvertGroup, Default}
import javax.ws.rs.core.MediaType
import javax.ws.rs.{POST, Path, Produces}

import org.littlewings.javaee7.beanvalidation.{GroupA, User, User2}

@Path("validation")
@RequestScoped
class ValidationResource {
  // 後で
}

リソースクラスの中身は後で書きます。

テスト時に、組み込みTomcatを起動するTrait。
src/test/scala/org/littlewings/javaee7/beanvalidation/EmbeddedTomcatCdiSupport.scala

package org.littlewings.javaee7.beanvalidation

import java.io.File

import org.apache.catalina.startup.Tomcat
import org.scalatest.{BeforeAndAfterAll, Suite}

trait EmbeddedTomcatCdiSupport extends Suite with BeforeAndAfterAll {
  protected val port: Int = 8080
  protected val tomcat: Tomcat = new Tomcat
  protected val baseDir: File = createTempDir("tomcat", port)
  protected val docBaseDir: File = createTempDir("tomcat-docbase", port)

  override def beforeAll(): Unit = {
    tomcat.setPort(port)

    tomcat.setBaseDir(baseDir.getAbsolutePath)

    val context =
      tomcat.addWebapp("", docBaseDir.getAbsolutePath)

    context.addParameter("org.jboss.weld.environment.servlet.archive.isolation", "false")
    context.addParameter("resteasy.injector.factory", "org.jboss.resteasy.cdi.CdiInjectorFactory")

    tomcat.start()
  }

  override def afterAll(): Unit = {
    tomcat.stop()
    tomcat.destroy()

    deleteDirs(baseDir)
    deleteDirs(docBaseDir)
  }

  private def createTempDir(prefix: String, port: Int): File = {
    val tempDir = File.createTempFile(s"${prefix}.", s".${port}")
    tempDir.delete()
    tempDir.mkdir()
    tempDir.deleteOnExit()
    tempDir
  }

  private def deleteDirs(file: File): Unit = {
    file
      .listFiles
      .withFilter(f => f.getName != "." && f.getName != "..")
      .foreach {
      case d if d.isDirectory => deleteDirs(d)
      case f => f.delete()
    }

    file.delete()
  }
}

テストコードの雛形。
src/test/scala/org/littlewings/javaee7/beanvalidation/ConvertGroupSpec.scala

package org.littlewings.javaee7.beanvalidation

import javax.ws.rs.client.{ClientBuilder, Entity}
import javax.ws.rs.core.Response

import org.scalatest.FunSpec
import org.scalatest.Matchers._

class ConvertGroupSpec extends FunSpec with EmbeddedTomcatCdiSupport {
  describe("ConvertGroup Spec") {
    // ここに、テストを書く!
  }
}

テストは、JAX-RS Client APIを使います。

@ConvertGroupを使ったコードを書く

それでは、@ConvertGroupを使ったコードを書いてみます。

今回は、ネストしたJavaBeansに対してGroupを切り替えていくことを考えます。

ここでは、以下のようなGroupおよびDefaultを使っていきます。
src/main/scala/org/littlewings/javaee7/beanvalidation/Groups.scala

package org.littlewings.javaee7.beanvalidation

trait GroupA

trait GroupB

trait GroupC
普通にJavaBeansを定義してみる

まずは、JavaBeans内では@ConvertGroupを使わないパターンで書いてみます。

まあ、単純なケースですね。
src/main/scala/org/littlewings/javaee7/beanvalidation/User.scala

package org.littlewings.javaee7.beanvalidation

import javax.validation.Valid
import javax.validation.constraints.{Max, Pattern}
import javax.validation.groups.{ConvertGroup, Default}

import scala.beans.BeanProperty

class User {
  @BeanProperty
  @Valid
  var name: Name = _

  @BeanProperty
  @Max.List(
    Array(
      new Max(value = 12),
      new Max(value = 32, groups = Array(classOf[GroupA]))
    )
  )
  var age: Int = _
}

class Name {
  @BeanProperty
  @Pattern.List(
    Array(
      new Pattern(regexp = "^(カツオ|ワカメ)$"),
      new Pattern(regexp = "^(サザエ|マスオ|タラオ)$", groups = Array(classOf[GroupA]))
    )
  )
  var first: String = _

  @BeanProperty
  @Pattern.List(
    Array(
      new Pattern(regexp = "^磯野$"),
      new Pattern(regexp = "^フグ田$", groups = Array(classOf[GroupA]))
    )
  )
  var last: String = _
}

それぞれ、DefaultとGroupAに対して、バリデーションを定義しています。

ちなみに、こういうのは

  @Max.List(
    Array(
      new Max(value = 12),
      new Max(value = 32, groups = Array(classOf[GroupA]))
    )
  )

Javaで書くとこういう意味になります。

  @Max.List({@Max(value = 12), @Max(value = 32, groups = {GroupA.class})})

Scalaアノテーションがネストする場合、書き方がちょっと…。

では、これに対するJAX-RSリソースクラス側の定義。

  @Path("default")
  @POST
  @Produces(Array(MediaType.APPLICATION_JSON))
  def defaultGroup(@Valid user: User): User =
    user

@Valid付き。

この場合、ネストしたJavaBeansも含めてDefaultとしてバリデーションされます。

    it("Default Group, valid") {
      val user = new User
      user.name = new Name
      user.name.first = "カツオ"
      user.name.last = "磯野"
      user.age = 12

      val client = ClientBuilder.newBuilder.build

      try {
        val response =
          client
            .target("http://localhost:8080/rest/validation/default")
            .request
            .post(Entity.json(user))

        response.getStatus should be(Response.Status.OK.getStatusCode)
        val responseEntity = response.readEntity(classOf[User])
        responseEntity.name.first = "カツオ"
        responseEntity.name.last = "磯野"

        response.close()
      } finally {
        client.close()
      }
    }

これはDefaultとしてはNG(lastnameが「磯野」じゃない)。

      val user = new User
      user.name = new Name
      user.name.first = "カツオ"
      user.name.last = "磯野?"
      user.age = 12

      val client = ClientBuilder.newBuilder.build

      try {
        val response =
          client
            .target("http://localhost:8080/rest/validation/default")
            .request
            .post(Entity.json(user))

        response.getStatus should be(Response.Status.BAD_REQUEST.getStatusCode)

これもDefaultとしてはNG(ageが大きい)。

      val user = new User
      user.name = new Name
      user.name.first = "カツオ"
      user.name.last = "磯野"
      user.age = 20

      val client = ClientBuilder.newBuilder.build

      try {
        val response =
          client
            .target("http://localhost:8080/rest/validation/default")
            .request
            .post(Entity.json(user))

        response.getStatus should be(Response.Status.BAD_REQUEST.getStatusCode)

では、JAX-RSリソース側で@ConvertGroupを付与するようにしてみます。

  @Path("convertGroupA")
  @POST
  @Produces(Array(MediaType.APPLICATION_JSON))
  def convertGroupA(@Valid @ConvertGroup(from = classOf[Default], to = classOf[GroupA]) user: User): User =
    user

@Validは、JAX-RSでの利用に限らず、合わせて使うことになるみたいです。

すると、バリデーションの内容がGroupAに切り替わります。

      val user = new User
      user.name = new Name
      user.name.first = "サザエ"
      user.name.last = "フグ田"
      user.age = 24

      val client = ClientBuilder.newBuilder.build

      try {
        val response =
          client
            .target("http://localhost:8080/rest/validation/convertGroupA")
            .request
            .post(Entity.json(user))

        response.getStatus should be(Response.Status.OK.getStatusCode)
        val responseEntity = response.readEntity(classOf[User])
        responseEntity.name.first = "サザエ"
        responseEntity.name.last = "フグ田"

        response.close()
      } finally {
        client.close()
      }

全部書くと長くなるので、NGのケースは省略します…。

ネストしたJavaBeansにも、@ConvertGroupをつけてみる

続いて、ネストしたJavaBeansにも@ConvertGroupをつけてみます。ネストしたJavaBeansに付与されたGroupが変えられないとか、呼び出し元に意識して欲しくないとかそういう時に使うといいのかな?

class User2 {
  @BeanProperty
  @Valid
  @ConvertGroup.List(
    Array(
      new ConvertGroup(from = classOf[Default], to = classOf[GroupB]),
      new ConvertGroup(from = classOf[GroupA], to = classOf[GroupC])
    )
  )
  var name: Name2 = _

  @BeanProperty
  @Max.List(
    Array(
      new Max(value = 12),
      new Max(value = 32, groups = Array(classOf[GroupA]))
    )
  )
  var age: Int = _
}

class Name2 {
  @BeanProperty
  @Pattern.List(
    Array(
      new Pattern(regexp = "^(カツオ|ワカメ)$", groups = Array(classOf[GroupB])),
      new Pattern(regexp = "^(サザエ|マスオ|タラオ)$", groups = Array(classOf[GroupC]))
    )
  )
  var first: String = _

  @BeanProperty
  @Pattern.List(
    Array(
      new Pattern(regexp = "^磯野$", groups = Array(classOf[GroupB])),
      new Pattern(regexp = "^フグ田$", groups = Array(classOf[GroupC]))
    )
  )
  var last: String = _
}

ネストしたName2クラスには、GroupB、GroupCのGroupが付与されています。で、外側のUser2はDefaultとGroupAを使うようになっています。

そこで、以下のように@ConvertGroupを付与して、Default時にName2はGroupBとしてバリデーションされるように、GroupAの時はGroupCとしてバリデーションされるようにしてみます。

  @BeanProperty
  @Valid
  @ConvertGroup.List(
    Array(
      new ConvertGroup(from = classOf[Default], to = classOf[GroupB]),
      new ConvertGroup(from = classOf[GroupA], to = classOf[GroupC])
    )
  )
  var name: Name2 = _

JAX-RSリソース側。まずはDefaultから。

  @Path("default2")
  @POST
  @Produces(Array(MediaType.APPLICATION_JSON))
  def default2(@Valid user: User2): User2 =
    user

テストがOKになるのは、User2がDefault、Name2がGroupBの時だけです。

    it("Default Group 2, valid") {
      val user = new User2
      user.name = new Name2
      user.name.first = "カツオ"
      user.name.last = "磯野"
      user.age = 12

      val client = ClientBuilder.newBuilder.build

      try {
        val response =
          client
            .target("http://localhost:8080/rest/validation/default2")
            .request
            .post(Entity.json(user))

        response.getStatus should be(Response.Status.OK.getStatusCode)
        val responseEntity = response.readEntity(classOf[User2])
        responseEntity.name.first = "カツオ"
        responseEntity.name.last = "磯野"

        response.close()
      } finally {
        client.close()
      }
    }

Name2側が、GroupBとして扱われていることになります。

では、DefaultをGroupAとして扱うようなメソッドを、JAX-RS側に追加します。

  @Path("convertGroupA2")
  @POST
  @Produces(Array(MediaType.APPLICATION_JSON))
  def convertGroupA2(@Valid @ConvertGroup(from = classOf[Default], to = classOf[GroupA]) user: User2): User2 =
    user

今度は、User2がGroupAとして、Name2がGroupCとしてバリデーションされるようになります。

    it("GroupA 2, valid") {
      val user = new User2
      user.name = new Name2
      user.name.first = "サザエ"
      user.name.last = "フグ田"
      user.age = 24

      val client = ClientBuilder.newBuilder.build

      try {
        val response =
          client
            .target("http://localhost:8080/rest/validation/convertGroupA2")
            .request
            .post(Entity.json(user))

        response.getStatus should be(Response.Status.OK.getStatusCode)
        val responseEntity = response.readEntity(classOf[User2])
        responseEntity.name.first = "サザエ"
        responseEntity.name.last = "フグ田"

        response.close()
      } finally {
        client.close()
      }
    }

動きましたね!

おわりに

今回は、@ConvertGroupで@Valid使用時のGroupの読み替えを試してみました。Groupまわりの話が、またひとつわかった気がします。

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

省略した、バリデーションがNGになるケースも入っています。

EhcacheとJGroupsで、Cacheのレプリケーションをしてみる

先日、こんなエントリが書かれました。

Ehcache 使い方メモ
http://qiita.com/opengl-8080/items/f42664769740674176ff

こちら、すごいですね。メモと題しているものの、日本語情報でここまでEhcacheについて書かれたのほとんど見たことないかも…。

この中で、RMIを使ったレプリケーションも紹介されていました。レプリケーションについてはそういえばやったことがないので、これを機にということで試してみました。

Ehcacheのレプリケーション

Ehcacheでは、Cacheのレプリケーションの方法として、以下の3つから選ぶことができるようです。

Ehcache Replication Guide
http://ehcache.org/generated/2.10.0/html/ehc-all/#page/Ehcache_Documentation_Set%2Fto-title_ehcache_replication_guide.html%23

  • RMI
  • JGroups
  • JMS

このうち、RMIはEhcacheのコアに入っており、それ以外はオプション扱いです。

ちょっと見てみた感じ、更新されていない感がすごいです…。

ehcache-jgroupsreplication(2012/7/26最終アップデート、Ehcache 2.5.2、JGroups 3.1.0.Finalに依存)
http://search.maven.org/#artifactdetails|net.sf.ehcache|ehcache-jgroupsreplication|1.7|jar

ehcache-jmsreplication(2013/8/13最終アップデート、Ehcache 2.5.2以上、JMS 1.1に依存)
http://search.maven.org/#artifactdetails|net.sf.ehcache|ehcache-jmsreplication|0.5|jar

という状態ですが、今回はムリヤリJGroupsを使ってみました(笑)。

なので、ここからの内容はご参考(?)までに。

Maven依存関係

Maven依存関係は、以下のように定義。

        <dependency>
            <groupId>net.sf.ehcache</groupId>
            <artifactId>ehcache</artifactId>
            <version>2.10.0</version>
        </dependency>
        <dependency>
            <groupId>net.sf.ehcache</groupId>
            <artifactId>ehcache-jgroupsreplication</artifactId>
            <version>1.7</version>
        </dependency>
        <dependency>
            <groupId>org.jgroups</groupId>
            <artifactId>jgroups</artifactId>
            <version>3.6.4</version>
        </dependency>

ムダにJGrouopsを最新化してみました。

Ehcacheの設定

こちらを参考に、Ehcacheの設定をしてみます。JGroupsの設定も含めます。

Example Configuration using UDP Multicast
http://ehcache.org/generated/2.10.0/html/ehc-all/index.html#page/Ehcache_Documentation_Set%2Fco-jgrp_example_config_using_udp_multicast.html%23

Configuring CacheReplicators
http://ehcache.org/generated/2.10.0/html/ehc-all/index.html#page/Ehcache_Documentation_Set%2Fco-jgrp_configuring_cache.html%23

なんですけど、ムダにちょっと新しめの設定にしてみました。
src/main/resources/ehcache.xml

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="ehcache.xsd"
         updateCheck="false" monitoring="autodetect"
         dynamicConfig="false">
    <cache name="replicatedCache"
           maxEntriesLocalHeap="10000"
           eternal="true"
           timeToIdleSeconds="0"
           timeToLiveSeconds="0"
           overflowToDisk="false"
           memoryStoreEvictionPolicy="LRU">
        <cacheEventListenerFactory
                class="net.sf.ehcache.distribution.jgroups.JGroupsCacheReplicatorFactory"
                properties="replicateAsynchronously=true, replicatePuts=true,
       replicateUpdates=true, replicateUpdatesViaCopy=false, replicateRemovals=true"/>
    </cache>

    <cacheManagerPeerProviderFactory
            class="net.sf.ehcache.distribution.jgroups.JGroupsCacheManagerPeerProviderFactory"
            properties="connect=UDP(mcast_addr=231.12.21.132;mcast_port=45566;ip_ttl=2):
            PING:
            MERGE3(min_interval=10000;max_interval=30000):
            FD_SOCK:
            FD_ALL(timeout=60000;interval=15000;timeout_check_interval=5000):
            VERIFY_SUSPECT(timeout=5000):
            pbcast.NAKACK2(xmit_interval=1000;xmit_table_num_rows=50;xmit_table_msgs_per_row=1024;xmit_table_max_compaction_time=30000;resend_last_seqno=true):
            UNICAST3(xmit_interval=500;xmit_table_num_rows=50;xmit_table_msgs_per_row=1024;xmit_table_max_compaction_time=30000;max_msg_batch_size=100;conn_expiry_timeout=0):
            pbcast.STABLE(stability_delay=500;desired_avg_gossip=5000;max_bytes=1M):
            pbcast.GMS(print_local_addr=true):
            UFC(max_credits=2m;min_threshold=0.40):
            MFC(max_credits=2m;min_threshold=0.40):
            FRAG2"
            propertySeparator="::"
            />
</ehcache>

Nodeの見つけ方は、マルチキャストで行うようにしています。Node間の通信方法はUDPです。

動作確認用のコード

確認用のコードとしては、こんなのを用意。ちょっとした対話形式ですね。
src/main/java/org/littlewings/ehcache/EhcacheReplicator.java

package org.littlewings.ehcache;

import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;

public class EhcacheReplicator {
    public static void main(String... args) {
        CacheManager cacheManager = CacheManager.newInstance();

        try {
            Cache cache = cacheManager.getCache("replicatedCache");

            while (true) {
                String line = System.console().readLine("Command> ");

                if (line == null) {
                    continue;
                } else if ("exit".equals(line)) {
                    break;
                } else {
                    try {
                        String[] tokens = line.split(" +");
                        String key;
                        String value;

                        switch (tokens[0]) {
                            case "get":
                                key = tokens[1];
                                Element element = cache.get(key);

                                if (element != null) {
                                    System.out.printf("get key[%s] => %s%n", key, element.getObjectValue());
                                } else {
                                    System.out.printf("get key[%s] not found.%n", key);
                                }

                                break;
                            case "put":
                                key = tokens[1];
                                value = tokens[2];

                                cache.put(new Element(key, value));

                                System.out.printf("putted key[%s] => %s%n", key, value);

                                break;
                            case "size":
                                System.out.printf("Cache size: %d%n", cache.getSize());
                                break;
                            default:
                                System.out.printf("Unknown command[%s]%n", tokens[0]);
                                break;
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        } finally {
            cacheManager.shutdown();
        }
    }
}

実行してみる

それでは、こちらを動かしてみましょう。

まずは、2つNodeを起動してみます。

## Node 1
$ mvn compile exec:java -Dexec.mainClass=org.littlewings.ehcache.EhcacheReplicator

[INFO] --- exec-maven-plugin:1.4.0:java (default-cli) @ ehcache-replication-jgroups ---
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.
6 27, 2015 11:18:05 午後 org.jgroups.protocols.UDP setBufferSize
警告: JGRP000015: the receive buffer of socket MulticastSocket was set to 500KB, but the OS only allocated 212.99KB. This might lead to performance problems. Please set your max receive buffer in the OS correctly (e.g. net.core.rmem_max on Linux)

-------------------------------------------------------------------
GMS: address=xxxxx-21169, cluster=EH_CACHE, physical address=fe80:0:0:0:20c:29ff:fe47:1a6e%2:47357
-------------------------------------------------------------------
Command> 

もうひとつ。

## Node 2
$ mvn compile exec:java -Dexec.mainClass=org.littlewings.ehcache.EhcacheReplicator

[INFO] --- exec-maven-plugin:1.4.0:java (default-cli) @ ehcache-replication-jgroups ---
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.
6 27, 2015 11:18:36 午後 org.jgroups.protocols.UDP setBufferSize
警告: JGRP000015: the receive buffer of socket MulticastSocket was set to 500KB, but the OS only allocated 212.99KB. This might lead to performance problems. Please set your max receive buffer in the OS correctly (e.g. net.core.rmem_max on Linux)

-------------------------------------------------------------------
GMS: address=xxxx-13394, cluster=EH_CACHE, physical address=fe80:0:0:0:20c:29ff:fe47:1a6e%2:34282
-------------------------------------------------------------------
Command> 

起動しました。メンバーとして認識されたかどうか怪しいですが、ちょっと試してみます。

Node 1でデータ登録。

Command> put key1 value1
putted key[key1] => value1

Node 2で参照。

Command> get key1
get key[key1] => value1

Node 2でデータ登録。

Command> put key2 value2 
putted key[key2] => value2

Node 1で参照。

Command> get key2
get key[key2] => value2

Node 1でCacheのサイズ確認。

Command> size
Cache size: 2

ちゃんとレプリケーションされていそうですね。

もうひとつNodeを加えると…?

ここで、もうひとつCacheのNodeを起動してみます。

## Node 3
$ mvn compile exec:java -Dexec.mainClass=org.littlewings.ehcache.EhcacheReplicator

[INFO] --- exec-maven-plugin:1.4.0:java (default-cli) @ ehcache-replication-jgroups ---
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.
6 27, 2015 11:21:35 午後 org.jgroups.protocols.UDP setBufferSize
警告: JGRP000015: the receive buffer of socket MulticastSocket was set to 500KB, but the OS only allocated 212.99KB. This might lead to performance problems. Please set your max receive buffer in the OS correctly (e.g. net.core.rmem_max on Linux)

-------------------------------------------------------------------
GMS: address=xxxxx-44544, cluster=EH_CACHE, physical address=fe80:0:0:0:20c:29ff:fe47:1a6e%2:52716
-------------------------------------------------------------------
Command> 

なんと、この新しいNodeにはこれまでのデータは入っていないようです。

Command> size
Cache size: 0
Command> get key1
get key[key1] not found.
Command> get key2
get key[key2] not found.

ここでめげずに、Node 3で新しいデータを登録してみます。

Command> put key3 value3
putted key[key3] => value3

Node 2から参照。

Command> get key3
get key[key3] => value3

Node 1でデータ登録。

Command> put key4 value4
putted key[key4] => value4

Node 3で参照。

Command> get key4
get key[key4] => value4

新しく登録した分については、共有されていますね。

これはどういうことか?

というわけで、新しいNodeについては起動前に登録されていたデータは参照不可なわけですが、この設定を見るとEventListenerでレプリケーションしてそうな感じですよね。

        <cacheEventListenerFactory
                class="net.sf.ehcache.distribution.jgroups.JGroupsCacheReplicatorFactory"
                properties="replicateAsynchronously=true, replicatePuts=true,
       replicateUpdates=true, replicateUpdatesViaCopy=false, replicateRemovals=true"/>

これなら、起動以降のイベント分しか反映されないかも…。

まとめ

かなりムリヤリですが、EhcacheのJGroupsを使ったレプリケーションを試してみました。

そもそもメンテナンスされてなさそうなモジュールですし(だったら、なんでドキュメントに残っているんだろう…)、使うのはオススメできない気がします。

また、新規に登録されたデータしか参照できないのは、RMIでもEventListnerで頑張る感じみたいなのできっと同じ挙動なのでしょう(設定こそ違いますが、たぶんJMSも…)。

Configuring the CacheManagerPeerListener
http://ehcache.org/generated/2.10.0/html/ehc-all/#page/Ehcache_Documentation_Set%2Fco-rmi_configuring_cache_replicators.html%23

新しいNode追加時に、同期してくれないと困らない…のかな?あくまでCacheなので、そういう割り切りなのかもしれません。

でも、複数Nodeでデータを共有したいのなら、Ehcacheじゃなくて他の分散Cache製品を選びそうな気がしますね…。個人的には、Ehcacheは単一のJavaVMでのCacheで使うイメージが強いです。それ以上なら、Ehcacheの方向でいくのなら、BigMemory MaxとかGoを選ぶんじゃないでしょうか?(商用ですし、機能は確認してないですけど…)。

BigMemory Max
http://terracotta.org/products/bigmemorymax

BigMemory Go
http://terracotta.org/products/bigmemorygo