CLOVER🍀

That was when it all began.

CDI管理BeanをInjectアノテーションではなく、手動で取得する

ちょっとCDIで気になったAPIがありまして、それを使ってみるついでに@Injectアノテーション以外でCDI管理Beanを取得する方法を少々書いてみました。

@Inject以外の方法で、要はAPI呼び出しでCDI管理Beanを取得したいケース…

  • 実行時の条件に応じてBeanの種類を変えたい
  • 実行時の条件に応じてQualifierを変えたい
  • デプロイした時に、対応するBean、Qualifierがないかもしれない
  • 登録されているBeanをイテレーションしたい

などらしいです(JSR-346 「5.6. Programmatic lookup」)。

今回は、JNDIを有効にしたEmbedded Tomcat+Weld ServletScalaで実践してみます。

準備

ビルド定義。
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"
)

サーバー側は、JAX-RSで記述。

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 {
}

アノテーションは、Javaで書きます…。

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