CDIを使うと、Client Proxy(擬似スコープの場合は拡張されたクラス?)がインジェクションされて(もしくはBeanManagerなどから取得)、それを使うことはわかりましたが、次に気になることとしてインターセプターのかかり方があります。
なにかというと、Client Proxyが使われるくらいだから、きっとインジェクションされたインスタンス越しに使わないとインターセプターってかからないんだろうなぁと思いまして。今回、それを確認してみます。
自分は仕事柄、Seasar2をメインでDI×AOPコンテナとして使っていることが多いのですが、このあたりは押さえておきたいなぁと。
それでは、いってみましょう。Weld SEを使います。
準備
ビルド定義。
build.sbt
name := "cdi-interceptor-apply-point" version := "1.0" scalaVersion := "2.11.6" organization := "org.littlewings" scalacOptions ++= Seq("-Xlint", "-unchecked", "-deprecation", "-feature") updateOptions := updateOptions.value.withCachedResolution(true) libraryDependencies ++= Seq( "org.jboss.weld.se" % "weld-se" % "2.2.11.Final", "org.scalatest" %% "scalatest" % "2.2.4" % "test" )
インターセプターの作成
今回は、文字列を返すメソッドに対して「★」を最初と最後に付与するインターセプターを作ってみます。
アノテーションの作成。
src/main/java/org/littlewings/javaee7/interceptor/AddStar.java
package org.littlewings.javaee7.interceptor; 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 AddStar { }
インターセプター。
src/main/scala/org/littlewings/javaee7/interceptor/AddStarInterceptor.scala
package org.littlewings.javaee7.interceptor import javax.enterprise.context.Dependent import javax.interceptor.{InvocationContext, AroundInvoke, Interceptor} @Interceptor @AddStar @Dependent @SerialVersionUID(1L) class AddStarInterceptor extends Serializable { @AroundInvoke @throws(classOf[Exception]) def invoke(ic: InvocationContext): Any = ic.proceed() match { case s: String => "★" + s + "★" case other => other } }
インターセプターを適用するために、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"> <interceptors> <class>org.littlewings.javaee7.interceptor.AddStarInterceptor</class> </interceptors> </beans>
インターセプターを適用する対象のクラス
続いて、インターセプターを適用するクラスを作成します。単純ですが、String#join的なもので。
Normal Scopeなクラス。
src/main/scala/org/littlewings/javaee7/interceptor/NormalScopedMessageService.scala
package org.littlewings.javaee7.interceptor import javax.enterprise.context.ApplicationScoped @ApplicationScoped @SerialVersionUID(1L) class NormalScopedMessageService extends Serializable { @AddStar def join(separator: String, tokens: String*): String = tokens.mkString(separator) @AddStar def joinChain(separator: String, tokens: String*): String = join(separator, tokens: _*) }
ここではメソッドを2つ用意し、片方は内部で別のアノテーションを付与したメソッドを呼び出しています。
擬似スコープなクラス。
src/main/scala/org/littlewings/javaee7/interceptor/PseudoScopedMessageService.scala
package org.littlewings.javaee7.interceptor import javax.enterprise.context.Dependent @Dependent @SerialVersionUID(1L) class PseudoScopedMessageService extends Serializable { @AddStar def join(separator: String, tokens: String*): String = tokens.mkString(separator) @AddStar def joinChain(separator: String, tokens: String*): String = join(separator, tokens: _*) }
やっていることは、Normal Scopeなものと同じです。
テストコードで確認
あとは、テストコードで確認してみます。
テストコードの骨格実装。
src/test/scala/org/littlewings/javaee7/interceptor/CdiInterceptorSpec.scala
package org.littlewings.javaee7.interceptor import javax.enterprise.inject.spi.CDI import org.jboss.weld.environment.se.Weld import org.scalatest.FunSpec import org.scalatest.Matchers._ class CdiInterceptorSpec extends FunSpec { describe("CDI Interceptor Spec") { // ここにテストを書く! } private def withWeld(f: => Unit): Unit = { val weld = new Weld try { weld.initialize() f } finally { weld.shutdown() } } }
Weldの起動/停止を行う簡易メソッド付き。
では、順に見ていきます。
Nomal Scope。
it("Normal Scoped") { withWeld { val messageService = CDI.current.select(classOf[NormalScopedMessageService]).get messageService.join(", ", "Hello", "World") should be("★Hello, World★") } }
インターセプターが効いています。
擬似スコープ。
it("Pseudo Scoped") { withWeld { val messageService = CDI.current.select(classOf[PseudoScopedMessageService]).get messageService.join(", ", "Hello", "World") should be("★Hello, World★") } }
こちらもOKです。
続いて、Normal Scopeでインターセプター適用対象のメソッドをインスタンス内で続けて呼ぶパターン。
it("Normal Scoped, Method Call Chain") { withWeld { val messageService = CDI.current.select(classOf[NormalScopedMessageService]).get messageService.joinChain(", ", "Hello", "World") should be("★Hello, World★") } }
この結果を見ると、1回しか適用されていないようです。ということは、やっぱりClient Proxy越しに実行しないと効かないということですね。
擬似スコープの場合も、同じ。
it("Pseudo Scoped, Method Call Chain") { withWeld { val messageService = CDI.current.select(classOf[PseudoScopedMessageService]).get messageService.joinChain(", ", "Hello", "World") should be("★Hello, World★") } }
バリエーションの追加
あとは、ちょっと意地悪気味?ですが、アクセス修飾子も作用するかどうか見てみます。
こういうクラスを作成。突然Javaになりましたが、キニシナイ。
src/main/java/org/littlewings/javaee7/interceptor/AccessModifierMessageService.java
package org.littlewings.javaee7.interceptor; import java.io.Serializable; import javax.enterprise.context.Dependent; @Dependent public class AccessModifierMessageService implements Serializable { private static final long serialVersionUID = 1L; @AddStar public String join(String separator, String... tokens) { return String.join(separator, tokens); } @AddStar String joinPackagePrivate(String separator, String... tokens) { return String.join(separator, tokens); } }
やっていることは先ほどまでのクラスと同じですが、片方のメソッドがパッケージプライベートです。
これを、このようなクラスから呼び出してみます。
src/main/scala/org/littlewings/javaee7/interceptor/MixedMessageService.scala
package org.littlewings.javaee7.interceptor import javax.enterprise.context.Dependent import javax.inject.Inject @Dependent @SerialVersionUID(1L) class MixedMessageService extends Serializable { @Inject private var delegate: AccessModifierMessageService = _ @AddStar def joinChain(separator: String, tokens: String*): String = delegate.join(separator, tokens: _*) @AddStar def joinChainToPackagePrivate(separator: String, tokens: String*): String = delegate.joinPackagePrivate(separator, tokens: _*) }
それぞれ、先ほどJavaで作成したクラスを呼び出すような実装になっています。
テスト。
it("Object Chain") { withWeld { val messageService = CDI.current.select(classOf[MixedMessageService]).get messageService.joinChain(", ", "Hello", "World") should be("★★Hello, World★★") messageService.joinChainToPackagePrivate(", ", "Hello", "World") should be("★★Hello, World★★") } }
同じクラス内のインスタンスメソッド呼び出しに効かないのは先ほどの通りですが、インジェクションされたインスタンス越しであればpublicでなくても効きそう?
まあ、普通はしないと思いますが…。
というわけで
インターセプターが効くのは、インジェクションされた、もしくはBeanManagerなどから取得したインスタンス越しに実行したその時だけ、ということでいいのかな。
また、別のバリエーションとして、インスタンス内で呼び出すメソッドに対して別のアノテーションを付与しても、やっぱり効きませんでした。
※同じ種類のインターセプターを重複してかけてたのが、良くなかったかどうかの確認です(同じインターセプターだから2回効かない、とかだったら紛らわしいなぁと)
とりあえず覚えておきましょう。
今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/javaee7-scala-examples/tree/master/cdi-interceptor-apply-point