CLOVER🍀

That was when it all began.

CDIのAlternativesを試す

あまり理解しないまま使っているCDIですが、最近ちょっと気になり出すところがありまして、少しずつ見ていこうかなぁという気になりました。

こういうの、自分で試してみないと覚えないので、すでに出ている内容でも気になったテーマは自分で書いていこうかなぁと表います。

で、今回はAlternativesを試してみます。

Alternativesについては、NetBeansのサイトにあるドキュメントを見るのがよいと思います。

@Alternative Beanおよびライフサイクル注釈の適用
https://netbeans.org/kb/docs/javaee/cdi-validate_ja.html

インジェクション対象が複数になりえるケースで、@Alternativeアノテーションとbeans.xmlの指定で、実際にどの管理Beanをインジェクションするのかを決めることができるらしいです。

というわけで、使ってみます。

準備

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

name := "cdi-alternative"

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.21"
val resteasyVersion = "3.0.11.Final"
val weldServletVersion = "2.2.10.SP1"
val scalaTestVersion = "2.2.4"

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(RESTEasy)、CDI(Weld)、そして組み込みTomcatを使用します。

JAX-RSリソースクラス

まずは、エントリポイントとなるJAX-RS関連のクラスを。Applicationクラスのサブクラス。
src/main/scala/org/littlewings/javaee7/cdi/rest/JaxrsApplication.scala

package org.littlewings.javaee7.cdi.rest

import javax.ws.rs.ApplicationPath
import javax.ws.rs.core.Application

@ApplicationPath("rest")
class JaxrsApplication extends Application

リソースクラス。
src/main/scala/org/littlewings/javaee7/cdi/rest/MessageResource.scala

package org.littlewings.javaee7.cdi.rest

import javax.enterprise.inject.spi.CDI
import javax.enterprise.context.RequestScoped
import javax.inject.Inject
import javax.ws.rs.{ GET, Path, Produces }
import javax.ws.rs.core.MediaType

import org.littlewings.javaee7.cdi.service.MessageService

@Path("message")
@RequestScoped
class MessageResource {
  @Inject
  private var messageService: MessageService = _

  @GET
  @Produces(Array(MediaType.TEXT_PLAIN))
  def message: String = messageService.get
}

このクラスに、@Injectで管理Beanをインジェクションするようにしています。

CDI管理Bean(Alternatives関連)

Alternativesを試すために、以下のようなトレイト(というかインターフェース)とクラスを用意。
src/main/scala/org/littlewings/javaee7/cdi/service/MessageService.scala

package org.littlewings.javaee7.cdi.service

import javax.enterprise.context.RequestScoped
import javax.enterprise.inject.Alternative

trait MessageService {
  def get: String
}

@Alternative
@RequestScoped
class HelloWorldMessageService extends MessageService {
  override def get: String = "Hello World"
}

@Alternative
@RequestScoped
class AnotherMessageService extends MessageService {
  override def get: String = "Another Message Service"
}

JAX-RSリソースクラスでインジェクションしていたのは、この型です。

trait MessageService {
  def get: String
}

これだと実体がないので、実装クラスは@Alternativeアノテーションを付与して定義しています。

@Alternative
@RequestScoped
class HelloWorldMessageService extends MessageService {
  override def get: String = "Hello World"
}

@Alternative
@RequestScoped
class AnotherMessageService extends MessageService {
  override def get: String = "Another Message Service"
}

ですが、このまま実行するとどちらのクラスを使用するのかがわからないので、エラーとなります。どちらを使用するかは、beans.xmlで指定します。

beans.xml

というわけで、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">
  <alternatives>
    <class>org.littlewings.javaee7.cdi.service.HelloWorldMessageService</class>
    <!-- <class>org.littlewings.javaee7.cdi.service.AnotherMessageService</class> -->
  </alternatives>
</beans>

配置先が、WEB-INF/beans.xmlでないのは、組み込みTomcatを使っているからです…。

そして、alternativesタグでどちらのクラスを使用するかを指定してます。今回は、HelloWorldMessageServiceというクラスを選びました。

テストで確認する

それでは、テストで実際の動きを確認してみます。

組み込みTomcatでJAX-RS+CDIを動かしたいのですが、このためにこんなトレイトを用意。
src/test/scala/org/littlewings/javaee7/cdi/EmbeddedTomcatCdiSupport.scala

package org.littlewings.javaee7.cdi

import java.io.File

import org.apache.catalina.startup.Tomcat

import org.scalatest.Suite
import org.scalatest.BeforeAndAfterAll

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.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()
  }
}

このトレイトをMix-inしたテストクラスは、開始時にCDIを有効化した組み込みTomcatを起動し、テスト終了時にTomcatをシャットダウンしてくれます。

ScalaTestのBeforeAndAfterAllトレイトをMix-inしているので、JUnitの@BeforeClassと@AfterClassと同じような動きをします。

で、実際のテストコード。
src/test/scala/org/littlewings/javaee7/cdi/CdiAlternativeSpec.scala

package org.littlewings.javaee7.cdi

import scala.io.Source

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

class CdiAlternativeSpec extends FunSpec with EmbeddedTomcatCdiSupport {
  describe("CDI Alternative Spec") {
    it("use HelloWorldMessageResource") {
      Source
        .fromURL(s"http://localhost:${port}/rest/message")
        .mkString should be ("Hello World")
    }

    ignore("use AnotherMessageResource") {
      Source
        .fromURL(s"http://localhost:${port}/rest/message")
        .mkString should be ("Another Message Service")
    }
  }
}

Alternativesを使っているので、どちらかしか通せません(片方ignoreにしています)が…beans.xmlの定義を反転させると、通るテストも反対になります。

とりあえず、簡単ですが組み込みTomcatを使ったテストの雛形もできたので、これからマイペースでCDIを見ていこうと思います。

別解

@glory_ofさんにご指摘いただいたのですが、Stereotypeアノテーションを使っても切り替えができるそうです。

インジェクションするオブジェクトの切り替え
http://d.hatena.ne.jp/gloryof/20121007/1349603701

この情報を元に、修正してみます。

まずは、@Stereotype付きのアノテーションを作成します。ここに、@Alternativeも付与します。
src/main/java/org/littlewings/javaee7/cdi/stereotype/Another.java

package org.littlewings.javaee7.cdi.stereotype;

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;

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

で、先ほどの@Alternative付きのクラス達を、こう修正します。

//@Alternative
@RequestScoped
class HelloWorldMessageService extends MessageService {
  override def get: String = "Hello World"
}

片方からは@Alternativeアノテーションを削除、

@Another
//@Alternative
@RequestScoped
class AnotherMessageService extends MessageService {
  override def get: String = "Another Message Service"
}

もう片方には、@Alternativeアノテーションを削除したうえで、Stereotypeで作成したアノテーションを付与。

この状態で、先ほどbeans.xmlに登録した内容を削除すると

    <!-- <class>org.littlewings.javaee7.cdi.service.HelloWorldMessageService</class> -->
    <!-- <class>org.littlewings.javaee7.cdi.service.AnotherMessageService</class> -->

Stereotypeを付与していない方のクラスが有効になります。

切り替えるには、stereotypeタグを使えばいいそうです。

  <alternatives>
    <stereotype>org.littlewings.javaee7.cdi.stereotype.Another</stereotype>
  </alternatives>

なるほど、勉強になりました。

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