Apache DeltaSpikeには、ProjectStageというものがあります。
(Test-Control) ProjectStage Control
参考)
https://tanoseam.wordpress.com/2015/11/03/cdi-test-2/
ProjectStageがどういうものかというと、SpringでいうProfileみたいなもののようです。
Apache DeltaSpikeの場合は、アプリケーションの起動時の設定や、テストコード中での指定によって、どのProjectStageとして動作するかを指定することができます。
Apache DeltaSpikeのProjectStageとしては、以下が用意されています。
- UnitTest
- Development
- SystemTest
- IntegrationTest
- Staging
- Production
デフォルトは通常の実行時はProdctionで、ユニットテストを実行する時にはUnitTestになります。これらはクラスとして表現されているので、タイプセーフに扱うことができるようです。
もちろん、両者とも変更可能です。
で、まあどういうものかというのは、実際に試しながら見ていってみましょう。
準備
sbtの設定は、このように。
build.sbt
name := "cdi-deltaspike-project-stage" version := "0.0.1-SNAPSHOT" organization := "org.littlewings" scalaVersion := "2.11.8" scalacOptions ++= Seq("-Xlint", "-deprecation", "-unchecked", "-feature") updateOptions := updateOptions.value.withCachedResolution(true) enablePlugins(JettyPlugin) webappWebInfClasses := true artifactName := { (scalaVersion: ScalaVersion, module: ModuleID, artifact: Artifact) => // artifact.name + "." + artifact.extension "ROOT." + artifact.extension } fork in Test := true libraryDependencies ++= Seq( "javax" % "javaee-web-api" % "7.0" % Provided, "org.apache.deltaspike.core" % "deltaspike-core-api" % "1.7.1" % Compile, "org.apache.deltaspike.core" % "deltaspike-core-impl" % "1.7.1" % Runtime, "org.apache.deltaspike.modules" % "deltaspike-test-control-module-api" % "1.7.1" % Test, "org.apache.deltaspike.modules" % "deltaspike-test-control-module-impl" % "1.7.1" % Test, "org.apache.deltaspike.cdictrl" % "deltaspike-cdictrl-weld" % "1.7.1" % Test, "org.jboss.weld.se" % "weld-se-core" % "2.3.5.Final" % Test, "org.scalatest" %% "scalatest" % "2.2.6" % Test, "junit" % "junit" % "4.12" % Test, "com.novocode" % "junit-interface" % "0.11" % Test )
WARプロジェクトにしています。
project/plugins.sbt
logLevel := Level.Warn addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "2.1.0")
今回は、アプリケーションサーバーにデプロイして動かすところまでやるので、ROOT.warでWARファイルができるようにしています。
ProjectStageを使うために、少なくとも必須なのはCoreモジュールです。
"org.apache.deltaspike.core" % "deltaspike-core-api" % "1.7.1" % Compile, "org.apache.deltaspike.core" % "deltaspike-core-impl" % "1.7.1" % Runtime,
core-implは、runtimeでOKです。
Test-Controlモジュールは必須ではありませんが、テストコード中にProjectStageを活用することもできるので、今回はつけておきます。
"org.apache.deltaspike.modules" % "deltaspike-test-control-module-api" % "1.7.1" % Test, "org.apache.deltaspike.modules" % "deltaspike-test-control-module-impl" % "1.7.1" % Test,
あと、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>
ProjectStageによって切り替わるCDI管理Beanを実装する
では、まずはProjectStageを使ったCDI管理Beanを実装してみましょう。
単純な例として、以下のようなものを作成。
src/main/scala/org/littlewings/javaee7/cdi/MessageService.scala
package org.littlewings.javaee7.cdi import javax.enterprise.context.ApplicationScoped import org.apache.deltaspike.core.api.exclude.Exclude import org.apache.deltaspike.core.api.projectstage.ProjectStage import org.littlewings.javaee7.projectstage.{MyProjectStage, MyProjectStageHolder} trait MessageService { def get: String } @ApplicationScoped @Exclude(exceptIfProjectStage = Array(classOf[ProjectStage.Production])) class ProductionMessageService extends MessageService { override def get: String = "ProductionStage!!" } @ApplicationScoped @Exclude(exceptIfProjectStage = Array(classOf[ProjectStage.SystemTest])) class SystemTestMessageService extends MessageService { override def get: String = "SystemTestStage!!" } @ApplicationScoped @Exclude(exceptIfProjectStage = Array(classOf[ProjectStage.Staging])) class StagingMessageService extends MessageService { override def get: String = "StagingStage!!" } @ApplicationScoped @Exclude(exceptIfProjectStage = Array(classOf[ProjectStage.Development])) class DevelopmentMessageService extends MessageService { override def get: String = "DevelopmentStage!!" } @ApplicationScoped @Exclude(exceptIfProjectStage = Array(classOf[ProjectStage.IntegrationTest])) class IntegrationTestMessageService extends MessageService { override def get: String = "IntegrationTestStage!!" } @ApplicationScoped @Exclude(exceptIfProjectStage = Array(classOf[ProjectStage.UnitTest])) class UnitTestMessageService extends MessageService { override def get: String = "UnitTestStage!!" }
MessageServiceというインターフェース(トレイト)に対して、
trait MessageService { def get: String }
@Excludeアノテーションを使用してCDI管理Beanを有効にするかどうかの設定をすることができます。
@ApplicationScoped @Exclude(exceptIfProjectStage = Array(classOf[ProjectStage.Development])) class DevelopmentMessageService extends MessageService { override def get: String = "DevelopmentStage!!" }
@Excludeなので除去なのですが、今回はexceptIfProjectStageで対象のProjectStageを表すクラスを指定すると(配列なので複数可)、指定したProjectStageで有効なCDI管理Beanとなります。
ちょっと読みにくいというか、わかりにくいですが…。
続いて、@Excludeでもうひとつ使いそうな、ifProjectStageも使用してみましょう。
src/main/scala/org/littlewings/javaee7/cdi/GreetingService.scala
package org.littlewings.javaee7.cdi import javax.enterprise.context.ApplicationScoped import javax.enterprise.inject.Default import org.apache.deltaspike.core.api.exclude.Exclude import org.apache.deltaspike.core.api.projectstage.ProjectStage import org.littlewings.javaee7.projectstage.MyProjectStage trait GreetingService { def greet: String } @ApplicationScoped class DefaultGreetingService extends GreetingService { def greet: String = "Hello CDI!!" } @ApplicationScoped @Exclude(ifProjectStage = Array(classOf[ProjectStage.Production], classOf[ProjectStage.Staging])) class TestingGreetingService extends GreetingService { def greet: String = "Fot Test!!" }
ここでは、ifProjectStageでProductionとStagingを指定しています。
@ApplicationScoped @Exclude(ifProjectStage = Array(classOf[ProjectStage.Production], classOf[ProjectStage.Staging])) class TestingGreetingService extends GreetingService {
この場合、「ProductionとStagingのProjectStage時には、除外する」という意味になります。
動作確認用のコードを書く
それでは、動作確認してみましょう。コードは、JAX-RSで書きます。
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
先ほど作成した、MessageServiceを使うJAX-RSリソースクラス。
src/main/scala/org/littlewings/javaee7/rest/MessageResource.scala
package org.littlewings.javaee7.rest import javax.enterprise.context.RequestScoped import javax.inject.Inject import javax.ws.rs.{GET, Path, Produces} import javax.ws.rs.core.MediaType import org.apache.deltaspike.core.api.projectstage.ProjectStage import org.littlewings.javaee7.cdi.MessageService @Path("message") @RequestScoped class MessageResource { @Inject var messageService: MessageService = _ @Inject var projectStage: ProjectStage = _ @GET @Produces(Array(MediaType.APPLICATION_JSON)) def message: java.util.Map[String, String] = { val map = new java.util.LinkedHashMap[String, String] map.put("message", messageService.get) map.put("projectStage", projectStage.getClass.getSimpleName) map } }
GreetingServiceを使うJAX-RSリソース。
src/main/scala/org/littlewings/javaee7/rest/GreetingResource.scala
package org.littlewings.javaee7.rest import javax.enterprise.context.RequestScoped import javax.inject.Inject import javax.ws.rs.core.MediaType import javax.ws.rs.{GET, Path, Produces} import org.apache.deltaspike.core.api.projectstage.ProjectStage import org.littlewings.javaee7.cdi.GreetingService @Path("greeting") @RequestScoped class GreetingResource { @Inject var greetingService: GreetingService = _ @Inject var projectStage: ProjectStage = _ @GET @Produces(Array(MediaType.APPLICATION_JSON)) def greeting: java.util.Map[String, String] = { val map = new java.util.LinkedHashMap[String, String] map.put("greeting", greetingService.greet) map.put("projectStage", projectStage.getClass.getSimpleName) map } }
実は、ProjectStage自体もインジェクション可能で、現在のProjectStageを取得して条件判定などに使えるようになっています。
@Inject
var projectStage: ProjectStage = _
今回は、各Serviceの実行結果とProjectStageの名前をJSONで返して挙げることにします。
JSONのやり取りにはJackson2を使いたいと思い、jboss-deployment-structure.xmlに設定を入れておきます。まあ、これはなくても動きますが…。
src/main/webapp/WEB-INF/jboss-deployment-structure.xml
<?xml version="1.0" encoding="UTF-8"?> <jboss-deployment-structure xmlns="urn:jboss:deployment-structure:1.3"> <deployment> <dependencies> <module name="org.jboss.resteasy.resteasy-jackson2-provider" services="import"/> </dependencies> </deployment> </jboss-deployment-structure>
確認
WildFlyを起動して、デプロイします。
$ bin/standalone.sh $ cp /path/to/ROOT.war /path/wildfly-10.0.0.Final/standalone/deployments/
アクセスしてみます。
まずは、MessageServiceを使うJAX-RSリソースへアクセス。
$ curl http://localhost:8080/rest/message {"message":"ProductionStage!!","projectStage":"Production"}
ProjectStageがProduction用のものが選ばれ、ProjectStage自体もProductionとなっていることがわかります。
Greetingも同様です。
$ curl http://localhost:8080/rest/greeting {"greeting":"Hello CDI!!","projectStage":"Production"}
では、1度WildFlyを停止します。
今度はWARファイルの中身はそのままで、「org.apache.deltaspike.ProjectStage」システムプロパティで「Staging」を指定してWildFlyを起動します。
$ bin/standalone.sh -Dorg.apache.deltaspike.ProjectStage=Staging
すると、動作結果が変わります。
$ curl http://localhost:8080/rest/message {"message":"StagingStage!!","projectStage":"Staging"} $ curl http://localhost:8080/rest/greeting {"greeting":"Hello CDI!!","projectStage":"Staging"}
ProjectStageが、Stagingになりましたね。
今回はWildFlyの起動パラメータとなりましたが、要するに「org.apache.deltaspike.ProjectStage」システムプロパティを使うことで、アプリケーションを実行するProjectStageを切り替えられるということですね。
Test-Controlと合わせて使う
続いて、Test-Control(テスト用のモジュール)と合わせて使ってみましょう。
ふつうにApache DeltaSpikeのTest-Controlを使うように、@RunWithにCdiTestRunnerを指定して実行します。
src/test/scala/org/littlewings/javaee7/cdi/SimpleProjectStageSpec.scala
package org.littlewings.javaee7.cdi import javax.inject.Inject import org.apache.deltaspike.core.api.projectstage.ProjectStage import org.apache.deltaspike.testcontrol.api.junit.CdiTestRunner import org.junit.Test import org.junit.runner.RunWith import org.scalatest.Matchers import org.scalatest.junit.JUnitSuite @RunWith(classOf[CdiTestRunner]) class SimpleProjectStageSpec extends JUnitSuite with Matchers { @Inject var messageService: MessageService = _ @Test def messageTest(): Unit = { messageService.get should be("UnitTestStage!!") } @Inject var projectStage: ProjectStage = _ @Test def projectStageTest(): Unit = { projectStage should be(a[ProjectStage.UnitTest]) } @Inject var greetingService: GreetingService = _ @Test def greetingTest(): Unit = { greetingService.greet should be("Hello CDI!!") } }
すると、最初にデフォルトのProjectStageのことについて記載しましたが、ユニットテスト中はUnitTestなProjectStageで動作していることになっています。
@Inject var messageService: MessageService = _ @Test def messageTest(): Unit = { messageService.get should be("UnitTestStage!!") } @Inject var projectStage: ProjectStage = _ @Test def projectStageTest(): Unit = { projectStage should be(a[ProjectStage.UnitTest]) }
また、@TestControlアノテーションのprojectStageにProjectStageを指定することで、指定のProjectStageとしても実行することができます。
src/test/scala/org/littlewings/javaee7/cdi/IntegrationTestProjectStageSpec.scala
package org.littlewings.javaee7.cdi import javax.inject.Inject import org.apache.deltaspike.core.api.projectstage.ProjectStage import org.apache.deltaspike.testcontrol.api.TestControl import org.apache.deltaspike.testcontrol.api.junit.CdiTestRunner import org.junit.Test import org.junit.runner.RunWith import org.scalatest.Matchers import org.scalatest.junit.JUnitSuite @RunWith(classOf[CdiTestRunner]) @TestControl(projectStage = classOf[ProjectStage.IntegrationTest]) class IntegrationTestProjectStageSpec extends JUnitSuite with Matchers { @Inject var messageService: MessageService = _ @Test def messageTest(): Unit = { messageService.get should be("IntegrationTestStage!!") } @Inject var projectStage: ProjectStage = _ @Test def projectStageTest(): Unit = { projectStage should be(a[ProjectStage.IntegrationTest]) } }
独自のProjectStageを作る
最後に、独自のProjectStageの作り方について書いておきます。
独自のProjectStageを作るには、
- ProjectStageHolderインターフェースの実装クラスの作成
- ProjectStageクラスのサブクラスの作成
をしたうえで、Service Providerの機構を使ってProjectStageHolderの実装をロードさせます。
今回は、「MyProjectStage」というProjectStageを作ってみましょう。
ProjectStageクラスを継承したクラス。
src/main/java/org/littlewings/javaee7/projectstage/MyProjectStage.java
package org.littlewings.javaee7.projectstage; import org.apache.deltaspike.core.api.projectstage.ProjectStage; public class MyProjectStage extends ProjectStage { private static final long serialVersionUID = 1L; }
ProjectStageHolderインターフェースの実装。このクラスには、作成したProjectStageのインスタンスをnewして保持しておきます。
src/main/java/org/littlewings/javaee7/projectstage/MyProjectStageHolder.java
package org.littlewings.javaee7.projectstage; import org.apache.deltaspike.core.api.projectstage.ProjectStageHolder; public class MyProjectStageHolder implements ProjectStageHolder { public static final MyProjectStage MyProjectStage = new MyProjectStage(); }
そして、この作成したProjectStageHolderのFQCNを、META-INF/servicesディレクトリ配下に「org.apache.deltaspike.core.api.projectstage.ProjectStageHolder」という名前のファイルとして配置します。
src/main/resources/META-INF/services/org.apache.deltaspike.core.api.projectstage.ProjectStageHolder
org.littlewings.javaee7.projectstage.MyProjectStageHolder
これで、「MyProjectStage」というProjectStageが使えるようになります。
@ExcludeのexceptIfProjectStageで指定したり、
@ApplicationScoped @Exclude(exceptIfProjectStage = Array(classOf[MyProjectStage])) class MyProjectStageMessageService extends MessageService { override def get: String = "MyProjectStage!!" }
ifProjectStageで指定したりすることができるようになります。
@ApplicationScoped @Exclude(ifProjectStage = Array(classOf[ProjectStage.Production], classOf[ProjectStage.Staging], classOf[MyProjectStage])) class TestingGreetingService extends GreetingService { def greet: String = "Fot Test!!" }
動作確認してみましょう。ProjectStageを「MyProjectStage」に指定して、WildFlyを起動。
$ bin/standalone.sh -Dorg.apache.deltaspike.ProjectStage=MyProjectStage
確認。
$ curl http://localhost:8080/rest/message {"message":"MyProjectStage!!","projectStage":"MyProjectStage"}
ちゃんと、反映されていますね。
なお、このProjectStageがどうやって登録されているかですが、ProjectStageHolderを定義した時に対象のProjectStageをインスタンスで持つようにしていましたが、ServiceLoaderでProjectStageHolderがロードされた時に、このProjectStageのインスタンス自身で登録するようです。
https://github.com/apache/deltaspike/blob/deltaspike-1.7.1/deltaspike/core/api/src/main/java/org/apache/deltaspike/core/api/projectstage/ProjectStage.java#L176
クラス名と同じフィールド名を持つことになり、若干気持ち悪い感じがしないでもないですが、使用用途からすると名前は合わせた方がよいと思います。
まとめ
Apache DeltaSpikeのProjectStageを使って、@Excludeと合わせて利用するクラスを切り替えること、そして自分でもProjectStageを作って追加することを確認してみました。
こういう仕組みがあると、便利ですね。覚えておきましょう。
今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/javaee7-scala-examples/tree/master/cdi-deltaspike-project-stage