CLOVER🍀

That was when it all began.

WildFly SwarmとApache DeltaSpikeで、Project Stage(s)を合わせたい

WildFly Swarmには、Project Stagesという概念があります。

Project Stages

一方で、CDIに関する機能強化をしてくれるフレームワークApache DeltaSpikeにも
ProjectStageという概念があります。

DeltaSpike ProjectStage

WildFly Swarmには、Apache DeltaSpikeを使ったサンプルなどもあるのですが、
特にProject Stagesについては考慮されていません。

thorntail-examples/jaxrs/jaxrs-cdi-deltaspike at master · thorntail/thorntail-examples · GitHub

thorntail-examples/jaxrs/jaxrs-deltaspike-data at master · thorntail/thorntail-examples · GitHub

よって、例えば「実行時にdevelopment」にしたい、と考えるとシステムプロパティにこういう指定を
することになります。

-Dswarm.project.stage=development -Dorg.apache.deltaspike.ProjectStage=Development

また、Apache DeltaSpike側のProjectStageは、型として決まっているので以下の
いずれから選択する(もしくは実装して追加する)必要があります。

  • UnitTest
  • Development
  • SystemTest
  • IntegrationTest
  • Staging
  • Production

なにも指定しないと、「Production」が選択されます。

以上です。

ですが

これで終わってしまってはあんまりなので、少しハック的に回避策を考えてみます。

できれば、WildFly SwarmとApache DeltaSpikeのProject Stageを合わせたいですよね?

追記
@emaggameさんに別解をいただいたので、最後に追記しています。

というわけで、お題を

  • システムプロパティ「swarm.project.stage」で設定したProject Stageを、Apache DeltaSpikeにも反映する
  • 「swarm.project.stage」で指定する値は、Apache DeltaSpike側で取りうるProjectStageに限るものとする(大文字・小文字区別なし)
  • Apache DeltaSpike側で理解できないProject Stageを指定した場合は、例外を投げる

という感じでチャレンジしたいと思います。

どうするか

Apache DeltaSpike側には、ProjectStageProducerという現在のProjectStageを管理している
クラスがあります。

このクラスのsetProjectStageメソッドを使うと、現在のProjectStageを変更することができます。
ただ、本来はUnitTestで使うためみたいですけどね…。

Apache DeltaSpikeのProjectStageはいつ決定するのか

かなり速いタイミングで決まります。

Apache DeltaSpikeが実装しているCDI ExtensionのstaticイニシャライザーでConfigurationを
扱う時に、一緒に決まってしまいます。

https://github.com/apache/deltaspike/blob/deltaspike-1.7.2/deltaspike/core/api/src/main/java/org/apache/deltaspike/core/api/provider/BeanManagerProvider.java#L79

この時に、ProjectStageを意識した設定を見ようとするので、ProjectStageProducerの初期化が
完了する、と。

https://github.com/apache/deltaspike/blob/deltaspike-1.7.2/deltaspike/core/api/src/main/java/org/apache/deltaspike/core/util/ProjectStageProducer.java#L101

なので、CDI Extensionが呼び出される時(しかもBeforeBeanDiscoveryでは遅い)までに
先にProjectStageProducerに指定のProjectStageを設定してあげる必要があります。

となると、ServletContextListenerではすでに遅いみたいで、それよりも前に起動するなにかを
使う必要があります。

CDI Extensionを使う?

で、今回はCDI Extensionを使いました。WARのclassesにあるクラスなら、とりあえずlibの方より
先に読まれそう?な感じなので。

危うい感覚ですよね。こうなると素直に両方システムプロパティ指定した方がよい気も。

準備

pom.xmlは、こんな感じで作りました。Apache DeltaSpike側にもbomが提供されているので、
そちらを使用しています。

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.littlewings</groupId>
    <artifactId>project-stages-with-deltaspike</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <scala.major.version>2.12</scala.major.version>
        <scala.version>${scala.major.version}.1</scala.version>
        <scala.maven.plugin.version>3.2.2</scala.maven.plugin.version>

        <failOnMissingWebXml>false</failOnMissingWebXml>

        <wildfly.swarm.version>2016.12.1</wildfly.swarm.version>
        <apache.deltaspike.version>1.7.2</apache.deltaspike.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.wildfly.swarm</groupId>
                <artifactId>bom</artifactId>
                <version>${wildfly.swarm.version}</version>
                <scope>import</scope>
                <type>pom</type>
            </dependency>
            <dependency>
                <groupId>org.apache.deltaspike.distribution</groupId>
                <artifactId>distributions-bom</artifactId>
                <version>${apache.deltaspike.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.scala-lang</groupId>
            <artifactId>scala-library</artifactId>
            <version>${scala.version}</version>
        </dependency>

        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>7.0</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.wildfly.swarm</groupId>
            <artifactId>jaxrs</artifactId>
        </dependency>
        <dependency>
            <groupId>org.wildfly.swarm</groupId>
            <artifactId>cdi</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.deltaspike.core</groupId>
            <artifactId>deltaspike-core-api</artifactId>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.deltaspike.core</groupId>
            <artifactId>deltaspike-core-impl</artifactId>
            <scope>runtime</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>net.alchim31.maven</groupId>
                <artifactId>scala-maven-plugin</artifactId>
                <version>${scala.maven.plugin.version}</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>testCompile</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <scalaVersion>${scala.version}</scalaVersion>
                    <args>
                        <arg>-Xlint</arg>
                        <arg>-unchecked</arg>
                        <arg>-deprecation</arg>
                        <arg>-feature</arg>
                    </args>
                    <recompileMode>incremental</recompileMode>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.wildfly.swarm</groupId>
                <artifactId>wildfly-swarm-plugin</artifactId>
                <version>${wildfly.swarm.version}</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>package</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

サンプルアプリケーション

続いて、動作確認用のサンプルコードを実装します。

JAX-RSリソースクラス。このクラスに突っ込むDecoratorを、Apache DeltaSpikeのProjectStageで
切り替えるようにしましょう。
src/main/scala/org/littlewings/wildflyswarm/deltaspike/MessageResource.scala

package org.littlewings.wildflyswarm.deltaspike

import javax.enterprise.context.ApplicationScoped
import javax.inject.Inject
import javax.ws.rs.core.MediaType
import javax.ws.rs.{GET, Path, Produces, QueryParam}

@Path("message")
@ApplicationScoped
class MessageResource {
  @Inject
  private[deltaspike] var decorator: Decorator = _

  @GET
  @Produces(Array(MediaType.TEXT_PLAIN))
  def message(@QueryParam("p") p: String): String = decorator.decorate(p)
}

で、Decorator側。名前が紛らわしいですが、CDIのDecoratorとは関係ありません。

Development以外ではメッセージを「***」で装飾し、Developmentの時は「Development」という
prefixと「|」で装飾します。
src/main/scala/org/littlewings/wildflyswarm/deltaspike/Decorator.scala

package org.littlewings.wildflyswarm.deltaspike

import javax.enterprise.context.ApplicationScoped

import org.apache.deltaspike.core.api.exclude.Exclude
import org.apache.deltaspike.core.api.projectstage.ProjectStage

trait Decorator {
  def decorate(message: String): String
}

@ApplicationScoped
@Exclude(ifProjectStage = Array(classOf[ProjectStage.Development]))
class AsteriskDecorator extends Decorator {
  override def decorate(message: String) = s"***${message}***"
}

@ApplicationScoped
@Exclude(exceptIfProjectStage = Array(classOf[ProjectStage.Development]))
class PipeDecorator extends Decorator {
  override def decorate(message: String) = s"[Development] |${message}|"
}
CDI Extension

で、Apache DeltaSpikeのProject Stageを制御するのに、実装したCDI Extensionはこちら。
src/main/scala/org/littlewings/wildflyswarm/deltaspike/DeltaSpikeProjectStageInitializeExtension.scala

package org.littlewings.wildflyswarm.deltaspike

import javax.enterprise.inject.spi.Extension

import org.apache.deltaspike.core.api.projectstage.ProjectStage
import org.apache.deltaspike.core.util.ProjectStageProducer
import org.jboss.logging.Logger

object DeltaSpikeProjectStageInitializeExtension {
  private[deltaspike] val logger = Logger.getLogger(classOf[DeltaSpikeProjectStageInitializeExtension])

  {
    val swarmProjectStage = System.getProperty("swarm.project.stage", "production")

    val filteredProjectStages =
      ProjectStage
        .values()
        .filter(_.toString.equalsIgnoreCase(swarmProjectStage))

    if (filteredProjectStages.nonEmpty) {
      val selectedDeltaSpikeProjectStage = filteredProjectStages(0)

      logger.infof("set DeltaSpike ProjectStage: [%s]", selectedDeltaSpikeProjectStage)
      ProjectStageProducer.setProjectStage(selectedDeltaSpikeProjectStage)
    } else {
      throw new IllegalStateException(s"Unknown ProjectStage[$swarmProjectStage]")
    }
  }
}

class DeltaSpikeProjectStageInitializeExtension extends Extension {
  DeltaSpikeProjectStageInitializeExtension
}

意味的にはApache DeltaSpikeと同様、staticイニシャライザーで設定しているに等しいです。

内容的には、WildFly Swarm側で指定したProject Stageのシステムプロパティの値を、大文字・小文字の
区別なしでApache DeltaSpikeのenumから引き当てている感じの実装です。

ServiceLoaderの設定も、合わせて。
src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension

org.littlewings.wildflyswarm.deltaspike.DeltaSpikeProjectStageInitializeExtension

また、CDI Extensionを作成すると、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>

CDI Extensionの実装がクラスパスにあってbeans.xmlがない状態だと、@Injectが動作しなくなる
みたいで、かなりハマりました。

[THORN-655] Using a custom CDI extension breaks injection - JBoss Issue Tracker

これ自体は、CDIの仕様みたいです。

今回はCDI Extensionで実装したわけですが、要するにApache DeltaSpikeのCDI Extensionが
動作する前にこんな感じのコードを差し込めればApache DeltaSpikeの初期化時を含めて
Project Stageをコントロールできますよ、と。

project-stages.yml

あとは、WildFly Swarm側もProject Stageの切り替えがわかるように、なにかProject Stageで
設定を変えてみましょう。

今回は、リッスンポートを変えました。
src/main/resources/project-stages.yml

swarm:
    http:
        port: 8080
---
project:
    stage: development
swarm:
    http:
        port: 18080

動作確認

それでは、動作確認してみます。

パッケージングして

$ mvn package

「development」なProject Stageで起動。

$ java -Dswarm.project.stage=development -jar target/project-stages-with-deltaspike-0.0.1-SNAPSHOT-swarm.jar

通常ではこんな感じに出力されるところが

2017-01-01 19:02:20,379 INFO  [org.apache.deltaspike.core.util.ProjectStageProducer] (MSC service thread 1-7) Computed the following DeltaSpike ProjectStage: Production

(今回は)代わりに自前で仕込んだログが出るようになれば成功です。

2017-01-01 19:04:25,495 INFO  [org.littlewings.wildflyswarm.deltaspike.DeltaSpikeProjectStageInitializeExtension] (MSC service thread 1-3) set DeltaSpike ProjectStage: [Development]

確認。

$ curl http://localhost:18080/message?p=HelloWorld
[Development] |HelloWorld|

リッスンポートも変わっています。

続いて、Project Stage指定なしで起動。

$ java -jar target/project-stages-with-deltaspike-0.0.1-SNAPSHOT-swarm.jar

デフォルトを「production」にしているので、Apache DeltaSpike側は「Production」になります。

2017-01-01 19:05:30,408 INFO  [org.littlewings.wildflyswarm.deltaspike.DeltaSpikeProjectStageInitializeExtension] (MSC service thread 1-3) set DeltaSpike ProjectStage: [Production]

確認。

$ curl http://localhost:8080/message?p=HelloWorld
***HelloWorld***

リッスンポート、文字列の装飾が切り替わったことが確認できました、と。

まとめ

だいぶやっつけな方法ですが、WildFly SwarmとApache DeltaSpikeのProject Stageを
合わせてみました。

本来は両方のシステムプロパティをそれぞれ指定するのがベターかとは思いますが、
Apache DeltaSpikeの起動の前に割り込めればなんとかなりそうなので、そこが把握できた
ところまでで良しとしましょう。

今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/wildfly-swarm-scala-examples/tree/master/project-stages-with-deltaspike

別解

@emaggameさんから、教えていただいた方法です。

project-stages.ymlに書いた項目は、システムプロパティにも反映されるようなので、これを
利用してWildFly SwarmのProject Stageに合わせたApache DeltaSpikeの設定を書いておくのも
よいかもしれません。

org.apache.deltaspike.ProjectStage: Production
---
project:
  stage: it
org.apache.deltaspike.ProjectStage: IntegrationTest

成功法としては、これが素直かもですねぇ…。