CLOVER🍀

That was when it all began.

CDI Extensionでコンテナ初期化処理のイベントを受け取る

以前Undertow+JAX-RS+CDIという組み合わせで遊んだのですが、この時JAX-RSのリソースクラスをスキャンする人が誰もおらず、自分でクラスをひとつひとつ登録する羽目になりました。

この時、Hammockというライブラリを教えてもらったのですが、こちらはそういうことをしておらず、別の方法でJAX-RSリソースクラスを収集していました。

Hammock
https://github.com/johnament/hammock

ここでHammockが使っていたのが、CDI Extensionでした。

CDI、Scope関係のアノテーション付けたり、Produces使ったりしてるだけなので、その他は全然知らないですねぇ…。

せっかくなので、自分でも試してみることにしました。CDIの実装にはWeldを使い、Java SE環境で動作させます。

CDI Extensionって?

JSR-346に書かれている

JSR 346: Contexts and Dependency Injection for JavaTM EE 1.1
https://jcp.org/en/jsr/detail?id=346

「11.5. Container lifecycle events」を見てみると、コンテナ初期化処理中にイベントを受け取ることができるもののようです。

この他、こちらも参考にしました。

Chapter 16. Portable extensions
http://docs.jboss.org/weld/reference/latest-2.2/en-US/html_single/#extend

CDI 1.1 university
http://www.slideshare.net/antoinesd/cdi-11-university

イベントには、「Application lifecycle events」と「Bean discovery events」があるようです。

Application lifecycle eventsは、こちら。

  • BeforeBeanDiscovery
  • AfterTypeDiscovery
  • AfterBeanDiscovery
  • AfterDeploymentValidation
  • BeforeShutdown

Bean discovery eventsは、こちら。

  • ProcessAnnotatedType
  • ProcessInjectionPoint
  • ProcessInjectionTarget
  • ProcessBeanAttributes
  • ProcessBean
  • ProcessProducer
  • ProcessObserverMethod

今回は、ProcessAnnotatedTypeを使ってみます。なお、各イベントの意味はJSRを参照のこと…。11章、12章あたりを…。

ProcessAnnotatedTypeを受け取るようにすることで、コンテナ初期化中に管理BeanのClassクラスを受け取ることができるようになります。

CDI Extensionを実装する

このコンテナのイベントを受け取るには、javax.enterprise.inject.spi.Extensionインターフェースを実装する必要があります。このExtensionインターフェースには、実装すべきメソッドは特にありません。特定のアノテーションを付与したメソッドを、自分で定義します。

まずは、依存関係とプロジェクトの設定。Scalaです。
build.sbt

name := "cdi-extension"

version := "0.0.1-SNAPSHOT"

organization := "org.littlewings"

scalaVersion := "2.11.6"

scalacOptions ++= Seq("-Xlint", "-deprecation", "-unchecked", "-feature")

updateOptions := updateOptions.value.withCachedResolution(true)

libraryDependencies ++= Seq(
  "org.jboss.weld.se" % "weld-se" % "2.2.10.SP1",
  "javax" % "javaee-web-api" % "7.0",
  "org.scalatest" %% "scalatest" % "2.2.4" % "test"
)

CDIの実装にはWeld、作成するクラスに付与するアノテーションに付けるために、Java EE 7 Web Profileを依存関係に引き込んでいます。テストは、ScalaTestで。

では、Extensionを実装します。
src/main/scala/org/littlewins/javaee7/extension/MyExtension.scala

package org.littlewings.javaee7.extension

import scala.collection.mutable

import javax.enterprise.event.Observes
import javax.enterprise.inject.spi._

class MyExtension extends Extension {
  val classes: mutable.Set[Class[_]] = mutable.Set.empty

  def collectClasses(@Observes pat: ProcessAnnotatedType[_]): Unit = {
    val annotatedType = pat.getAnnotatedType
    classes += annotatedType.getJavaClass
  }
}

イベントを受け取るメソッドには、@Observesアノテーションを引数に付与します。今回は、ProcessAnnotatedTypeを受け取りここから対象のClassやMethodなどを受け取るものとします。

とりあえず、今回は全クラスをSetに収集することにしました。

Extensionを作成したら、「META-INF/services/javax.enterprise.inject.spi.Extension」というファイルを作り、その中に作成したクラスのFQCNを書いておきます。
src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension

org.littlewings.javaee7.extension.MyExtension

これで、Extensionが使えます。

では、スキャンされるクラスを用意します。

////////////////////////////////////////////////
// JAX-RSのリソースクラス
package org.littlewings.javaee7.rest

import javax.ws.rs.{ GET, Path, Produces }
import javax.ws.rs.core.MediaType

@Path("top")
class TopResource {
  @GET
  @Path("hello")
  @Produces(Array(MediaType.TEXT_PLAIN))
  def hello: String = "Hello World"
}

////////////////////////////////////////////////
// JAX-RSのリソースクラス
package org.littlewings.javaee7.rest.sub

import javax.ws.rs.{ POST, Path, Produces }
import javax.ws.rs.core.MediaType

@Path("sub")
class SubResource {
  @POST
  @Path("hello")
  @Produces(Array(MediaType.TEXT_PLAIN))
  def hello: String = "Hello World"
}

////////////////////////////////////////////////
// JPAのEntityクラス
package org.littlewings.javaee7.entity

import javax.persistence.Entity

@Entity
class TopEntity

////////////////////////////////////////////////
// JPAのEntityクラス
package org.littlewings.javaee7.entity.sub

import javax.persistence.Entity

@Entity
class SubEntity

////////////////////////////////////////////////
// Extensionをインジェクションするクラス
package org.littlewings.javaee7.service

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

import org.littlewings.javaee7.extension.MyExtension

@ApplicationScoped
class ExtensionService {
  @Inject
  private var myExtension: MyExtension = null

  def extensionClasses: Set[Class[_]] = Set.empty ++ myExtension.classes
}

最後に変なのが混じっていますが、こちらはExtensionもインジェクションする対象に含めることができることを示しています。

もう少しExtensionを書く

ところで、先ほど実装したExtensionでは、全クラスを対象にイベントを受け取ってしまいます。

もう少し、受け取る対象を絞り込んだExtensionも作ってみましょう。

JPAの@Entityアノテーションが付与されたものを対象にするExtension。
src/main/scala/org/littlewins/javaee7/extension/EntityExtension.scala

package org.littlewings.javaee7.extension

import scala.collection.mutable

import javax.enterprise.event.Observes
import javax.enterprise.inject.spi._
import javax.persistence.Entity

class EntityExtension extends Extension {
  val entityClasses: mutable.Set[Class[_]] = mutable.Set.empty

  def collectEntity(@Observes @WithAnnotations(Array(classOf[Entity])) pat: ProcessAnnotatedType[_]): Unit = {
    val annotatedType = pat.getAnnotatedType
    entityClasses += annotatedType.getJavaClass
  }
}

@Observesに加えて、@WithAnnotationsアノテーションを付与してEntityアノテーションが付いたものを対象にすることを定義します。

あとは、@POSTアノテーションでも。
src/main/scala/org/littlewins/javaee7/extension/PostExtension.scala

package org.littlewings.javaee7.extension

import scala.collection.mutable

import javax.enterprise.event.Observes
import javax.enterprise.inject.spi._
import javax.ws.rs.POST

class PostExtension extends Extension {
  val postClasses: mutable.Set[Class[_]] = mutable.Set.empty

  def collectEntity(@Observes @WithAnnotations(Array(classOf[POST])) pat: ProcessAnnotatedType[_]): Unit = {
    val annotatedType = pat.getAnnotatedType
    postClasses += annotatedType.getJavaClass
  }
}

まあ、中身はほとんど一緒なんですけど…。でも、これで分かるのはメソッドに付与されたアノテーションも対象にできるかどうか、ですね。

「META-INF/services/javax.enterprise.inject.spi.Extension」ファイルも、中身を追加。
src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension

org.littlewings.javaee7.extension.MyExtension
org.littlewings.javaee7.extension.EntityExtension
org.littlewings.javaee7.extension.PostExtension

あとは、CDIを使うために、空のbeans.xmlを作成しておきます。

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

動かしてみる

それでは、ここまで作成したコードを使ってテストして確認してみます。

テストコードの骨格は、このように。
src/test/scala/org/littlewins/javaee7/extension/CdiExtensionSpec.scala

package org.littlewings.javaee7.extension

import javax.enterprise.inject.spi.CDI

import org.jboss.weld.environment.se.Weld

import org.littlewings.javaee7.entity._
import org.littlewings.javaee7.entity.sub._
import org.littlewings.javaee7.rest._
import org.littlewings.javaee7.rest.sub._
import org.littlewings.javaee7.service._

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

class CdiExtensionSpec extends FunSpec {
  describe("CDI Extension Spec") {
    // ここに、テストを書く!
  }
}

まずは、最初に作ったExtensionを使ってテスト。

    it("Simple CDI Extension case") {
      val weld = new Weld
      val container = weld.initialize()

      // Exension自身をCDIで取得できる
      val myExtension = CDI.current.select(classOf[MyExtension]).get

      myExtension.classes should contain allOf (
        classOf[TopResource],
        classOf[SubResource],
        classOf[TopEntity],
        classOf[SubEntity],
        classOf[ExtensionService],
        classOf[MyExtension]  // Extension自身が入っている!
      )

      // 他の管理BeanにExtensionをインジェクション可能
      val extensionService = CDI.current.select(classOf[ExtensionService]).get
      extensionService.extensionClasses should contain allOf(
        classOf[TopResource],
        classOf[SubResource],
        classOf[TopEntity],
        classOf[SubEntity],
        classOf[ExtensionService],
        classOf[MyExtension]
      )

      weld.shutdown()
    }

Weldの初期化後、Extension自身をCDIで取得することができます。

      // Exension自身をCDIで取得できる
      val myExtension = CDI.current.select(classOf[MyExtension]).get

Setに収集したクラスを確認すると、期待通り作成したクラスが入っています。

      myExtension.classes should contain allOf (
        classOf[TopResource],
        classOf[SubResource],
        classOf[TopEntity],
        classOf[SubEntity],
        classOf[ExtensionService],
        classOf[MyExtension]  // Extension自身が入っている!
      )

なんと、Extension自身まで。

Extensionをインジェクションした管理Beanを取得しても、同様のことが確認できます。

      // 他の管理BeanにExtensionをインジェクション可能
      val extensionService = CDI.current.select(classOf[ExtensionService]).get
      extensionService.extensionClases should contain allOf(
        classOf[TopResource],
        classOf[SubResource],
        classOf[TopEntity],
        classOf[SubEntity],
        classOf[ExtensionService],
        classOf[MyExtension]
      )

なお、このExtensionを使用すると、Weldから以下のように警告されます。

WARN: WELD-000411: Observer method [BackedAnnotatedMethod] public org.littlewings.javaee7.extension.MyExtension.collectClasses(@Observes ProcessAnnotatedType<Object>) receives events for all annotated types. Consider restricting events using @WithAnnotations or a generic type with bounds.

アノテーションが付与された型を全部受け取るよ?いいの?と言っているようです。@WithAnnotationsアノテーションか、ジェネリクスを使えとも言っていますね。

あれ?ということは、ProcessAnnotatedTypeの型パラメータを変えたら型でも絞り込めたりするのかな…?(ここは未確認)

続いて、次のExtension。JPAの@Entityアノテーションを@WithAnnotationsで絞り込んだもの。

    it("Annotation Type Spec CDI Extension case") {
      val weld = new Weld
      val container = weld.initialize()

      val entityExtension = CDI.current.select(classOf[EntityExtension]).get

      entityExtension.entityClasses should contain allOf(
        classOf[TopEntity],
        classOf[SubEntity]
      )

      entityExtension.entityClasses should contain noneOf(
        classOf[TopResource],
        classOf[SubResource],
        classOf[ExtensionService],
        classOf[MyExtension],
        classOf[EntityExtension]
      )

      weld.shutdown()
    }

JPAのEntityのみが収集対象になりました。その他は(noneOfで確認しているので)除外されています。

@POSTアノテーションを対象にしたExtension。

    it("Annotation Method Spec CDI Extension case") {
      val weld = new Weld
      val container = weld.initialize()

      val postExtension = CDI.current.select(classOf[PostExtension]).get

      postExtension.postClasses should contain (
        classOf[SubResource]
      )

      postExtension.postClasses should contain noneOf(
        classOf[TopEntity],
        classOf[SubEntity],
        classOf[TopResource],
        classOf[ExtensionService],
        classOf[MyExtension],
        classOf[EntityExtension],
        classOf[PostExtension]
      )

      weld.shutdown()
    }

こちらも、期待通りになりました。

まとめ

CDIのExtensionを使って、意外と簡単に初期処理とクラスのスキャン(というかCDIのイベント)を受け取れることがわかりました。とりあえずWeldを起動すれば有効になりそうなので、導入は簡単そうですが。

あとは、他のイベントも見てみるとかですかねぇ。CDIをもうちょっと勉強しようかなぁという気になりました。

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