CLOVER🍀

That was when it all began.

CDIのInjectionPointを使ってみる

時々ブログや本で目にする、CDIのInjectionPointを使ってそこから取れる情報を見てみました。

よくLoggerを使った例を見かけるので、自分もそれに習って気になるところを試してみます。

InjectionPointを使うと、依存関係を注入する場所のメタデータにアクセスできるようになるようです。

準備

まずは、ビルドの定義。
build.sbt

name := "cdi-injectionpoint"

version := "0.0.1-SNAPSHOT"

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.12.Final",
  "org.jboss.logging" % "jboss-logging" % "3.3.0.Final",
  "org.slf4j" % "slf4j-api" % "1.7.12",
  "ch.qos.logback" % "logback-classic" % "1.1.3",
  "org.scalatest" %% "scalatest" % "2.2.5" % "test"
)

Weld SEでやります。また、LoggerはJBoss Logging(裏はSLF4J+Logback)とします。

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>

あとは、Logbackの設定を。
src/main/resources/logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%msg%n</pattern>
        </encoder>
    </appender>

    <root level="info">
        <appender-ref ref="STDOUT"/>
    </root>
</configuration>

単純な例から

それでは、コードを書いていきます。まずは、LoggerのProducerを作成。
src/main/scala/org/littlewings/javaee7/cdi/LoggerProducer.scala

package org.littlewings.javaee7.cdi

import javax.enterprise.context.Dependent
import javax.enterprise.inject.Produces
import javax.enterprise.inject.spi.InjectionPoint

import org.jboss.logging.Logger

@Dependent
class LoggerProducer {
  @Produces
  def createLogger(injectionPoint: InjectionPoint): Logger = {
    val logger = Logger.getLogger(injectionPoint.getMember.getDeclaringClass)

    logger.infof("annotated = %s", injectionPoint.getAnnotated)
    logger.infof("annotated.annotations = %s", injectionPoint.getAnnotated.getAnnotations)
    logger.infof("annotated.baseType = %s", injectionPoint.getAnnotated.getBaseType)
    logger.infof("annotated.typeClosure = %s", injectionPoint.getAnnotated.getTypeClosure)
    logger.infof("bean = %s", injectionPoint.getBean)
    logger.infof("bean.beanClass = %s", injectionPoint.getBean.getBeanClass)
    logger.infof("bean.injectionPoints = %s", injectionPoint.getBean.getInjectionPoints)
    logger.infof("bean.name = %s", injectionPoint.getBean.getName)
    logger.infof("bean.qualifier = %s", injectionPoint.getBean.getQualifiers)
    logger.infof("bean.scope = %s", injectionPoint.getBean.getScope)
    logger.infof("bean.stereotypes = %s", injectionPoint.getBean.getStereotypes)
    logger.infof("bean.types = %s", injectionPoint.getBean.getTypes)
    logger.infof("member = %s", injectionPoint.getMember)
    logger.infof("member.declaringClass = %s", injectionPoint.getMember.getDeclaringClass)
    logger.infof("member.modifiers = %s", injectionPoint.getMember.getModifiers)
    logger.infof("member.name = %s", injectionPoint.getMember.getName)
    logger.infof("qualifier = %s", injectionPoint.getQualifiers)
    logger.infof("type = %s", injectionPoint.getType)
    logger.infof("delegate = %b", injectionPoint.isDelegate)
    logger.infof("transient = %b", injectionPoint.isTransient)

    logger
  }
}

せっかくなので、アクセス時にいろいろログ出力するようにしてみました。

で、このLoggerを@InjectするCDI管理Bean。
src/main/scala/org/littlewings/javaee7/cdi/CalcService.scala

package org.littlewings.javaee7.cdi

import javax.enterprise.context.{ApplicationScoped, Dependent}
import javax.inject.Inject

import org.jboss.logging.Logger

trait CalcService {
  def add(a: Int, b: Int): Int
}

@Dependent
class SimpleCalcService extends CalcService {
  @Inject
  private var logger: Logger = _

  override def add(a: Int, b: Int): Int = {
    val result = a + b
    logger.infof("%d + %d = %d", a, b, result)
    result
  }
}

インジェクションしたLoggerは、一応使うようにしています。

また、後のために、インターフェース(トレイト)を定義しています。

では、テストコードで動作確認。
src/test/scala/org/littlewings/javaee7/cdi/InjectionPointSpec.scala

package org.littlewings.javaee7.cdi

import javax.enterprise.inject.spi.CDI
import javax.enterprise.util.AnnotationLiteral

import org.jboss.logging.Logger
import org.jboss.weld.environment.se.Weld
import org.scalatest.FunSpec
import org.scalatest.Matchers._

class InjectionPointSpec extends FunSpec {
  describe("InjectionPoint Spec") {
    it("trace simple") {
      withWeld {
        val calcService = CDI.current.select(classOf[CalcService]).get
        calcService.add(2, 3) should be(5)
      }
    }
  }

  protected def withWeld(f: => Unit): Unit = {
    val weld = new Weld

    try {
      weld.initialize()

      f
    } finally {
      weld.shutdown()
    }
  }
}

結果は、このように。Producerが出力したログを貼り付けています。

annotated = [BackedAnnotatedField] @Inject private org.littlewings.javaee7.cdi.SimpleCalcService.logger
annotated.annotations = [@javax.inject.Inject()]
annotated.baseType = class org.jboss.logging.Logger
annotated.typeClosure = [interface org.jboss.logging.BasicLogger, class java.lang.Object, class org.jboss.logging.Logger, interface java.io.Serializable]
bean = Managed Bean [class org.littlewings.javaee7.cdi.SimpleCalcService] with qualifiers [@Any @Default]
bean.beanClass = class org.littlewings.javaee7.cdi.SimpleCalcService
bean.injectionPoints = [[BackedAnnotatedField] @Inject private org.littlewings.javaee7.cdi.SimpleCalcService.logger]
bean.name = null
bean.qualifier = [@javax.enterprise.inject.Any(), @javax.enterprise.inject.Default()]
bean.scope = interface javax.enterprise.context.Dependent
bean.stereotypes = []
bean.types = [interface org.littlewings.javaee7.cdi.CalcService, class org.littlewings.javaee7.cdi.SimpleCalcService, class java.lang.Object]
member = private org.jboss.logging.Logger org.littlewings.javaee7.cdi.SimpleCalcService.logger
member.declaringClass = class org.littlewings.javaee7.cdi.SimpleCalcService
member.modifiers = 2
member.name = logger
qualifier = [@javax.enterprise.inject.Default()]
type = class org.jboss.logging.Logger
delegate = false
transient = false

各項目の意味は…出力結果を見ようという感じですが…ちょこっと書いておきます。

  • InjectionPoint#getAnnotated … インジェクションされる場所のアノテーションに関する情報が取得可能
  • InjectionPoint#getBean … インジェクションされるjavax.enterprise.inject.spi.Beanについての情報が取得可能
  • InjectionPoint#getMember … インジェクションされるFieldやMethodなどが取得可能
  • InjectionPoint#getType および getQualifier … インジェクションされる型情報および@Qualifierが取得可能
  • InjectionPoint#isDelegate … Decoratorが使われているかどうか
  • InjectionPoint#isTransient … インジェクションされる場所が、transientかどうか

Loggerで使うなら、InjectionPoint#getMember#getDeclaringClassとかでしょうか。

@Qualifierおよび@Stereotypeを使う

続いて、@Qualifierやjavax.enterprise.inject.spi.Beanから取得できる@Stereotypeがあることから、これらについても試してみます。

アノテーションを定義。@Qualifier。
src/main/java/org/littlewings/javaee7/cdi/ServiceQualifier.java

package org.littlewings.javaee7.cdi;

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

@Stereotype。
src/main/java/org/littlewings/javaee7/cdi/MyStereotype.java

package org.littlewings.javaee7.cdi;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.enterprise.inject.Alternative;
import javax.enterprise.inject.Stereotype;

@Inherited
@Stereotype
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyStereotype {
}

Loggerに付与するQualifier。
src/main/java/org/littlewings/javaee7/cdi/LoggerQualifier.java

package org.littlewings.javaee7.cdi;

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

Producer。

  @LoggerQualifier
  @Produces
  def createLoggerWithQualifier(injectionPoint: InjectionPoint): Logger = {
    val logger = Logger.getLogger(injectionPoint.getMember.getDeclaringClass)

    logger.infof("annotated = %s", injectionPoint.getAnnotated)
    logger.infof("annotated.annotations = %s", injectionPoint.getAnnotated.getAnnotations)
    logger.infof("annotated.baseType = %s", injectionPoint.getAnnotated.getBaseType)
    logger.infof("annotated.typeClosure = %s", injectionPoint.getAnnotated.getTypeClosure)
    logger.infof("bean = %s", injectionPoint.getBean)
    logger.infof("bean.beanClass = %s", injectionPoint.getBean.getBeanClass)
    logger.infof("bean.injectionPoints = %s", injectionPoint.getBean.getInjectionPoints)
    logger.infof("bean.name = %s", injectionPoint.getBean.getName)
    logger.infof("bean.qualifier = %s", injectionPoint.getBean.getQualifiers)
    logger.infof("bean.scope = %s", injectionPoint.getBean.getScope)
    logger.infof("bean.stereotypes = %s", injectionPoint.getBean.getStereotypes)
    logger.infof("bean.types = %s", injectionPoint.getBean.getTypes)
    logger.infof("member = %s", injectionPoint.getMember)
    logger.infof("member.declaringClass = %s", injectionPoint.getMember.getDeclaringClass)
    logger.infof("member.modifiers = %s", injectionPoint.getMember.getModifiers)
    logger.infof("member.name = %s", injectionPoint.getMember.getName)
    logger.infof("qualifier = %s", injectionPoint.getQualifiers)
    logger.infof("type = %s", injectionPoint.getType)
    logger.infof("delegate = %b", injectionPoint.isDelegate)
    logger.infof("transient = %b", injectionPoint.isTransient)

    logger
  }

CDI管理Bean。

@MyStereotype
@ServiceQualifier
@ApplicationScoped
class WithQualifierStereotypeCalcService extends CalcService {
  @LoggerQualifier
  @Inject
  private var logger: Logger = _

  override def add(a: Int, b: Int): Int = {
    val result = a + b
    logger.infof("%d + %d = %d", a, b, result)
    result
  }
}

テストコード。

    it("with Qualifier, Stereotypes") {
      withWeld {
        val calcService = CDI.current.select(classOf[CalcService], new AnnotationLiteral[ServiceQualifier] {}).get
        calcService.add(2, 3) should be(5)
      }
    }

結果。

annotated = [BackedAnnotatedField] @LoggerQualifier @Inject private org.littlewings.javaee7.cdi.WithQualifierStereotypeCalcService.logger
annotated.annotations = [@org.littlewings.javaee7.cdi.LoggerQualifier(), @javax.inject.Inject()]
annotated.baseType = class org.jboss.logging.Logger
annotated.typeClosure = [interface org.jboss.logging.BasicLogger, class org.jboss.logging.Logger, class java.lang.Object, interface java.io.Serializable]
bean = Managed Bean [class org.littlewings.javaee7.cdi.WithQualifierStereotypeCalcService] with qualifiers [@ServiceQualifier @Any]
bean.beanClass = class org.littlewings.javaee7.cdi.WithQualifierStereotypeCalcService
bean.injectionPoints = [[BackedAnnotatedField] @LoggerQualifier @Inject private org.littlewings.javaee7.cdi.WithQualifierStereotypeCalcService.logger]
bean.name = null
bean.qualifier = [@org.littlewings.javaee7.cdi.ServiceQualifier(), @javax.enterprise.inject.Any()]
bean.scope = interface javax.enterprise.context.ApplicationScoped
bean.stereotypes = [interface org.littlewings.javaee7.cdi.MyStereotype]
bean.types = [interface org.littlewings.javaee7.cdi.CalcService, class java.lang.Object, class org.littlewings.javaee7.cdi.WithQualifierStereotypeCalcService]
member = private org.jboss.logging.Logger org.littlewings.javaee7.cdi.WithQualifierStereotypeCalcService.logger
member.declaringClass = class org.littlewings.javaee7.cdi.WithQualifierStereotypeCalcService
member.modifiers = 2
member.name = logger
qualifier = [@org.littlewings.javaee7.cdi.LoggerQualifier()]
type = class org.jboss.logging.Logger
delegate = false
transient = false

@Qualifierや@Stereotypeが入りました。

手動でルックアップしたらどうなる?

最後、確認してみようということで、@Injectを使わずに手動でCDI管理Beanを取得したらどうなるのか試してみました。

@Qualifierを追加。
src/main/java/org/littlewings/javaee7/cdi/ManualLoggerQualifier.java

package org.littlewings.javaee7.cdi;

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

Producer。

  @ManualLoggerQualifier
  @Produces
  def createLoggerWithQualifierFromManual(injectionPoint: InjectionPoint): Logger = {
    val logger =
      if (injectionPoint.getMember != null) Logger.getLogger(injectionPoint.getMember.getDeclaringClass)
      else Logger.getLogger(getClass)

    logger.infof("annotated = %s", injectionPoint.getAnnotated)

    if (injectionPoint.getAnnotated != null) {
      logger.infof("annotated.annotations = %s", injectionPoint.getAnnotated.getAnnotations)
      logger.infof("annotated.baseType = %s", injectionPoint.getAnnotated.getBaseType)
      logger.infof("annotated.typeClosure = %s", injectionPoint.getAnnotated.getTypeClosure)
    }

    logger.infof("bean = %s", injectionPoint.getBean)

    if (injectionPoint.getBean != null) {
      logger.infof("bean.beanClass = %s", injectionPoint.getBean.getBeanClass)
      logger.infof("bean.injectionPoints = %s", injectionPoint.getBean.getInjectionPoints)
      logger.infof("bean.name = %s", injectionPoint.getBean.getName)
      logger.infof("bean.qualifier = %s", injectionPoint.getBean.getQualifiers)
      logger.infof("bean.scope = %s", injectionPoint.getBean.getScope)
      logger.infof("bean.stereotypes = %s", injectionPoint.getBean.getStereotypes)
      logger.infof("bean.types = %s", injectionPoint.getBean.getTypes)
      logger.infof("member = %s", injectionPoint.getMember)
    }

    if (injectionPoint.getMember != null) {
      logger.infof("member.declaringClass = %s", injectionPoint.getMember.getDeclaringClass)
      logger.infof("member.modifiers = %s", injectionPoint.getMember.getModifiers)
      logger.infof("member.name = %s", injectionPoint.getMember.getName)
    }

    logger.infof("qualifier = %s", injectionPoint.getQualifiers)
    logger.infof("type = %s", injectionPoint.getType)
    logger.infof("delegate = %b", injectionPoint.isDelegate)
    logger.infof("transient = %b", injectionPoint.isTransient)

    logger
  }

このLoggerを直接取得するようにして、動作させてみます。

    it("manual lookup") {
      withWeld {
        val logger = CDI.current.select(classOf[Logger], new AnnotationLiteral[ManualLoggerQualifier] {}).get
        logger.infof("get Logger by CDI.")
      }
    }

結果ですが、Producerのコードにいろいろifでnullを判定していることからわかるように、けっこうな項目がnullになります。

annotated = null
bean = null
qualifier = [@org.littlewings.javaee7.cdi.ManualLoggerQualifier()]
type = class org.jboss.logging.Logger
delegate = false
transient = false

まあ、そうですよね、と。このあたりはJSR-346にも書いてあります。

終わり

今回は、InjectionPointを確認してみました。どういう情報が取れるのか確認してみたかったのと、コードを書いていて手動でCDI管理Beanを取得した場合だとどうなるのかな?とか気になったりしましたが、だいたい知りたかったことは確認できた感じです。

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