ちょっとCDIで気になったAPIがありまして、それを使ってみるついでに@Injectアノテーション以外でCDI管理Beanを取得する方法を少々書いてみました。
@Inject以外の方法で、要はAPI呼び出しでCDI管理Beanを取得したいケース…
- 実行時の条件に応じてBeanの種類を変えたい
- 実行時の条件に応じてQualifierを変えたい
- デプロイした時に、対応するBean、Qualifierがないかもしれない
- 登録されているBeanをイテレーションしたい
などらしいです(JSR-346 「5.6. Programmatic lookup」)。
今回は、JNDIを有効にしたEmbedded Tomcat+Weld Servlet+Scalaで実践してみます。
準備
ビルド定義。
build.sbt
name := "cdi-programmatic-lookup" version := "0.0.1-SNAPSHOT" scalaVersion := "2.11.6" organization := "org.littlewings" scalacOptions ++= Seq("-Xlint", "-deprecation", "-unchecked", "-feature") updateOptions := updateOptions.value.withCachedResolution(true) fork in Test := true val tomcatVersion = "8.0.22" val resteasyVersion = "3.0.11.Final" val weldServletVersion = "2.2.12.Final" val scalaTestVersion = "2.2.5" libraryDependencies ++= Seq( "org.apache.tomcat.embed" % "tomcat-embed-core" % tomcatVersion, "org.apache.tomcat.embed" % "tomcat-embed-jasper" % tomcatVersion, "org.apache.tomcat.embed" % "tomcat-embed-logging-juli" % tomcatVersion, "org.jboss.resteasy" % "resteasy-servlet-initializer" % resteasyVersion, "org.jboss.resteasy" % "resteasy-cdi" % resteasyVersion, "org.jboss.weld.servlet" % "weld-servlet" % weldServletVersion, "org.scalatest" %% "scalatest" % scalaTestVersion % "test" )
CDI有効化のために、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"> </beans>
JAX-RS有効化のためのクラス。
src/main/scala/org/littlewings/javaee7/rest/JaxrsApplication.scala
package org.littlewings.javaee7.rest import javax.ws.rs.ApplicationPath import javax.ws.rs.core.Application @ApplicationPath("rest") class JaxrsApplication extends Application
では、単純なパターンから書いていきます。
テストコードの雛形トレイト。テストクラス全体の開始時にEmbedded Tomcatを起動し、終了時にシャットダウンします。JNDIを有効化のうえ、CDIも利用可能にしています。
src/test/scala/org/littlewings/javaee7/EmbeddedTomcatCdiSupport.scala
package org.littlewings.javaee7 import java.io.File import org.apache.catalina.startup.Tomcat import org.apache.tomcat.util.descriptor.web.ContextResource import org.scalatest.{BeforeAndAfterAll, Suite} trait EmbeddedTomcatCdiSupport extends Suite with BeforeAndAfterAll { protected val port: Int = 8080 protected val tomcat: Tomcat = new Tomcat protected val baseDir: File = createTempDir("tomcat", port) protected val docBaseDir: File = createTempDir("tomcat-docbase", port) override def beforeAll(): Unit = { tomcat.setPort(port) tomcat.setBaseDir(baseDir.getAbsolutePath) val context = tomcat.addWebapp("", docBaseDir.getAbsolutePath) context.addParameter("org.jboss.weld.environment.servlet.archive.isolation", "false") context.addParameter("resteasy.injector.factory", "org.jboss.resteasy.cdi.CdiInjectorFactory") tomcat.enableNaming() val resource = new ContextResource resource.setAuth("Container") resource.setName("BeanManager") resource.setType("javax.enterprise.inject.spi.BeanManager") resource.setProperty("factory", "org.jboss.weld.resources.ManagerObjectFactory") context.getNamingResources.addResource(resource) tomcat.start() } override def afterAll(): Unit = { tomcat.stop() tomcat.destroy() deleteDirs(baseDir) deleteDirs(docBaseDir) } private def createTempDir(prefix: String, port: Int): File = { val tempDir = File.createTempFile(s"${prefix}.", s".${port}") tempDir.delete() tempDir.mkdir() tempDir.deleteOnExit() tempDir } private def deleteDirs(file: File): Unit = { file .listFiles .withFilter(f => f.getName != "." && f.getName != "..") .foreach { case d if d.isDirectory => deleteDirs(d) case f => f.delete() } file.delete() } }
Qualifierなしの場合
まずは、CDI管理Beanを用意しておきます。
src/main/scala/org/littlewings/javaee7/service/CalcService.scala
package org.littlewings.javaee7.service import javax.enterprise.context.RequestScoped @RequestScoped class CalcService { def add(a: Int, b: Int): Int = a + b }
このCDI管理Beanを、JAX-RSリソースクラスで@Inject以外の方法で取得して使用します。
src/main/scala/org/littlewings/javaee7/rest/CalcResource.scala
package org.littlewings.javaee7.rest import javax.enterprise.context.RequestScoped import javax.enterprise.inject.Instance import javax.enterprise.inject.spi.{Bean, BeanManager, CDI} import javax.inject.Inject import javax.naming.InitialContext import javax.ws.rs._ import javax.ws.rs.core.MediaType import org.littlewings.javaee7.service.CalcService @Path("calc") @RequestScoped class CalcResource { // ここで、CalcServiceを取得して使うコードを書く }
javax.enterprise.inject.spi.BeanManagerを使う
BeanManagerを使う方法から。BeanManager自体を、@Injectでインジェクションして使ってみます。
@Inject private var beanManager: BeanManager = _ @GET @Path("beanManagerInject") @Produces(Array(MediaType.TEXT_PLAIN)) def beanManagerInject(@QueryParam("a") @DefaultValue("0") a: Int, @QueryParam("b") @DefaultValue("0") b: Int): Int = { val beans = beanManager.getBeans(classOf[CalcService]) val bean = beanManager.resolve[CalcService](beans.asInstanceOf[java.util.Set[Bean[_ <: CalcService]]]) beanManager .getReference(bean, classOf[CalcService], beanManager.createCreationalContext(bean)) .asInstanceOf[CalcService] .add(a, b) }
ちなみに、BeanManagerをScalaから使うのは超面倒です…。
JNDIルックアップでBeanManagerを取得してもOKです。
@GET @Path("beanManagerLookup") @Produces(Array(MediaType.TEXT_PLAIN)) def beanManagerLookup(@QueryParam("a") @DefaultValue("0") a: Int, @QueryParam("b") @DefaultValue("0") b: Int): Int = { val ic = new InitialContext() val bm = ic.lookup("java:comp/env/BeanManager").asInstanceOf[BeanManager] ic.close() val beans = bm.getBeans(classOf[CalcService]) val bean = bm.resolve[CalcService](beans.asInstanceOf[java.util.Set[Bean[_ <: CalcService]]]) bm .getReference(bean, classOf[CalcService], bm.createCreationalContext(bean)) .asInstanceOf[CalcService] .add(a, b) }
このためだけに、JNDIを有効化しています…。
※Weld Servletを適用していれば、JNDIを使わなくても@Injectは使えるので
javax.enterprise.inject.spi.CDIを使う
CDI 1.1以降で使える?ちょっと便利なクラス。自分は、このクラスを使ってよくWeld SEを使った動作確認をしています。
@GET @Path("cdiUtil") @Produces(Array(MediaType.TEXT_PLAIN)) def cdiUtil(@QueryParam("a") @DefaultValue("0") a: Int, @QueryParam("b") @DefaultValue("0") b: Int): Int = CDI.current.select(classOf[CalcService]).get.add(a, b)
かなり簡単に使えます。CDI.current -> select -> getで。
このクラスから、BeanManagerを取得することも可能みたいです。
javax.enterprise.inject.Instanceを使う
今回は、これを試してみたくて書き始めたものになります。Instanceを@Injectで、対象のクラスに注入します。
@Inject private var calcServiceInstance: Instance[CalcService] = _ @GET @Path("instanceLookup") @Produces(Array(MediaType.TEXT_PLAIN)) def instanceLookup(@QueryParam("a") @DefaultValue("0") a: Int, @QueryParam("b") @DefaultValue("0") b: Int): Int = calcServiceInstance.select().get().add(a, b)
あとは、selectして対象のCDI管理Beanをgetします。
動作確認
これらに対する、テストコードです。
src/test/scala/org/littlewings/javaee7/CalcResourceSpec.scala
package org.littlewings.javaee7 import org.scalatest.FunSpec import org.scalatest.Matchers._ import scala.io.Source class CalcResourceSpec extends FunSpec with EmbeddedTomcatCdiSupport { describe("CalcResource Spec") { it("Inject BeanManager") { Source.fromURL("http://localhost:8080/rest/calc/beanManagerInject?a=1&b=2").mkString should be("3") } it("JNDI Lookup BeanManager") { Source.fromURL("http://localhost:8080/rest/calc/beanManagerLookup?a=1&b=2").mkString should be("3") } it("CDI Util") { Source.fromURL("http://localhost:8080/rest/calc/cdiUtil?a=1&b=2").mkString should be("3") } it("Instance Lookup") { Source.fromURL("http://localhost:8080/rest/calc/instanceLookup?a=1&b=2").mkString should be("3") } } }
Qualifierありの場合
今度は、Qualifierを合わせて使ってみます。@Qualifierを付与したアノテーションを用意。
src/main/java/org/littlewings/javaee7/qualifier/HelloScala.java
package org.littlewings.javaee7.qualifier; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import javax.inject.Qualifier; @Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PACKAGE, ElementType.TYPE}) public @interface HelloScala { }
もうひとつ。
src/main/java/org/littlewings/javaee7/qualifier/HelloGroovy.java
package org.littlewings.javaee7.qualifier; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import javax.inject.Qualifier; @Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PACKAGE, ElementType.TYPE}) public @interface HelloGroovy { }
Qualifierを適用するためのトレイトを用意。
src/main/scala/org/littlewings/javaee7/service/MessageService.scala
package org.littlewings.javaee7.service trait MessageService { def get: String }
あとは、デフォルトのもの、
src/main/scala/org/littlewings/javaee7/service/DefaultMessageService.scala
package org.littlewings.javaee7.service import javax.enterprise.context.RequestScoped @RequestScoped class DefaultMessageService extends MessageService { override def get: String = "Hello World" }
@HelloScalaアノテーションを付与したもの、
src/main/scala/org/littlewings/javaee7/service/ScalaMessageService.scala
package org.littlewings.javaee7.service import javax.enterprise.context.RequestScoped import org.littlewings.javaee7.qualifier.HelloScala @HelloScala @RequestScoped class ScalaMessageService extends MessageService { override def get: String = "Hello Scala" }
これらを、先ほどのBeanManager、CDI、Instanceを用いて取得してみます。
先ほどと同じく、これらのCDI管理Beanを取得して使うのは、JAX-RSリソースクラスということで。
src/main/scala/org/littlewings/javaee7/rest/MessageResource.scala
package org.littlewings.javaee7.rest import javax.enterprise.context.RequestScoped import javax.enterprise.inject.Instance import javax.enterprise.inject.spi.{Bean, BeanManager, CDI} import javax.enterprise.util.AnnotationLiteral import javax.inject.Inject import javax.ws.rs.core.MediaType import javax.ws.rs.{GET, Path, Produces} import org.littlewings.javaee7.qualifier.{HelloGroovy, HelloScala} import org.littlewings.javaee7.service.MessageService @Path("message") @RequestScoped class MessageResource { // ここで、MessageServiceを取得して使うコードを書く }
BeanManagerを使う
BeanManagerは、今回は@Injectで取得したものを使用します。JNDIは省略…。
@Inject private var beanManager: BeanManager = _
まずは、Qualifierを付けていないCDI管理Beanを取得。
@GET @Path("beanManagerDefault") @Produces(Array(MediaType.TEXT_PLAIN)) def beanManagerDefault: String = { val beans = beanManager.getBeans(classOf[MessageService]) val bean = beanManager.resolve[MessageService](beans.asInstanceOf[java.util.Set[Bean[_ <: MessageService]]]) beanManager .getReference(bean, classOf[MessageService], beanManager.createCreationalContext(bean)) .asInstanceOf[MessageService] .get }
続いて、Qualifierが付いたCDI管理Beanを取得する場合。今回は、@HelloScalaが付与されたものを取得してみます。
@GET @Path("beanManagerWithQualifier") @Produces(Array(MediaType.TEXT_PLAIN)) def beanManagerWithQualifier: String = { val beans = beanManager.getBeans(classOf[MessageService], new AnnotationLiteral[HelloScala] {}) val bean = beanManager.resolve[MessageService](beans.asInstanceOf[java.util.Set[Bean[_ <: MessageService]]]) beanManager .getReference(bean, classOf[MessageService], beanManager.createCreationalContext(bean)) .asInstanceOf[MessageService] .get }
ちょっとわかりにくいのは、AnnotationLiteralのインスタンスを渡してあげる必要があるところでしょうか。
val beans = beanManager.getBeans(classOf[MessageService], new AnnotationLiteral[HelloScala] {})
BeanManager#getBeansの後半の引数は、可変長のAnnotation型でしてね…。Class<Annotation>ではなく、インスタンスを渡せと。
これができると、他のパターンもほぼ同じ流れで取得できます。
CDIを使う
今度は、CDIを使って@HelloGroovyアノテーションを付与したクラスのインスタンスを取得してみます。
@GET @Path("cdiUtil") @Produces(Array(MediaType.TEXT_PLAIN)) def cdiUtil: String = CDI .current .select(classOf[MessageService], new AnnotationLiteral[HelloGroovy] {}) .get .get
ここでも、AnnotationLiteralが。
Instanceを使う
Instanceの場合はちょっと他と違って、@Anyを@Injectと共に付与してあげる必要があるみたいです。
@Inject @javax.enterprise.inject.Any private var messageServiceInstance: Instance[MessageService] = _ @GET @Path("instanceLookup") @Produces(Array(MediaType.TEXT_PLAIN)) def instanceLookup: String = messageServiceInstance .select(new AnnotationLiteral[HelloScala] {}) .get .get
Instanceを取得した後は、他と同じくAnnotationLiteralを使います。
動作確認
これらを使用した、テストコード。
src/test/scala/org/littlewings/javaee7/MessageResourceSpec.scala
package org.littlewings.javaee7 import org.scalatest.FunSpec import org.scalatest.Matchers._ import scala.io.Source class MessageResourceSpec extends FunSpec with EmbeddedTomcatCdiSupport { describe("MessageResource Spec") { it("BeanManager default") { Source.fromURL("http://localhost:8080/rest/message/beanManagerDefault").mkString should be("Hello World") } it("BeanManager with Qualifier") { Source.fromURL("http://localhost:8080/rest/message/beanManagerWithQualifier").mkString should be("Hello Scala") } it("CDI Util") { Source.fromURL("http://localhost:8080/rest/message/cdiUtil").mkString should be("Hello Groovy") } it("Instance Lookup") { Source.fromURL("http://localhost:8080/rest/message/instanceLookup").mkString should be("Hello Scala") } } }
OKです。
まとめ
これで、ある程度CDI管理Beanを手動で(@Injectを使って直接インジェクションするのではなく)取得する方法がわかったかなと思います。
AnnotationLiteralのサブクラスは、今回は端折って作ってしまいましたが、もしも本当に使うのならやっぱりちゃんとクラスとして定義した方がいいんでしょうかね。
今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/javaee7-scala-examples/tree/master/cdi-programmatic-lookup