CLOVER🍀

That was when it all began.

Apache DeltaSpikeのProjectStageを使って、プログラムの切り替えを行う

Apache DeltaSpikeには、ProjectStageというものがあります。

DeltaSpike ProjectStage

(Core) Type-safe 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