CLOVER🍀

That was when it all began.

CDIのDecoratorを試す

CDIのDecoratorを試してみました、ということで。

CDIによるデコレータ
http://kikutaro777.hatenablog.com/entry/2014/06/18/232219

なにやら、@Decoratorアノテーションを使用することで、CDIを使ってDecorator Patternを実現する機能?だそうな。

使ったことがないので、ちょっと試してみました。

実行環境は、Scala+Weld SEで行います。

準備

まずは、ビルド準備。
build.sbt

name := "cdi-decorator"

version := "0.0.1-SNAPSHOT"

scalaVersion := "2.11.6"

organization := "org.littlewings"

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

updateOptions := updateOptions.value.withCachedResolution(true)

libraryDependencies ++= Seq(
  "org.jboss.weld.se" % "weld-se" % "2.2.12.Final",
  "org.scalatest" %% "scalatest" % "2.2.5" % "test"
)

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>

ここまでが、最低限。

それでは、Decoratorを使っていってみます。

インターフェースと実装クラスの定義

Decoratorを使うには、まずインターフェースを定義します。今回は、このようなものを作成。
src/main/scala/org/littlewings/javaee7/cdi/MessageService.scala

package org.littlewings.javaee7.cdi

trait MessageService {
  def get: String

  def getWith(prefix: String, suffix: String): String
}

2つ目に妙なメソッドが付いていますが、こちらは最初は気にしない方向で。

このインターフェースに対して、実装クラスを用意します。
src/main/scala/org/littlewings/javaee7/cdi/DefaultMessageService.scala

package org.littlewings.javaee7.cdi

import javax.enterprise.context.ApplicationScoped

@ApplicationScoped
class DefaultMessageService extends MessageService {
  override def get: String = "Hello World!"

  override def getWith(prefix: String, suffix: String): String = s"${prefix}${get}${suffix}"
}

いたって普通。

Decoratorの用意

続いて、Decoratorを用意します。
src/main/scala/org/littlewings/javaee7/cdi/DecorateMessageService.scala

package org.littlewings.javaee7.cdi

import javax.annotation.Priority
import javax.decorator.{Decorator, Delegate}
import javax.inject.Inject
import javax.interceptor.Interceptor

@Decorator
class DecorateMessageService extends MessageService {
  @Inject
  @Delegate
  private var messageService: MessageService = _

  override def get: String = s"★${messageService.get}★"

  override def getWith(prefix: String, suffix: String): String = s"${prefix}${get}${suffix}"
}

なんとなく「★」を付けてみました。

ポイントは、@Decoratorアノテーションを付与して、かつDecoratorの適用先となるインターフェースを実装したクラスを作ること、@Injectおよび@Delegateを付与して委譲先のインスタンスを注入すること、ですね。

動作確認

それでは、まずDecoratorなしの場合で動かしてみましょう。テストコードの雛形を用意します。
src/test/scala/org/littlewings/javaee7/cdi/DecoratorSpec.scala

package org.littlewings.javaee7.cdi

import javax.enterprise.inject.spi.CDI

import org.jboss.weld.environment.se.Weld
import org.scalatest.FunSpec
import org.scalatest.Matchers._

class DecoratorSpec extends FunSpec {
  describe("CDI Decorator Spec") {
    // ここに、テストを書く!
  }

  private def withWeld(f: => Unit): Unit = {
    val weld = new Weld

    try {
      weld.initialize()

      f
    } finally {
      weld.shutdown()
    }
  }
}

Weld SEの起動・停止のヘルパーメソッド付き。

では、テストコードを。

    it("no Decorator") {
      withWeld {
        val messageService = CDI.current.select(classOf[MessageService]).get
        messageService.get should be("Hello World!")
      }
    }

ここでは、特にDecoratorの効果は現れません。

Decoratorの有効化

JSR-346の「8.2. Decorator enablement and ordering」によると、Decoratorを有効化するには

  • beans.xmlにDecoratorの定義を記述する
  • @Decoratorを付与したクラスに、@Priorityアノテーションを付与する

のいずれかのようです。

今回は、@Priorityを付与する方向とします。先ほどの@Decoratorを付与したクラスを、このように修正。

@Decorator
@Priority(Interceptor.Priority.APPLICATION)
class DecorateMessageService extends MessageService {

すると、先ほどのテストと同等のコードですが、動作が変わります。

    it("with Decorator") {
      withWeld {
        val messageService = CDI.current.select(classOf[MessageService]).get
        messageService.get should be("★Hello World!★")

        messageService.getClass.getSimpleName should be("DefaultMessageService$Proxy$_$$_WeldClientProxy")
      }
    }

コード上は、特にDecoratorを意識する必要はありません。Client Proxyについても、特にDecoratorが拡張されたクラス名としては現れないみたいですね。

なお、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">
    <decorators>
        <class>org.littlewings.javaee7.cdi.DecorateMessageService</class>
    </decorators>
</beans>

Decoratorを積み重ねる

Decoratorは、積み重ねが可能なようです。追加で、このようなDecoratorを用意。
src/main/scala/org/littlewings/javaee7/cdi/CircleDecorateMessageService.scala

package org.littlewings.javaee7.cdi

import javax.annotation.Priority
import javax.decorator.{Delegate, Decorator}
import javax.inject.Inject
import javax.interceptor.Interceptor

@Decorator
@Priority(Interceptor.Priority.APPLICATION)
class CircleDecorateMessageService extends MessageService {
  @Inject
  @Delegate
  private var messageService: MessageService = _

  override def get: String = s"◯${messageService.get}◯"

  override def getWith(prefix: String, suffix: String): String = s"${prefix}${get}${suffix}"
}

「◯」が付きます…。

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

    it("with Double Decorator") {
      withWeld {
        val messageService = CDI.current.select(classOf[MessageService]).get
        messageService.get should be("★◯Hello World!◯★")
      }
    }

結果に、「★」と「◯」が付与されて現れるようになります。

Decoratorの適用順を制御する

Decoratorの適用順を制御するには、@Priorityに付与する値、beans.xmlへの記述順で行うようです。

先ほどの2つのDecoratorの定義を、このように修正してみます。

@Decorator
@Priority(Interceptor.Priority.APPLICATION + 100)
class DecorateMessageService extends MessageService {

Interceptor.Priority.APPLICATIONに+100、

@Decorator
@Priority(Interceptor.Priority.APPLICATION + 10)
class CircleDecorateMessageService extends MessageService {

Interceptor.Priority.APPLICATIONに+10。

すると、Decoratorの適用順が入れ替わります。

    it("with Ordering Double Decorator") {
      withWeld {
        val messageService = CDI.current.select(classOf[MessageService]).get
        messageService.get should be("◯★Hello World!★◯")
      }
    }

@Priorityに付与する値が小さい方が、先に適用されるようです。

beans.xmlで、同様の挙動とする場合は、このようになります。

    <decorators>
        <class>org.littlewings.javaee7.cdi.CircleDecorateMessageService</class>
        <class>org.littlewings.javaee7.cdi.DecorateMessageService</class>
    </decorators>

書いた順ってことですね。

Interceptorと合わせて使う

JSR-346のDecoratorについて書いている中に、Interceptorとの適用順について触れられていたので、こちらも試してみます。

まずは、Interceptorを定義します。
src/main/java/org/littlewings/javaee7/cdi/AddBrackets.java

package org.littlewings.javaee7.cdi;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.interceptor.InterceptorBinding;

@InterceptorBinding
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AddBrackets {
}

アノテーションの定義は、Java…。

Interceptor。今度は、前後に「[」と「]」と付与します。
src/main/scala/org/littlewings/javaee7/cdi/AddBracketsInterceptor.scala

package org.littlewings.javaee7.cdi

import javax.enterprise.context.Dependent
import javax.interceptor.{AroundInvoke, InvocationContext, Interceptor}

@Interceptor
@AddBrackets
@Dependent
@SerialVersionUID(1L)
class AddBracketsInterceptor extends Serializable {
  @AroundInvoke
  @throws(classOf[Exception])
  def invoke(ic: InvocationContext): Any =
    ic.proceed() match {
      case s: String => s"[$s]"
      case other => other
    }
}

作成したアノテーションを、実際に処理を行うクラスの方に付与。

@ApplicationScoped
class DefaultMessageService extends MessageService {
  @AddBrackets
  override def get: String = "Hello World!"

  override def getWith(prefix: String, suffix: String): String = s"${prefix}${get}${suffix}"
}

beans.xmlで、Interceptorを有効化します。

    <interceptors>
        <class>org.littlewings.javaee7.cdi.AddBracketsInterceptor</class>
    </interceptors>

すると、テスト結果はこのように。

    it("with Interceptor") {
      withWeld {
        val messageService = CDI.current.select(classOf[MessageService]).get
        messageService.get should be("[◯★Hello World!★◯]")
      }
    }

JSR-346の「8.2. Decorator enablement and ordering」には「Decorators are called after interceptors.」と書かれており、Interceptorの後にDecoratorが呼び出されるようになっています。

なお、@Decorator自体にInterceptorを適用しようとしても、効果がありませんでした。

@Decorator
@Priority(Interceptor.Priority.APPLICATION + 100)
class DecorateMessageService extends MessageService {
  @Inject
  @Delegate
  private var messageService: MessageService = _

  @AddBrackets  // <-効かない
  override def get: String = s"★${messageService.get}★"

InterceptorとClient Proxy?

Interceptorは、Client Proxy越しに呼び出さないと適用されませんが、Decoratorの中で自分のメソッドを呼び、そのメソッドがInterceptor適用対象と定義されていた場合はどうなるのかな?と思ってちょっと試してみました。

Client Proxyとして作られたクラス名に特にDecoratorが現れていないと、InterceptorがClient Proxy越しに叩かないと動かないことからある程度想定できますが…。

ここまで全然使っていなかった、getWithメソッドを使います。任意のPrefix、Suffixが付けられますよ、と。

@Decorator
@Priority(Interceptor.Priority.APPLICATION + 10)
class CircleDecorateMessageService extends MessageService {
  @Inject
  @Delegate
  private var messageService: MessageService = _

  override def get: String = s"◯${messageService.get}◯"

  override def getWith(prefix: String, suffix: String): String = s"${prefix}${get}${suffix}"
}

getWithメソッドからすぐgetメソッドを呼び出します。

ここで、委譲先のクラスにおけるgetWithメソッドはInterceptor適用対象外、getメソッドはInterceptor適用対象です。

@ApplicationScoped
class DefaultMessageService extends MessageService {
  @AddBrackets
  override def get: String = "Hello World!"

  override def getWith(prefix: String, suffix: String): String = s"${prefix}${get}${suffix}"
}

結果。

    it("getWith with Interceptor") {
      withWeld {
        val messageService = CDI.current.select(classOf[MessageService]).get
        messageService.getWith("{", "}") should be("{◯★Hello World!★◯}")
      }
    }

Interceptorの適用結果(「[」と「]」)がありません、と。

というわけで、あくまでInterceptorを使いたければ呼び出し元のコードからClient Proxy越しに呼び出しましょう、ということですね。

オマケ

この例だと、DecoratorとInterceptorの呼び出し順がピンと来ない、と思われた方のために。よくあるトレース用のInterceptor、そしてDecoratorを用意してみます。

アノテーション
src/main/java/org/littlewings/javaee7/cdi/trace/Trace.java

package org.littlewings.javaee7.cdi.trace;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.interceptor.InterceptorBinding;

@InterceptorBinding
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Trace {
}

Interceptor。
src/main/scala/org/littlewings/javaee7/cdi/trace/TraceInterceptor.scala

package org.littlewings.javaee7.cdi.trace

import javax.enterprise.context.Dependent
import javax.interceptor.{InvocationContext, AroundInvoke, Interceptor}

@Interceptor
@Trace
@Dependent
@SerialVersionUID(1L)
class TraceInterceptor extends Serializable {
  @AroundInvoke
  @throws(classOf[Exception])
  def invoke(ic: InvocationContext): Any = {
    println("start Interceptor.")

    try {
      ic.proceed()
    } finally {
      println("end Interceptor.")
    }
  }
}

インターフェースの定義と、Interceptorのbeans.xmlへの登録は省略します。

委譲先のクラス。
src/main/scala/org/littlewings/javaee7/cdi/trace/DefaultMessageService.scala

package org.littlewings.javaee7.cdi.trace

import javax.enterprise.context.ApplicationScoped

@ApplicationScoped
class DefaultMessageService extends MessageService {
  @Trace
  override def get: String = {
    println("called.")
    "Hello World!"
  }
}

Decorator。このオマケでは、Decoratorはこれひとつとします。

src/main/scala/org/littlewings/javaee7/cdi/trace/DecorateMessageService.scala
package org.littlewings.javaee7.cdi.trace

import javax.annotation.Priority
import javax.decorator.{Decorator, Delegate}
import javax.inject.Inject
import javax.interceptor.Interceptor

@Decorator
@Priority(Interceptor.Priority.APPLICATION)
class DecorateMessageService extends MessageService {
  @Inject
  @Delegate
  private var messageService: MessageService = _

  override def get: String = {
    println("start Decorator.")

    try {
      messageService.get
    } finally {
      println("end Decorator.")
    }
  }
}

ここでは、「★」が付いたり「◯」が付いたりしません。

テストコード。

    it("with Decorator") {
      withWeld {
        val messageService = CDI.current.select(classOf[MessageService]).get
        messageService.get should be("Hello World!")
      }
    }

この時の、コンソール上の出力。

start Interceptor.
start Decorator.
called.
end Decorator.
end Interceptor.

というわけで、Interceptor → Decorator → 委譲先 の順で呼び出されます、と。

最後に

ちょっと長くなってしまいましたが、そこそこDecoratorについて追うことができました。

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