CLOVER🍀

That was when it all began.

CDIのDependentアノテーションがよくわからないので、Weld SEでちょっと確認してみました

CDIをちょっとずつ見ているのですが、個人的にどうしても引っかかるのが@Dependentというスコープ。

少し調べてみると見かけるのは、

  • 擬似スコープと呼ばれる
  • インジェクション先のライフサイクルに準ずる

といった説明なのですが、個人的にはちょっと得体の知れないものに映るのです…(気にするところじゃない?それとも、どういうところで使うとかある程度決まってる?)。
※そういえば、PrototypeみたいなスコープってCDIにはないんだなぁと最近思いました

で、JSRを読んでみても理解半分だし、よくわからないところも多いので動かして挙動を見てみることにしました。

Weld SEを使って。

準備

今回、CDI管理Beanを定義してWeld SE上で動作確認をします。まずはそのためのビルド定義。
build.sbt

name := "cdi-dependent"

version := "1.0"

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"
)

コードは、Scalaで…。

最初のCDI管理Bean

テスト用として、このようなCDI管理Beanを用意しました。
ApplicationScopedのCDI管理Bean
src/main/scala/org/littlewings/javaee/service/NormalScopedCalcService.scala

package org.littlewings.javaee.service

import javax.enterprise.context.ApplicationScoped

@ApplicationScoped
class NormalScopedCalcService {
  def add(a: Int, b: Int): (Int, String) = (a + b, getClass.getSimpleName)

  def multiply(a: Int, b: Int): (Int, String) = (a * b, getClass.getSimpleName)
}

内容は足し算、掛け算なのですが、ちょっとした意図があって一緒にクラスの単純名も返すようにしています。この他のCDI管理Beanも、全部同じ実装です。

擬似スコープ(@Dependent)のCDI管理Bean
src/main/scala/org/littlewings/javaee/service/PseudoScopedCalcService.scala

package org.littlewings.javaee.service

import javax.enterprise.context.Dependent

@Dependent
class PseudoScopedCalcService {
  def add(a: Int, b: Int): (Int, String) = (a + b, getClass.getSimpleName)

  def multiply(a: Int, b: Int): (Int, String) = (a * b, getClass.getSimpleName)
}

ApplicationScopedのCDI管理Beanに、擬似スコープ(@Dependent)のCDI管理BeanをInject
src/main/scala/org/littlewings/javaee/service/NormalScopedMixedCalcService.scala

package org.littlewings.javaee.service

import javax.enterprise.context.ApplicationScoped
import javax.inject.Inject

@ApplicationScoped
class NormalScopedMixedCalcService {
  @Inject
  var delegate: PseudoScopedCalcService = _

  def add(a: Int, b: Int): (Int, String) = (delegate.add(a, b)._1, getClass.getSimpleName)

  def multiply(a: Int, b: Int): (Int, String) = (delegate.multiply(a, b)._1, getClass.getSimpleName)
}

ネストしたものについては、外側のクラスの名前を返すようにします。

擬似スコープ(@Dependent)のCDI管理Beanに、ApplicationScopedのCDI管理BeanをInject
src/main/scala/org/littlewings/javaee/service/PseudoScopedMixedCalcService.scala

package org.littlewings.javaee.service

import javax.enterprise.context.Dependent
import javax.inject.Inject

@Dependent
class PseudoScopedMixedCalcService {
  @Inject
  var delegate: NormalScopedCalcService = _

  def add(a: Int, b: Int): (Int, String) = (delegate.add(a, b)._1, getClass.getSimpleName)

  def multiply(a: Int, b: Int): (Int, String) = (delegate.multiply(a, b)._1, getClass.getSimpleName)
}

追加)
擬似スコープ(@Dependent)のCDI管理Beanに、擬似スコープ(@Dependent)のCDI管理BeanをInject
cdi-dependent/src/main/scala/org/littlewings/javaee/service/PseudoScopedWithPseudoScopedMixedCalcService.scala

package org.littlewings.javaee.service

import javax.enterprise.context.Dependent
import javax.inject.Inject

@Dependent
class PseudoScopedWithPseudoScopedMixedCalcService {
  @Inject
  var delegate: PseudoScopedCalcService = _

  def add(a: Int, b: Int): (Int, String) = (delegate.add(a, b)._1, getClass.getSimpleName)

  def multiply(a: Int, b: Int): (Int, String) = (delegate.multiply(a, b)._1, getClass.getSimpleName)
}

確認

あとはbeans.xmlを用意して

src/main/resources/META-INF/beans.xml

テストコードで確認していきます。
src/test/scala/org/littlewings/javaee/service/CdiScopeSpec.scala

package org.littlewings.javaee.service

import javax.enterprise.inject.spi.CDI

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

class CdiScopeSpec extends FunSpec with WeldSpecSupport {
  describe("CDI Scope spec") {
    // ここに、テストを書く!
  }
}

WeldSpecSupportというトレイトは、Weldの起動・停止のヘルパーメソッドを提供するちょっとした実装です。こんな感じで使います。

      withWeld {
        // ここで、CDIの機能を使ったコードを書く
      }

では、確認してみます。

まずはNormal Scope(@ApplicationScoped)。

    it("Normal Scope") {
      withWeld {
        val service1 = CDI.current.select(classOf[NormalScopedCalcService]).get
        val service2 = CDI.current.select(classOf[NormalScopedCalcService]).get

        service1 should be theSameInstanceAs (service2)
      }
    }

コンテナから2回CDI管理Beanを取得しましたが、同じインスタンスが取得できています。

擬似スコープ(@Dependent)。

    it("Pseudo Scope") {
      withWeld {
        val service1 = CDI.current.select(classOf[PseudoScopedCalcService]).get
        val service2 = CDI.current.select(classOf[PseudoScopedCalcService]).get

        service1 should not be theSameInstanceAs(service2)
      }
    }

こちらは、別人になるようですね。

Normal Scope(@ApplicationScoped)のCDI管理Beanに、擬似スコープ(@Dependent)のCDI管理BeanをInject。

    it("Normal Scope with Pseudo Scope") {
      withWeld {
        val service1 = CDI.current.select(classOf[NormalScopedMixedCalcService]).get
        val service2 = CDI.current.select(classOf[NormalScopedMixedCalcService]).get

        service1 should be theSameInstanceAs (service2)
        service1.delegate should be theSameInstanceAs (service2.delegate)
      }
    }

こちらは、Normal Scope、擬似スコープともに同じインスタンスになっています。こういうのを見ると、「インジェクション先のライフサイクルに準ずる」という挙動なんだなぁと思います。

擬似スコープ(@Dependent)のCDI管理Beanに、Normal Scope(@ApplicationScoped)のCDI管理BeanをInject。

    it("Pseudo Scope with Normal Scope") {
      withWeld {
        val service1 = CDI.current.select(classOf[PseudoScopedMixedCalcService]).get
        val service2 = CDI.current.select(classOf[PseudoScopedMixedCalcService]).get

        service1 should not be theSameInstanceAs (service2)
        service1.delegate should be theSameInstanceAs (service2.delegate)
      }
    }

外側(?)が擬似スコープのCDI管理Beanですが、こちらは別のインスタンス、インジェクションで内部に持っているNormal ScopeのCDI管理Beanは同じインスタンスですね。

追記
このケースも、気になったのでちょっと追加で確認してみました。

擬似スコープ(@Dependent)のCDI管理Beanに、擬似スコープ(@Dependent)のCDI管理BeanをInject。

    it("Pseudo Scope with Pseudo Scope") {
      withWeld {
        val service1 = CDI.current.select(classOf[PseudoScopedWithPseudoScopedMixedCalcService]).get
        val service2 = CDI.current.select(classOf[PseudoScopedWithPseudoScopedMixedCalcService]).get

        service1 should not be theSameInstanceAs (service2)
        service1.delegate should not be theSameInstanceAs (service2.delegate)
      }
    }

こちらは、外側も内部で持っているインスタンスも別人ですね。

Client Proxy?

続いて、これらのインスタンスを参照する時に、Client Proxy越しに見ているかどうかを確認してみます。

Normal Scope(@ApplicationScoped)。

    it("Normal Scope, Client Proxies?") {
      withWeld {
        val service = CDI.current.select(classOf[NormalScopedCalcService]).get

        service.add(1, 2) should be ((3, "NormalScopedCalcService"))

        service.getClass.getPackage.getName should be(classOf[NormalScopedCalcService].getPackage.getName)
        service.getClass.getSimpleName should be("NormalScopedCalcService$Proxy$_$$_WeldClientProxy")
        service.getClass.getSuperclass.getSimpleName should be("NormalScopedCalcService")
      }
    }

コンテナから取得したインスタンスのクラス名が「NormalScopedCalcService$Proxy$_$$_WeldClientProxy」となっていて、Client Proxy越しに見ているんだなぁということがわかる感じですね。また、足し算しているメソッドから戻ってきているクラス名は、あくまでご本人様のようで。

うん、プロキシ…。

擬似スコープ(@Dependent)。

    it("Pseudo Scope, Client Proxies?") {
      withWeld {
        val service = CDI.current.select(classOf[PseudoScopedCalcService]).get

        service.add(1, 2) should be ((3, "PseudoScopedCalcService"))

        service.getClass.getPackage.getName should be(classOf[PseudoScopedCalcService].getPackage.getName)
        service.getClass.getSimpleName should be("PseudoScopedCalcService")
      }
    }

こちらは、コンテナから取得したインスタンスもご本人様ですね。こっちはプロキシなしなんですね。

Normal Scope(@ApplicationScoped)のCDI管理Beanに、擬似スコープ(@Dependent)のCDI管理BeanをInject。

    it("Normal Scope with Pseudo Scope, Client Proxies?") {
      withWeld {
        val service = CDI.current.select(classOf[NormalScopedMixedCalcService]).get

        service.add(1, 2) should be ((3, "NormalScopedMixedCalcService"))

        service.getClass.getPackage.getName should be(classOf[NormalScopedMixedCalcService].getPackage.getName)
        service.getClass.getSimpleName should be("NormalScopedMixedCalcService$Proxy$_$$_WeldClientProxy")
        service.getClass.getSuperclass.getSimpleName should be("NormalScopedMixedCalcService")

        service.delegate.getClass.getPackage.getName should be(classOf[PseudoScopedCalcService].getPackage.getName)
        service.delegate.getClass.getSimpleName should be("PseudoScopedCalcService")
      }
    }

Normal ScopeのCDI管理Beanは、Client Proxy越しに、擬似スコープのCDI管理Beanはご本人様、と。

擬似スコープ(@Dependent)のCDI管理Beanに、Normal Scope(@ApplicationScoped)のCDI管理BeanをInject。

    it("Pseudo Scope with Normal Scope, Client Proxies?") {
      withWeld {
        val service = CDI.current.select(classOf[PseudoScopedMixedCalcService]).get

        service.add(1, 2) should be ((3, "PseudoScopedMixedCalcService"))

        service.getClass.getPackage.getName should be(classOf[PseudoScopedMixedCalcService].getPackage.getName)
        service.getClass.getSimpleName should be("PseudoScopedMixedCalcService")

        service.delegate.getClass.getPackage.getName should be(classOf[NormalScopedCalcService].getPackage.getName)
        service.delegate.getClass.getSimpleName should be("NormalScopedCalcService$Proxy$_$$_WeldClientProxy")
        service.delegate.getClass.getSuperclass.getSimpleName should be("NormalScopedCalcService")
      }
    }

単純に関係がひっくり返っただけですね。

JSRを見ているとNormal Scopeのものに関しては、Client Proxyが必要そうな感じです。擬似スコープは、そうでもない…?

Interceptorを付けてみる

こんな感じで、Normal ScopeであればClient Proxy越しのアクセスとなり、擬似スコープだとそうでもないようなので、ではここにInterceptorを足してみたらどうなるんだろうと思い、試してみました。

とりあえず、トレース用のアノテーション
src/main/java/org/littlewings/javaee/service/interceptor/Trace.java

package org.littlewings.javaee.service.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.annotation.Priority;
import javax.interceptor.Interceptor;
import javax.interceptor.InterceptorBinding;

@InterceptorBinding
@Inherited
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Priority(Interceptor.Priority.APPLICATION)
public @interface Trace {
}

Interceptorを作ってみます。
src/main/scala/org/littlewings/javaee/service/interceptor/TraceInterceptor.scala

package org.littlewings.javaee.service.interceptor

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

@Interceptor
@Trace
@Dependent
@SerialVersionUID(1L)
class TraceInterceptor extends Serializable {
  @AroundInvoke
  @throws(classOf[Exception])
  def invoke(ic: InvocationContext): Any = {
    println(s"[${ic.getTarget.getClass.getSuperclass.getSimpleName}] method[${ic.getMethod.getName}] start.")

    try {
      ic.proceed()
    } finally {
      println(s"[${ic.getTarget.getClass.getSuperclass.getSimpleName}] method[${ic.getMethod.getName}] end.")
    }
  }
}

で、このInterceptorを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.javaee.service.interceptor.TraceInterceptor</class>
    </interceptors>
</beans>

先ほどのCDI管理Beanに付与します。ここでは、クラスの宣言に付与しました。

ApplicationScopedのCDI管理Bean
src/main/scala/org/littlewings/javaee/service/interceptor/NormalScopedCalcService.scala

package org.littlewings.javaee.service.interceptor

import javax.enterprise.context.ApplicationScoped

@Trace
@ApplicationScoped
@SerialVersionUID(1L)
class NormalScopedCalcService extends Serializable {
  def add(a: Int, b: Int): (Int, String) = (a + b, getClass.getSimpleName)

  def multiply(a: Int, b: Int): (Int, String) = (a * b, getClass.getSimpleName)
}

擬似スコープ(@Dependent)のCDI管理Bean
src/main/scala/org/littlewings/javaee/service/interceptor/PseudoScopedCalcService.scala

package org.littlewings.javaee.service.interceptor

import javax.enterprise.context.Dependent

@Trace
@Dependent
@SerialVersionUID(1L)
class PseudoScopedCalcService extends Serializable {
  def add(a: Int, b: Int): (Int, String) = (a + b, getClass.getSimpleName)

  def multiply(a: Int, b: Int): (Int, String) = (a * b, getClass.getSimpleName)
}

ApplicationScopedのCDI管理Beanに、擬似スコープ(@Dependent)のCDI管理BeanをInject
src/main/scala/org/littlewings/javaee/service/interceptor/NormalScopedMixedCalcService.scala

package org.littlewings.javaee.service.interceptor

import javax.enterprise.context.ApplicationScoped
import javax.inject.Inject

@Trace
@ApplicationScoped
@SerialVersionUID(1L)
class NormalScopedMixedCalcService extends Serializable {
  @Inject
  var delegate: PseudoScopedCalcService = _

  def add(a: Int, b: Int): (Int, String) = (delegate.add(a, b)._1, getClass.getSimpleName)

  def multiply(a: Int, b: Int): (Int, String) = (delegate.multiply(a, b)._1, getClass.getSimpleName)
}

擬似スコープ(@Dependent)のCDI管理Beanに、ApplicationScopedのCDI管理BeanをInject
src/main/scala/org/littlewings/javaee/service/interceptor/PseudoScopedMixedCalcService.scala

package org.littlewings.javaee.service.interceptor

import javax.enterprise.context.Dependent
import javax.inject.Inject

@Trace
@Dependent
@SerialVersionUID(1L)
class PseudoScopedMixedCalcService extends Serializable {
  @Inject
  var delegate: NormalScopedCalcService = _

  def add(a: Int, b: Int): (Int, String) = (delegate.add(a, b)._1, getClass.getSimpleName)

  def multiply(a: Int, b: Int): (Int, String) = (delegate.multiply(a, b)._1, getClass.getSimpleName)
}

では、動作確認。今度は、クラスの型・名前などに注目して見ていきます。

Normal Scope(@ApplicationScoped)。

    it("Normal Scope") {
      withWeld {
        val service = CDI.current.select(classOf[NormalScopedCalcService]).get

        service.add(1, 2) should be((3, "NormalScopedCalcService$Proxy$_$$_WeldSubclass"))

        service.getClass.getPackage.getName should be(classOf[NormalScopedCalcService].getPackage.getName)
        service.getClass.getSimpleName should be("NormalScopedCalcService$Proxy$_$$_WeldClientProxy")
        service.getClass.getSuperclass.getSimpleName should be("NormalScopedCalcService")
      }
    }

外側はClient Proxyっぽいですが、実際に操作されたインスタンスもサブクラスっぽいものになっています。

なお、Interceptor自体もちゃんと動作しています。

[NormalScopedCalcService] method[add] start.
[NormalScopedCalcService] method[add] end.

擬似スコープ(@Dependent)。

    it("Pseudo Scope") {
      withWeld {
        val service = CDI.current.select(classOf[PseudoScopedCalcService]).get

        service.add(1, 2) should be((3, "PseudoScopedCalcService$Proxy$_$$_WeldSubclass"))

        service.getClass.getPackage.getName should be(classOf[PseudoScopedCalcService].getPackage.getName)
        service.getClass.getSimpleName should be("PseudoScopedCalcService$Proxy$_$$_WeldSubclass")
        service.getClass.getSuperclass.getSimpleName should be("PseudoScopedCalcService")
      }
    }

Client Proxyの確認をした後、このパターンが特に気になって確認してみたのですが、普通に動きました。Interceptorも動作します。

[PseudoScopedCalcService] method[add] start.
[PseudoScopedCalcService] method[add] end.

擬似スコープでClient Proxyができないのなら、Interceptorが使えるのかな?と思ったのですがそんなことはないみたいです。この場合、中身も外見も同じ人っぽいですが、「$Proxy$_$$_WeldClientProxy」というサフィックスのクラス名ではなく、「$Proxy$_$$_WeldSubclass」となっていますね。

ApplicationScopedのCDI管理Beanに、擬似スコープ(@Dependent)のCDI管理BeanをInject

    it("Normal Scope with Pseudo Scope") {
      val thrown =
        the[IllegalStateException] thrownBy CDI.current.select(classOf[NormalScopedMixedCalcService]).get
      thrown.getMessage should be ("Singleton not set for STATIC_INSTANCE => []")
    }

このパターンは、失敗するようになりました。擬似スコープの同一のインスタンスを突っ込めないみたいです…。

擬似スコープ(@Dependent)のCDI管理Beanに、ApplicationScopedのCDI管理BeanをInject

    it("Pseudo Scope with Normal Scope") {
      withWeld {
        val service = CDI.current.select(classOf[PseudoScopedMixedCalcService]).get

        service.add(1, 2) should be((3, "PseudoScopedMixedCalcService$Proxy$_$$_WeldSubclass"))

        service.getClass.getPackage.getName should be(classOf[PseudoScopedMixedCalcService].getPackage.getName)
        service.getClass.getSimpleName should be("PseudoScopedMixedCalcService$Proxy$_$$_WeldSubclass")
        service.getClass.getSuperclass.getSimpleName should be("PseudoScopedMixedCalcService")
      }
    }

こちらはOKでした。Interceptorも問題なし。

[PseudoScopedMixedCalcService] method[add] start.
[NormalScopedCalcService] method[add] start.
[NormalScopedCalcService] method[add] end.
[PseudoScopedMixedCalcService] method[add] end.

とりあえず今回試した範囲での挙動はなんとなくわかりましたが、@Dependentを実際使う時はどうなんでしょうね…。ある程度、インジェクションされうる先を想定して決めていく感じなのかなぁと。

というか、スコープを決められるものはNormal Scopeとしてアノテーションを付与して、そうでないものは@Dependentにする感じ?(振り出しに戻った感が…)

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