CLOVER🍀

That was when it all began.

CDIでの継承関係とTypedアノテーションと

以前、こんなエントリを書きました。

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