CLOVER🍀

That was when it all began.

InfinispanのCustom Interceptorを実装する

最近ちょっと気になった、Infinispanのこの機能。

20. Custom Interceptors
http://infinispan.org/docs/7.1.x/user_guide/user_guide.html#_custom_interceptors_chapter

全然試したことがなかったので、これを機にと。

Custom Interceptorとは?

ドキュメントに書かれている概要のまんまですが、以下のような感じです。

  • Infinispanを拡張する方法のひとつ
  • Cacheに対するput、get、removeやトランザクションのコミットなどの操作に対して、何らかの処理を入れ込める

要は、Cacheのいろいろな操作に自分でInterceptorを挟み込めるということのようです。

実装方法は…

  • CommandInterceptorのAPIを見てね
  • BaseCustomInterceptorクラスを継承して作るんだよ(BaseCustomInterceptorクラスは、CommandInterceptorのサブクラス)
  • publicな引数なしのコンストラクタを定義してね
  • setterを付けておくと、XMLで定義したpropertyタグで定義した値を設定できるよ
  • Interceptorは、XMLの設定やAPIでCacheに設定できるよ

以上!あとはJavadocを見てね、と…。

え?以上?

ドキュメントには、概ねここまでしかありません。なので、ここから先は、CommandInterceptorのJavadocやテストコード、各APIを見ながら読み解いてみました。

実際の実装方法について

とりあえず、BaseCustomInterceptorクラスを継承するのは確定です。CommandInterceptorのJavadocを見るとvisitXXXメソッドをオーバーライドしてね、みたいなことが書かれています。これらのメソッドは、BaseCustomInterceptorクラスが実装しているインターフェースであるVisitorで宣言されています。
https://github.com/infinispan/infinispan/blob/7.1.1.Final/core/src/main/java/org/infinispan/commands/Visitor.java

このVisitorインターフェースで定義されているメソッドの骨格実装をAbstractVisitor、CommandInterceptor、BaseCustomInterceptorクラスが実装しているわけですが(全部継承ツリーにいます)、CustomInterceptorの実装としては、Visitorインターフェースに定義されているメソッドで割り込みたいものをオーバーライドする形になります。



Visitorインターフェースに定義されているメソッドは、以下の通りです。

  • visitPutKeyValueCommand
  • visitRemoveCommand
  • visitReplaceCommand
  • visitClearCommand
  • visitPutMapCommand
  • visitEvictCommand
  • visitApplyDeltaCommand
  • visitSizeCommand
  • visitGetKeyValueCommand
  • visitGetCacheEntryCommand
  • visitGetAllCommand
  • visitKeySetCommand
  • visitValuesCommand
  • visitEntrySetCommand
  • visitEntryRetrievalCommand
  • visitPrepareCommand
  • visitRollbackCommand
  • visitCommitCommand
  • visitInvalidateCommand
  • visitInvalidateL1Command
  • visitLockControlCommand
  • visitUnknownCommand
  • visitDistributedExecuteCommand
  • visitGetKeysInGroupCommand

引数には、InvocationContextとメソッドに対応したCommandクラス(visitPutKeyValueCommandメソッドなら、PutKeyValueCommandクラス)を取ります。

このオーバーライドしたメソッドの中で何か処理をして、上位クラスのメソッドもしくはinvokeNextInterceptorメソッドを呼び出して、次のInterceptorにつなげます。
※なお、どのvisitメソッドが何のメソッド呼び出しなどにトリガーするかは、特に書かれていない様子…

こんなイメージです。これはvisitPutKeyValueCommandメソッドをオーバーライドした例で、Cache#putに割り込むことができます。

   @Override
   public Object visitPutKeyValueCommand(InvocationContext ctx, PutKeyValueCommand command) throws Throwable {
      // ここで何かする

      Object result = super.visitPutKeyValueCommand(ctx, command);
      // これでもOK?
      // Object result = invokeNextInterceptor(ctx, command);

      // ここでも、何かするかもしれない
      return result;
   }

この場合は、super#visitPutKeyValueCommandもしくはinvokeNextInterceptorメソッドを呼び出して次のInterceptorにつなげることを忘れてはいけません。どちらを使えばよいかですが、invokeNextInterceptorだとCommandの型がVisitableCommandクラスというちょっと汎用的なものになってしまうので、型安全性という意味でも素直に上位クラスのデフォルト実装を呼び出すのが良いような気がします。
※最初に見つけたテスト実装はinvokeNextInterceptorを呼び出していましたが…

別解)
あと、もうひとつの実装方法としてはCommandInterceptor#handleDefault(InvocationContext, VisitableCommand)メソッドをオーバーライドして、このCommmandの型でswitchする方法もあるようです。この場合は、super#handleDefaultを呼び出すことになりそうな気がしますね。

それでは、CustomInterceptorを実装してみます。

準備

まずはビルド定義。
build.sbt

name := "embedded-custom-interceptor"

version := "0.0.1-SNAPSHOT"

scalaVersion := "2.11.6"

organization := "org.littlewings"

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

updateOptions := updateOptions.value.withCachedResolution(true)

fork in Test := true

libraryDependencies ++= Seq(
  "org.infinispan" % "infinispan-core" % "7.1.1.Final",
  "net.jcip" % "jcip-annotations" % "1.0" % "provided",
  "org.scalatest" %% "scalatest" % "2.2.4" % "test"
)

設定ファイルとテストコードの骨格

今回は、いきなりInterceptorの実装を全部並べるのではなく、設定ファイルの断片とそれに対応するInterceptorの実装を載せていく形で書いていこうと思います。

で、その時に使う設定ファイルとテストコードの骨格を書いておきます。

設定ファイルから。
src/test/resources/infinispan.xml

<?xml version="1.0" encoding="UTF-8"?>
<infinispan
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="urn:infinispan:config:7.1 http://www.infinispan.org/schemas/infinispan-config-7.1.xsd"
    xmlns="urn:infinispan:config:7.1">
  <cache-container name="cacheManager" shutdown-hook="REGISTER">
    <jmx duplicate-domains="true" />

    <!-- ここに、Cacheの定義を書く! -->

  </cache-container>
</infinispan>

テストコードの雛形。
src/test/scala/org/littlewings/infinispan/interceptor/CustomInterceptSpec.scala

package org.littlewings.infinispan.interceptor

import org.infinispan.{ AdvancedCache, Cache }
import org.infinispan.manager.DefaultCacheManager

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

class CustomInterceptorSpec extends FunSpec {
  describe("Infinispan Custom Interceptor Spec") {

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

  private def withCache[K, V](fileName: String, cacheName: String)(f: Cache[K, V] => Unit): Unit = {
    val manager = new DefaultCacheManager(fileName)

    try {
      val cache = manager.getCache[K, V](cacheName)

      f(cache)

      cache.stop()
    } finally {
      manager.stop()
    }
  }
}

Cacheを作成して破棄する、ヘルパーメソッド的なものも用意。

この開いている部分を埋めつつ、Interceptorと合わせて例を出していきます。

visitPutKeyValueCommandメソッドを実装してみる

最初は、visitPutKeyValueCommandメソッドをオーバーライドして実装してみます。Cache#putが対象になるわけですが、ここでトレースログを出すように実装してみました。
src/main/scala/org/littlewings/infinispan/interceptor/PutKeyValueTraceInterceptor.scala

package org.littlewings.infinispan.interceptor

import org.infinispan.commands.write.PutKeyValueCommand
import org.infinispan.context.InvocationContext
import org.infinispan.interceptors.base.BaseCustomInterceptor

class PutKeyValueTraceInterceptor extends BaseCustomInterceptor {
  override def visitPutKeyValueCommand(ctx: InvocationContext, command: PutKeyValueCommand): AnyRef = {
    println(s"[${getClass.getSimpleName}] Start. key = ${command.getKey}, value = ${command.getValue}")

    val result = super.visitPutKeyValueCommand(ctx, command)
    // もしくは
    // val result = invokeNextInterceptor(ctx, command)

    println(s"[${getClass.getSimpleName}] End. key = ${command.getKey}, value = ${command.getValue}")

    result
  }
}

これを適用したCacheの設定はこちら。

    <local-cache name="withPutKeyValueTraceCache">
      <custom-interceptors>
        <interceptor position="FIRST"
                     class="org.littlewings.infinispan.interceptor.PutKeyValueTraceInterceptor" />
      </custom-interceptors>
    </local-cache>

Custom InterceptorはInterceptorのクラス名を設定しますが、複数登録することができるので場所を指定する必要があります。指定方法は、index、before、after、position属性で絶対位置、もしくは相対位置で指定することができます。今回は、position="FIRST"と指定しました。

各属性にどのような値が指定可能かは、XML Schemaの定義を記載してくれているconfigdocsを見てください。
http://docs.jboss.org/infinispan/7.1/configdocs/infinispan-config-7.1.html#

これに対するテストコード。

    it("PutKeyValue Trace Interceptor") {
      withCache[String, String]("infinispan.xml", "withPutKeyValueTraceCache") { cache =>
        (1 to 3).foreach(i => cache.put(s"key$i", s"value$i"))

        cache.get("key1") should be ("value1")
        cache.get("key2") should be ("value2")
        cache.get("key3") should be ("value3")
      }
    }

実行してみると、このようなログがコンソールに出力されます。

[PutKeyValueTraceInterceptor] Start. key = key1, value = value1
[PutKeyValueTraceInterceptor] End. key = key1, value = value1
[PutKeyValueTraceInterceptor] Start. key = key2, value = value2
[PutKeyValueTraceInterceptor] End. key = key2, value = value2
[PutKeyValueTraceInterceptor] Start. key = key3, value = value3
[PutKeyValueTraceInterceptor] End. key = key3, value = value3

ちゃんとInterceptorが効いていますね。

visitGetKeyValueCommandメソッドをオーバーライドしてみる

今度は、visitGetKeyValueCommandメソッドをオーバーライドしてみます。
src/main/scala/org/littlewings/infinispan/interceptor/GetKeyValueTraceInterceptor.scala

package org.littlewings.infinispan.interceptor

import org.infinispan.commands.read.GetKeyValueCommand
import org.infinispan.context.InvocationContext
import org.infinispan.interceptors.base.BaseCustomInterceptor

class GetKeyValueTraceInterceptor extends BaseCustomInterceptor {
  override def visitGetKeyValueCommand(ctx: InvocationContext, command: GetKeyValueCommand): AnyRef = {
    println(s"[${getClass.getSimpleName}] Start. key = ${command.getKey}")

    val result = super.visitGetKeyValueCommand(ctx, command)
    // もしくは
    // val result = invokeNextInterceptor(ctx, command)

    println(s"[${getClass.getSimpleName}] End. key = ${command.getKey}, value = ${result}")

    result
  }
}

Cache#getに割り込むので、Cache#putの時と違ってGetKeyValueCommandからはCacheに設定されている値を取得することができません。super#visitGetKeyValueCommandの呼び出し結果がCache#getの戻り値になりますので、今回はこちらをログ出力します。

Cacheの設定。せっかくなので、先ほど作成したCache#putに対するInterceptorも適用してみましょう。順番は、PutとGetのInterceptorにしていますが、今回は重なるところがありません…。

    <local-cache name="withPutGetTraceCache">
      <custom-interceptors>
        <interceptor position="FIRST"
                     class="org.littlewings.infinispan.interceptor.PutKeyValueTraceInterceptor" />
        <interceptor position="LAST"
                     class="org.littlewings.infinispan.interceptor.GetKeyValueTraceInterceptor" />
      </custom-interceptors>
    </local-cache>

テストコード。

    it("PutKeyValue, GetKeyVakue, Trace Interceptor") {
      withCache[String, String]("infinispan.xml", "withPutGetTraceCache") { cache =>
        (1 to 3).foreach(i => cache.put(s"key$i", s"value$i"))

        cache.get("key1") should be ("value1")
        cache.get("key2") should be ("value2")
        cache.get("key3") should be ("value3")
      }
    }

実行結果。

[PutKeyValueTraceInterceptor] Start. key = key1, value = value1
[PutKeyValueTraceInterceptor] End. key = key1, value = value1
[PutKeyValueTraceInterceptor] Start. key = key2, value = value2
[PutKeyValueTraceInterceptor] End. key = key2, value = value2
[PutKeyValueTraceInterceptor] Start. key = key3, value = value3
[PutKeyValueTraceInterceptor] End. key = key3, value = value3
[GetKeyValueTraceInterceptor] Start. key = key1
[GetKeyValueTraceInterceptor] End. key = key1, value = value1
[GetKeyValueTraceInterceptor] Start. key = key2
[GetKeyValueTraceInterceptor] End. key = key2, value = value2
[GetKeyValueTraceInterceptor] Start. key = key3
[GetKeyValueTraceInterceptor] End. key = key3, value = value3

それぞれ、Cache#putとCache#getに対してInterceptorが適用されていますね。

AdvancedCacheに対して、動的にInterceptorを追加する

先ほどまでは、XMLの設定ファイルでInterceptorを追加していましたが(もちろん、ConfigurationBuilderによる定義も可能です)、すでに構築済みのCacheに対して、後からInterceptorを追加することもできます。

Cacheの設定としては、空っぽのものを用意します。

    <local-cache name="noInterceptorCache" />

テストコード。ここで、Cacheから取得できるAdvancedCacheに対してaddInterceptorで動的にInterceptorを追加しています。

    it("PutKeyValue, GetKeyVakue, Trace Interceptor, add AdvancedCache") {
      withCache[String, String]("infinispan.xml", "noInterceptorCache") { cache =>
        val advancedCache: AdvancedCache[String, String] = cache.getAdvancedCache
        advancedCache.addInterceptor(new PutKeyValueTraceInterceptor, 0)
        advancedCache.addInterceptor(new GetKeyValueTraceInterceptor, 1)

        (1 to 3).foreach(i => cache.put(s"key$i", s"value$i"))

        cache.get("key1") should be ("value1")
        cache.get("key2") should be ("value2")
        cache.get("key3") should be ("value3")
      }
    }

追加場所は、index指定で。

visitメソッドの複数オーバーライド

これまでひとつずつしかvisitメソッドをオーバーライドしていませんでしたが、先ほど作成したPutとGetを合わせたInterceptorを作りたければ、それぞれのメソッドをオーバーライドすればOKです。まあ、当たり前ですが…。
src/main/scala/org/littlewings/infinispan/interceptor/PutGetTraceInterceptor.scala

package org.littlewings.infinispan.interceptor

import org.infinispan.commands.read.GetKeyValueCommand
import org.infinispan.commands.write.PutKeyValueCommand
import org.infinispan.context.InvocationContext
import org.infinispan.interceptors.base.BaseCustomInterceptor

class PutGetTraceInterceptor extends BaseCustomInterceptor {
  override def visitGetKeyValueCommand(ctx: InvocationContext, command: GetKeyValueCommand): AnyRef = {
    println(s"[${getClass.getSimpleName}] Start. key = ${command.getKey}")

    val result = super.visitGetKeyValueCommand(ctx, command)
    // もしくは
    // val result = invokeNextInterceptor(ctx, command)

    println(s"[${getClass.getSimpleName}] End. key = ${command.getKey}, value = ${result}")

    result
  }

  override def visitPutKeyValueCommand(ctx: InvocationContext, command: PutKeyValueCommand): AnyRef = {
    println(s"[${getClass.getSimpleName}] Start. key = ${command.getKey}, value = ${command.getValue}")

    val result = super.visitPutKeyValueCommand(ctx, command)
    // もしくは
    // val result = invokeNextInterceptor(ctx, command)

    println(s"[${getClass.getSimpleName}] End. key = ${command.getKey}, value = ${command.getValue}")

    result
  }
}

設定。

    <local-cache name="withTraceCache">
      <custom-interceptors>
        <interceptor position="FIRST"
                     class="org.littlewings.infinispan.interceptor.PutGetTraceInterceptor" />
      </custom-interceptors>
    </local-cache>

実行結果は、ひとつ前のPutとGetのInterceptorをそれぞれ適用したのと同じなので、割愛。

Cache操作の値の変更と、propertyでInterceptorへの項目設定をする

最後に、Cache操作に割り込んだ際にCommandの値を変更してみるのと、Interceptorにpropertyを設定する例を作ってみたいと思います。

Cache#putされるvalueの方がIntegerだったら、指定の値で掛け算してCacheに入る値を増やすInterceptorです。普通はこんなことしないと思いますが、例ということで…。
src/main/scala/org/littlewings/infinispan/interceptor/IntegerMultiplyInterceptor.scala

package org.littlewings.infinispan.interceptor

import scala.beans.BeanProperty

import org.infinispan.commands.write.PutKeyValueCommand
import org.infinispan.context.InvocationContext
import org.infinispan.interceptors.base.BaseCustomInterceptor

class IntegerMultiplyInterceptor extends BaseCustomInterceptor {
  @BeanProperty
  var num: String = "1" 

  override def visitPutKeyValueCommand(ctx: InvocationContext, command: PutKeyValueCommand): AnyRef = {
    val newValue: AnyRef = command.getValue match {
      case n: Integer => Integer.valueOf(n * num.toInt)
      case n => n
    }

    command.setValue(newValue)

    super.visitPutKeyValueCommand(ctx, command)
    // もしくは
    // invokeNextInterceptor(ctx, command)
  }
}

Cacheの設定。

    <local-cache name="withMultiplyAndTraceCache">
      <custom-interceptors>
        <interceptor index="0"
                     class="org.littlewings.infinispan.interceptor.IntegerMultiplyInterceptor">
          <property name="num">10</property>
        </interceptor>
        <interceptor index="1"
                     class="org.littlewings.infinispan.interceptor.PutGetTraceInterceptor" />
      </custom-interceptors>
    </local-cache>

IntegerMultiplyInterceptorの方には、propertyで項目設定がしてあります。Interceptorにpublicなsetterがあれば、この値をStringとして受け取ることができます。また、今回のInterceptorの適用位置は、index指定としました。

テストコード。これまでのものとは違い、しれっとvalueがIntegerになっています。

    it("Multiply, Trace Interceptor") {
      withCache[String, Int]("infinispan.xml", "withMultiplyAndTraceCache") { cache =>
        (1 to 3).foreach(i => cache.put(s"key$i", i))

        cache.get("key1") should be (10)
        cache.get("key2") should be (20)
        cache.get("key3") should be (30)
      }
    }

で、設定ファイルでかける数を10にしたので、Cache#putした値が10倍になっているだろうというテストコードです。

実行してみます。

[PutGetTraceInterceptor] Start. key = key1, value = 10
[PutGetTraceInterceptor] End. key = key1, value = 10
[PutGetTraceInterceptor] Start. key = key2, value = 20
[PutGetTraceInterceptor] End. key = key2, value = 20
[PutGetTraceInterceptor] Start. key = key3, value = 30
[PutGetTraceInterceptor] End. key = key3, value = 30
[PutGetTraceInterceptor] Start. key = key1
[PutGetTraceInterceptor] End. key = key1, value = 10
[PutGetTraceInterceptor] Start. key = key2
[PutGetTraceInterceptor] End. key = key2, value = 20
[PutGetTraceInterceptor] Start. key = key3
[PutGetTraceInterceptor] End. key = key3, value = 30

テストにはパスし、後で実行されたTraceInterceptorの時点で値が10倍されていることがわかります。

ここで、Interceptorの順番(indexの値)を入れ替えてみます。

      <custom-interceptors>
        <interceptor index="1"
                     class="org.littlewings.infinispan.interceptor.IntegerMultiplyInterceptor">
          <property name="num">10</property>
        </interceptor>
        <interceptor index="0"
                     class="org.littlewings.infinispan.interceptor.PutGetTraceInterceptor" />
      </custom-interceptors>

テストにはパスしますが、TraceInterceptorの開始時点ではまだ10倍されていないので、開始ログと終了ログで値が異なる結果になります。

[PutGetTraceInterceptor] Start. key = key1, value = 1
[PutGetTraceInterceptor] End. key = key1, value = 10
[PutGetTraceInterceptor] Start. key = key2, value = 2
[PutGetTraceInterceptor] End. key = key2, value = 20
[PutGetTraceInterceptor] Start. key = key3, value = 3
[PutGetTraceInterceptor] End. key = key3, value = 30
[PutGetTraceInterceptor] Start. key = key1
[PutGetTraceInterceptor] End. key = key1, value = 10
[PutGetTraceInterceptor] Start. key = key2
[PutGetTraceInterceptor] End. key = key2, value = 20
[PutGetTraceInterceptor] Start. key = key3
[PutGetTraceInterceptor] End. key = key3, value = 30

これで、Interceptorにプロパティが指定できることと、適用順が反映されていることを確認できました。

Custom Interceptorの確認としては、こんなところでしょう!

実装を見ていて

いかんせん、ドキュメント不足なのでけっこうソースコードを追うことになりました。

まずは、Interceptorというか、Commandが使われているところとか。
https://github.com/infinispan/infinispan/blob/7.1.1.Final/core/src/main/java/org/infinispan/cache/impl/CacheImpl.java#L1055
https://github.com/infinispan/infinispan/blob/7.1.1.Final/core/src/main/java/org/infinispan/cache/impl/CacheImpl.java#L1578

また、Infinispanが内部で使用しているInterceptorを組み上げているところなども見ることができました。
https://github.com/infinispan/infinispan/blob/7.1.1.Final/core/src/main/java/org/infinispan/factories/InterceptorChainFactory.java#L103

設定を変えると、こうやってInterceptorが追加されていくんだなぁと。

Custom Interceptorが追加される箇所。
https://github.com/infinispan/infinispan/blob/7.1.1.Final/core/src/main/java/org/infinispan/factories/InterceptorChainFactory.java#L303
https://github.com/infinispan/infinispan/blob/7.1.1.Final/core/src/main/java/org/infinispan/factories/InterceptorChainFactory.java#L307

最後に、なんとなくソースを見ていて前から思っていたのですが、Infinispanの持つ依存関係の注入部分など。
https://github.com/infinispan/infinispan/blob/7.1.1.Final/core/src/main/java/org/infinispan/factories/AbstractComponentRegistry.java#L201

独自の@Injectアノテーションとかあったりします。どうもコンパイル時にいろいろやっているみたいです。結果は、こんなファイルに入っている模様。

infinispan-core-component-metadata.dat

JARファイル中ですが。

そのうち、このあたりも追ってみようかなと思います。

意外と使うのにハードルが高い?感じでしたが、いろいろ面白かったです。

今回作成したソースコードは、こちらに置いています。
https://github.com/kazuhira-r/infinispan-getting-started/tree/master/embedded-custom-interceptor