以前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