CLOVER🍀

That was when it all began.

Infinispan Embedded Mode/Remote(Hot Rod Client) × Spring Cache

Infinispanには、Springとの連携(Spring Cache Provider)がけっこう前からあります。Infinispan 5.0.0.Finalの時点では、すでにあったみたいです。

とはいえ、しばらくEmbedded Modeのみだったのですが、8.1.0.FinalでClient/Server(Hot Rod Client)での連携が追加され、最近Infinispan側のSpring連携も進んでいるようなので、ちょっと試してみることにしました。

9.0.0.Finalでは、Spring Sessionの実装も入りそうな雰囲気です。

で、今回作成するコードの実行にはSpring Bootは使いますが、Spring Boot側で提供しているInfinispanに対するautoconfigureについては今回はおいておくものとします。
※Spring IO Platformに入っているInfinispanのバージョンは、ちょっと古め…

あくまで、Infinispan側が提供するSpring Cacheの機能に対する内容が、今回の焦点です。

Embedded Mode/Hot Rod Client両方とも扱います。あと、コードはScalaで書きますが(テストはScalaTest)そのあたりを含めたMavenの設定は書くと長くなるので、気になる方はGitHubソースコードを置いているのでそちらを参照いただければと思います。

使用するバージョンは、Infinispanについては8.2.5.Final、Spring Bootについては1.4.2.RELEASEを使用します。

Embedded Mode

まずは、Embedded Modeから。ドキュメントは、こちらです。

Using Infinispan as a Spring Cache provider

見てると、なんか古い気がしますが…。

準備

Embedded ModeのInfinispanをSpring Cache Providerとして使うには、「infinispan-spring4-embedded」を使用します。

        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-spring4-embedded</artifactId>
            <version>8.2.5.Final</version>
        </dependency>

その他、ScalaやSpring Boot、テスト関係を含めた依存関係。

        <dependency>
            <groupId>org.scala-lang</groupId>
            <artifactId>scala-library</artifactId>
            <version>${scala.version}</version>
        </dependency>

        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-spring4-embedded</artifactId>
            <version>8.2.5.Final</version>
        </dependency>

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.scalatest</groupId>
            <artifactId>scalatest_${scala.major.version}</artifactId>
            <version>3.0.1</version>
            <scope>test</scope>
        </dependency>
Cacheを適用するクラスと起動用のクラス

Spring Cacheを適用するクラスは、単純なもので用意。
src/main/scala/org/littlewings/infinispan/spring/CalcService.scala

package org.littlewings.infinispan.spring

import java.util.concurrent.TimeUnit

import org.springframework.cache.annotation.{CacheConfig, CacheEvict, Cacheable}
import org.springframework.stereotype.Service

@Service
@CacheConfig(cacheNames = Array("calcCache"))
class CalcService {
  @Cacheable
  def add(a: Int, b: Int): Int = {
    TimeUnit.SECONDS.sleep(3L)

    a + b
  }

  @CacheEvict
  def evict(a: Int, b: Int): Unit = ()
}

あと、@SpringBootApplicationアノテーションと、Cacheを有効にするように@EnableCachingアノテーションを付与したクラスを作成します。
src/main/scala/org/littlewings/infinispan/spring/App.scala

package org.littlewings.infinispan.spring

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.cache.annotation.EnableCaching

@SpringBootApplication
@EnableCaching
class App
CacheManagerの作成

@Beanとして、SpringのCacheManagerを作成します。Embedded Modeの場合は、Infinispan側でSpringEmbeddedCacheManagerというクラスが提供されているので、こちらを使用します。
src/main/scala/org/littlewings/infinispan/spring/Config.scala

package org.littlewings.infinispan.spring

import java.util.concurrent.TimeUnit

import org.infinispan.configuration.cache.ConfigurationBuilder
import org.infinispan.manager.DefaultCacheManager
import org.infinispan.spring.provider.SpringEmbeddedCacheManager
import org.springframework.cache.CacheManager
import org.springframework.context.annotation.{Bean, Configuration}

@Configuration
class Config {
  @Bean
  def cacheManager: CacheManager = {
    val cacheConfiguration = new ConfigurationBuilder().expiration.lifespan(5L, TimeUnit.SECONDS).build
    val nativeCacheManager = new DefaultCacheManager
    nativeCacheManager.defineConfiguration("calcCache", cacheConfiguration)

    new SpringEmbeddedCacheManager(nativeCacheManager)
  }
}

SpringEmbeddedCacheManagerに対して、InfinispanのEmbeddedCacheManager(DefaultCacheManager)を渡すようにしてあげればOKです。

Cacheの設定は、Local Cacheでexpireを5秒としました。

SpringEmbeddedCacheManagerFactoryBean、ContainerEmbeddedCacheManagerFactoryBeanといったクラスもありますが、今回は省略。

https://github.com/infinispan/infinispan/tree/8.2.5.Final/spring/spring4/spring4-embedded/src/main/java/org/infinispan/spring/provider

確認(テストコード)

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

作成したのは、こちら。
rc/test/scala/org/littlewings/infinispan/spring/SpringEmbeddedCacheTest.scala

package org.littlewings.infinispan.spring

import java.util.concurrent.TimeUnit

import org.junit.runner.RunWith
import org.junit.{Before, Test}
import org.scalatest.Matchers
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.cache.CacheManager
import org.springframework.test.context.junit4.SpringRunner

@RunWith(classOf[SpringRunner])
@SpringBootTest(classes = Array(classOf[App]))
class SpringEmbeddedCacheTest extends Matchers {
  @Autowired
  var calcService: CalcService = _

  @Autowired
  var cacheManager: CacheManager = _

  @Before
  def setUp(): Unit =
    cacheManager.getCache("calcCache").clear()

  protected def sw(fun: => Unit): Long = {
    val startTime = System.nanoTime
    fun
    TimeUnit.SECONDS.convert(System.nanoTime - startTime, TimeUnit.NANOSECONDS)
  }

  @Test
  def embeddedCacheSimpleTest(): Unit = {
    sw {
      calcService.add(1, 3) should be(4)
    } should be >= 3L
    sw {
      calcService.add(1, 3) should be(4)
    } should be < 1L

    TimeUnit.SECONDS.sleep(5L)

    sw {
      calcService.add(1, 3) should be(4)
    } should be >= 3L
  }

  @Test
  def embeddedCacheEvictTest(): Unit = {
    sw {
      calcService.add(1, 3) should be(4)
    } should be >= 3L

    calcService.evict(1, 3)

    sw {
      calcService.add(1, 3) should be(4)
    } should be >= 3L
  }
}

それぞれ、Cacheに登録してexpireを待った後にアクセスしたり、Cacheエントリを削除したりするテストです。こんな感じで。

Embedded Mode向けに作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/infinispan-getting-started/tree/master/embedded-spring-cache

Remote(Hot Rod Client)

続いて、Hot Rod ClientでのSpring Cache Provider。

こちらは、ドキュメントに記載のない機能です…。

準備

Hot Rod ClientのInfinispanをSpring Cache Providerとして使うには、「infinispan-spring4-remote」を使用します。

        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-spring4-remote</artifactId>
            <version>8.2.5.Final</version>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.logging.log4j</groupId>
                    <artifactId>log4j-api</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.apache.logging.log4j</groupId>
                    <artifactId>log4j-core</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.apache.logging.log4j</groupId>
                    <artifactId>log4j-jcl</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.apache.logging.log4j</groupId>
                    <artifactId>log4j-slf4j-impl</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-client-hotrod</artifactId>
            <version>8.2.5.Final</version>
        </dependency>
        <dependency>
            <groupId>net.jcip</groupId>
            <artifactId>jcip-annotations</artifactId>
            <version>1.0</version>
            <scope>provided</scope>
        </dependency>

jcip-annotationsが入っているのは、Scalaの都合上です。

infinispan-spring-remote4の場合は、Hot Rod Clientへの依存関係を別途追加する必要があります。また、Spring Bootで実行するにあたり、Log4j2のSLF4J向けのブリッジがSpring BootデフォルトのSLF4Jと衝突するのでexclusionしてあります。

その他に使用したライブラリは、こちら。

        <dependency>
            <groupId>org.scala-lang</groupId>
            <artifactId>scala-library</artifactId>
            <version>${scala.version}</version>
        </dependency>

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-server-hotrod</artifactId>
            <version>8.2.5.Final</version>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.logging.log4j</groupId>
                    <artifactId>log4j-api</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.apache.logging.log4j</groupId>
                    <artifactId>log4j-core</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.apache.logging.log4j</groupId>
                    <artifactId>log4j-jcl</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.apache.logging.log4j</groupId>
                    <artifactId>log4j-slf4j-impl</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.scalatest</groupId>
            <artifactId>scalatest_${scala.major.version}</artifactId>
            <version>3.0.1</version>
            <scope>test</scope>
        </dependency>

UnitTest内でInfinspan Hot Rod Serverを使いますが、こちらにもLog4j2が含まれているため、除外。

というか、Log4j2がスコープruntimeでHot Rodに入っているのは、どうしてなんでしょう…?

Cacheを適用するクラスと起動用のクラス

ここは、Embedded Modeと同じ。
src/main/scala/org/littlewings/infinispan/spring/CalcService.scala

package org.littlewings.infinispan.spring

import java.util.concurrent.TimeUnit

import org.springframework.cache.annotation.{CacheConfig, CacheEvict, Cacheable}
import org.springframework.stereotype.Service

@Service
@CacheConfig(cacheNames = Array("calcCache"))
class CalcService {
  @Cacheable
  def add(a: Int, b: Int): Int = {
    TimeUnit.SECONDS.sleep(3L)

    a + b
  }

  @CacheEvict
  def evict(a: Int, b: Int): Unit = ()
}

src/main/scala/org/littlewings/infinispan/spring/App.scala

package org.littlewings.infinispan.spring

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.cache.annotation.EnableCaching

@SpringBootApplication
@EnableCaching
class App
CacheManagerの作成

@Beanとして、SpringのCacheManagerを作成します。Remote(Hot Rod Client)の場合は、Infinispan側でSpringRemoteCacheManagerというクラスが提供されているので、こちらを使用します。
src/main/scala/org/littlewings/infinispan/spring/Config.scala

package org.littlewings.infinispan.spring

import org.infinispan.client.hotrod.RemoteCacheManager
import org.infinispan.client.hotrod.configuration.ConfigurationBuilder
import org.infinispan.spring.provider.SpringRemoteCacheManager
import org.springframework.cache.CacheManager
import org.springframework.context.annotation.{Bean, Configuration}

@Configuration
class Config {
  @Bean
  def cacheManager: CacheManager = {
    val nativeCacheManager =
      new RemoteCacheManager(new ConfigurationBuilder().addServer.host("localhost").port(11222).build)
    new SpringRemoteCacheManager(nativeCacheManager)
  }
}

SpringRemoteCacheManagerには、RemoteCacheManagerを渡せばOKです。

Cacheの設定は、Hot Rodの場合はサーバー側ですね。

こちらにも、SpringRemoteCacheManagerFactoryBeanやContainerRemoteCacheManagerFactoryBeanといったクラスがあったりします。

https://github.com/infinispan/infinispan/tree/8.2.5.Final/spring/spring4/spring4-remote/src/main/java/org/infinispan/spring/provider

確認(テストコード)

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

Hot Rodの場合はサーバーが必要になるので、UnitTest内でHot Rod Serverを起動/停止するようなコードを書いて実行します。
src/test/scala/org/littlewings/infinispan/spring/SpringRemoteCacheTest.scala

package org.littlewings.infinispan.spring

import java.util.concurrent.TimeUnit

import org.infinispan.commons.equivalence.AnyServerEquivalence
import org.infinispan.configuration.cache.ConfigurationBuilder
import org.infinispan.manager.DefaultCacheManager
import org.infinispan.server.hotrod.HotRodServer
import org.infinispan.server.hotrod.configuration.HotRodServerConfigurationBuilder
import org.junit.runner.RunWith
import org.junit.{AfterClass, Before, BeforeClass, Test}
import org.scalatest.Matchers
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.cache.CacheManager
import org.springframework.test.context.junit4.SpringRunner

object SpringRemoteCacheTest {
  var hotRodServer: HotRodServer = _

  @BeforeClass
  def setUpClass(): Unit = {
    val embeddedCacheManager = new DefaultCacheManager
    embeddedCacheManager
      .defineConfiguration(
        "calcCache",
        new ConfigurationBuilder()
          .dataContainer()
          .keyEquivalence(new AnyServerEquivalence)
          .valueEquivalence(new AnyServerEquivalence)
          .expiration
          .lifespan(5L, TimeUnit.SECONDS)
          .build
      )

    val hotRodServerHost = "localhost"
    val hotRodServerPort = 11222
    hotRodServer = new HotRodServer
    hotRodServer.start(
      new HotRodServerConfigurationBuilder()
        .host(hotRodServerHost)
        .port(hotRodServerPort)
        .build,
      embeddedCacheManager
    )
  }

  @AfterClass
  def tearDownClass(): Unit = hotRodServer.stop
}

@RunWith(classOf[SpringRunner])
@SpringBootTest(classes = Array(classOf[App]))
class SpringRemoteCacheTest extends Matchers {
  @Autowired
  var calcService: CalcService = _

  @Autowired
  var cacheManager: CacheManager = _

  @Before
  def setUp(): Unit =
    cacheManager.getCache("calcCache").clear()

  protected def sw(fun: => Unit): Long = {
    val startTime = System.nanoTime
    fun
    TimeUnit.SECONDS.convert(System.nanoTime - startTime, TimeUnit.NANOSECONDS)
  }

  @Test
  def remoteCacheSimpleTest(): Unit = {
    sw {
      calcService.add(1, 3) should be(4)
    } should be >= 3L
    sw {
      calcService.add(1, 3) should be(4)
    } should be < 1L

    TimeUnit.SECONDS.sleep(5L)

    sw {
      calcService.add(1, 3) should be(4)
    } should be >= 3L
  }

  @Test
  def remoteCacheEvictTest(): Unit = {
    sw {
      calcService.add(1, 3) should be(4)
    } should be >= 3L

    calcService.evict(1, 3)

    sw {
      calcService.add(1, 3) should be(4)
    } should be >= 3L
  }
}

テスト自体は、Embedded Modeと同じです。

最初に、Hot Rod Serverの起動/停止の実装をしてあります。Cacheの設定も、ここでしています。

object SpringRemoteCacheTest {
  var hotRodServer: HotRodServer = _

  @BeforeClass
  def setUpClass(): Unit = {
    val embeddedCacheManager = new DefaultCacheManager
    embeddedCacheManager
      .defineConfiguration(
        "calcCache",
        new ConfigurationBuilder()
          .dataContainer()
          .keyEquivalence(new AnyServerEquivalence)
          .valueEquivalence(new AnyServerEquivalence)
          .expiration
          .lifespan(5L, TimeUnit.SECONDS)
          .build
      )

    val hotRodServerHost = "localhost"
    val hotRodServerPort = 11222
    hotRodServer = new HotRodServer
    hotRodServer.start(
      new HotRodServerConfigurationBuilder()
        .host(hotRodServerHost)
        .port(hotRodServerPort)
        .build,
      embeddedCacheManager
    )
  }

  @AfterClass
  def tearDownClass(): Unit = hotRodServer.stop
}

結果は、expireの設定がEmbedded Modeの時と同じなので、ほぼ同じテストがパスします、と。

Remote(Hot Rod Client)向けに作成したコードは、こちらに置いています。

https://github.com/kazuhira-r/infinispan-getting-started/tree/master/remote-spring-cache

まとめ

Spring Cache Providerとしての、Infinispanの機能を試してみました。双方の使い方がわかっていれば、わりかし簡単に使えるのではないかなぁと思います。

Infinispan 9.0.0.FinalではSpring Sessionの実装が入りそうなので(Embedded Mode/Remoteともに)、リリースされたらそちらも試してみましょう。