あまり理解しないまま使っている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リソースクラス
まずは、エントリポイントとなる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アノテーションを使っても切り替えができるそうです。
@kazuhira_r Alternativeはステレオタイプアノテーションでも切り替えが出来ますよ。以前、ブログで書いたので参考になればと。URL
インジェクションするオブジェクトの切り替え
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