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を有効化するには
のいずれかのようです。
今回は、@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 { }
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