以前、こんなエントリを書きました。
CDIでコンストラクタインジェクションしたい
http://d.hatena.ne.jp/Kazuhira/20150412/1428846124
ここで、@Typedアノテーションを使ってBeanを限定することができるということを教えていただきました。
で、これについてなのですが、JSR-346に書かれている「2.2. Bean types」(「2.2.1. Legal bean types」および「2.2.2. Restricting the bean types of a bean」)を見ると、もうちょっといろいろ継承関係のある例が書かれているので、これを自分で試してみることにしました。
@Typedアノテーションなし
public class BookShop extends Business implements Shop<Book> { ... }
この場合、すべての型(BookShop、Business、Shop<Book>)に対して、Beanとして登録されるそうです(あとObjectも)。
@Typedアノテーションあり
@Typed(Shop.class) public class BookShop extends Business implements Shop<Book> { ... }
この場合、BookShopクラスの登録範囲がShopインターフェースに絞られます。
JSRには、これらのクラスの具体的な定義は書かれていないので、自分で確認したいことができるような定義を適当に書きました。
準備
まずはビルド準備。
build.sbt
name := "cdi-typed" 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.11.Final", "org.scalatest" %% "scalatest" % "2.2.4" % "test" )
ビルドはScalaですが、とりあえず気にしない。
beans.xmlは明示的に用意しますが、Java SE環境で動かす場合にはbean-discovery-modeをannotatedに明示することにしました。
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>
これを書かないと、どうもbean-discovery-modeがallで動いてそうな気がします…。
テストコードの用意
最初に、テストで使うWeld SEを起動・停止するための簡易トレイトを作成。
src/test/scala/org/littlewings/javaee7/WeldSpecSupport.scala
package org.littlewings.javaee7 import org.jboss.weld.environment.se.Weld import org.scalatest.Suite trait WeldSpecSupport extends Suite { protected def withWeld(f: => Unit): Unit = { val weld = new Weld try { weld.initialize() f } finally { weld.shutdown() } } }
各テストメソッド内では、このメソッドに関数を渡してテストを行います。
@Typedアノテーションを使わない、単純なパターン
まず用意したのは、以下のクラス。これをCDI管理Beanとして使用します。
src/main/scala/org/littlewings/javaee7/BookShop.scala
package org.littlewings.javaee7 import javax.enterprise.context.ApplicationScoped class Book trait Shop[T] class Business @ApplicationScoped class BookShop extends Business with Shop[Book]
JSRの例と違うのは、BookShopクラスに明示的に@ApplicationScopedアノテーションを付与しました。
Businessクラスは抽象クラスにするか迷いましたが、今回は具象クラスとして定義することにしました。
これに対するテストコードは、こちら。
src/test/scala/org/littlewings/javaee7/BookShopSpec.scala
package org.littlewings.javaee7 import javax.enterprise.inject.spi.CDI import javax.enterprise.util.TypeLiteral import org.scalatest.FunSpec import org.scalatest.Matchers._ class BookShopSpec extends FunSpec with WeldSpecSupport { describe("Non Scoped Non Typed Spec") { it("select BookShop type") { withWeld { val bookShop = CDI.current.select(classOf[BookShop]).get bookShop should not be (null) bookShop should be(a[BookShop]) bookShop should be(a[Business]) bookShop should be(a[Shop[_]]) } } it("select Business type") { withWeld { val business = CDI.current.select(classOf[Business]).get business should be(a[BookShop]) business should be(a[Business]) business should be(a[Shop[_]]) } } it("select Shop type") { withWeld { val shop = CDI.current.select(new TypeLiteral[Shop[Book]] {}).get shop should not be (null) shop should be(a[BookShop]) shop should be(a[Business]) shop should be(a[Shop[_]]) } } } }
このパターンだと、どの型を指定してBeanを取得してもBookShopクラスを指すので、全部動くようです。
なお、Shop<Book>を指定するのは少々大変で、これにけっこうな時間ハマっていました。ジェネリクスが使われているものをインジェクションではない方法で取得する場合(CDIユーティリティやBeanManagerの利用)には、TypeLiteralが必要なんですね。
val shop = CDI.current.select(new TypeLiteral[Shop[Book]] {}).get
これを調べていく過程で、@Qualifierを指定する場合は、AnnotationLiteralが必要なこともわかりました。
なるほどー。
Businessクラスも@ApplicationScopedを付与してみる
次のパターン。今度は、継承元であるBusinessクラスもCDI管理Beanとして登録し、何が起こるか見てみましょう。
src/main/scala/org/littlewings/javaee7/scoped/BookShop.scala
package org.littlewings.javaee7.scoped import javax.enterprise.context.ApplicationScoped class Book trait Shop[T] @ApplicationScoped class Business @ApplicationScoped class BookShop extends Business with Shop[Book]
先ほどと変わったのは、Businessクラスに@ApplicationScopedアノテーションが付与されただけです。
これに対するテストコードと結果は、こちら。
src/test/scala/org/littlewings/javaee7/scoped/ScopedBookShopTestSpec.scala
package org.littlewings.javaee7.scoped import javax.enterprise.inject.AmbiguousResolutionException import javax.enterprise.inject.spi.CDI import javax.enterprise.util.TypeLiteral import org.littlewings.javaee7.WeldSpecSupport import org.scalatest.FunSpec import org.scalatest.Matchers._ class ScopedBookShopTestSpec extends FunSpec with WeldSpecSupport { describe("With Scoped, Non Typed Spec") { it("select BookShop type") { withWeld { val bookShop = CDI.current.select(classOf[BookShop]).get bookShop should not be (null) bookShop should be(a[BookShop]) bookShop should be(a[Business]) bookShop should be(a[Shop[_]]) } } it("select Business type") { withWeld { val thrown = the[AmbiguousResolutionException] thrownBy CDI.current.select(classOf[Business]).get thrown.getMessage should include("Cannot resolve an ambiguous dependency between") thrown.getMessage should include("Managed Bean [class org.littlewings.javaee7.scoped.Business] with qualifiers [@Any @Default]") thrown.getMessage should include("Managed Bean [class org.littlewings.javaee7.scoped.BookShop] with qualifiers [@Any @Default]") } } it("select Shop type") { withWeld { val shop = CDI.current.select(new TypeLiteral[Shop[Book]] {}).get shop should not be (null) shop should be(a[BookShop]) shop should be(a[Business]) shop should be(a[Shop[_]]) } } } }
予想通りといえばそうなのですが、Businessクラスは取得対象が重複してしまうため、例外がスローされますと。
@Typedアノテーションを付与する
最後は、JSRに載っていたものと同じパターン。
src/main/scala/org/littlewings/javaee7/typed/BookShop.scala
package org.littlewings.javaee7.typed import javax.enterprise.context.ApplicationScoped import javax.enterprise.inject.Typed class Book trait Shop[T] class Business @Typed(Array(classOf[Shop[Book]])) @ApplicationScoped class BookShop extends Business with Shop[Book]
BookShopに@ApplicationScopedと@Typedアノテーションを付与しました。JSRの例だと、@Typedに指定してあるShop.classに型パラメータを書いていませんが、まあそれはScalaで書いてあるが故でして…。
これに対するテストコードと結果は、こちら。
src/test/scala/org/littlewings/javaee7/typed/TypedBookShopSpec.scala
package org.littlewings.javaee7.typed import javax.enterprise.inject.UnsatisfiedResolutionException import javax.enterprise.inject.spi.CDI import javax.enterprise.util.TypeLiteral import org.littlewings.javaee7.WeldSpecSupport import org.scalatest.FunSpec import org.scalatest.Matchers._ class TypedBookShopSpec extends FunSpec with WeldSpecSupport { describe("Non Typed Spec") { it("select BookShop type") { withWeld { val thrown = the[UnsatisfiedResolutionException] thrownBy CDI.current.select(classOf[BookShop]).get thrown.getMessage should include("Unable to resolve any beans for Type: class org.littlewings.javaee7.typed.BookShop; Qualifiers: []") } } it("select Business type") { withWeld { val thrown = the[UnsatisfiedResolutionException] thrownBy CDI.current.select(classOf[Business]).get thrown.getMessage should include("Unable to resolve any beans for Type: class org.littlewings.javaee7.typed.Business; Qualifiers: []") } } it("select Shop type") { withWeld { val shop = CDI.current.select(new TypeLiteral[Shop[Book]] {}).get shop should not be (null) shop should not be(a[BookShop]) shop should not be(a[Business]) shop should be(a[Shop[_]]) } } } }
今度は、だいぶ結果が変わります。BookShopおよびBusinessではCDI管理Beanを取得できなくなります(@Typedで絞っているからそりゃそうだという感じですが)。
また、Shop<Book>で取得したCDI管理Beanは、BookShopやBusinessへのキャストができなくなります。
val shop = CDI.current.select(new TypeLiteral[Shop[Book]] {}).get shop should not be (null) shop should not be(a[BookShop]) shop should not be(a[Business]) shop should be(a[Shop[_]])
作成されるClient Proxyの型階層が変わったということですね…。
ちなみにこれ、BookShopに付与するアノテーションを@Dependentにした場合
@Typed(Array(classOf[Shop[Book]])) @Dependent class BookShop extends Business with Shop[Book]
取得できるCDI管理Beanが本人(Client Proxyではなくなる)になるので、型判定の結果が他のパターンと同じになります。
val shop = CDI.current.select(new TypeLiteral[Shop[Book]] {}).get shop should not be (null) shop should be(a[BookShop]) shop should be(a[Business]) shop should be(a[Shop[_]])
本線とはちょっと外れたTypeLiteralでだいぶハマりましたが、とりあえず意味的には再確認できたかなと思います。
今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/javaee7-scala-examples/tree/master/cdi-typed