CLOVER🍀

That was when it all began.

Apache DeltaSpikeのConfiguration Mechanismを試す

Apache DeltaSpikeには、Configuration Mechanismというものがあり、設定情報を管理することができます。

DeltaSpike Configuration Mechanism

こちらを利用すると、設定ファイルに定義した情報をProjectStageごとに分けて管理できたり、設定ファイルの中で変数展開をできたりするといったことができるようになります。

Springでいう、@PropertySource/@Valueに近いものみたいですが…同じと思うと、いろいろしっぺ返しを喰らうようです。

Apache DeltaSpikeのConfigurationを使うには、大きくConfigResolverを使ってルックアップする方法と@ConfigPropertyを使ってインジェクションする方法の2つがあります。

順を追って、見ていってみましょう。

準備

まずは、環境準備。

ビルド定義から。
build.sbt

name := "cdi-deltaspike-configuration"

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" % "3.0.0" % 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")

Configurationの機能を使うには、「core-api」および「core-impl」があればよいのですが、Test-Controlも使って動作させてみる関係上、テスト用のモジュールも追加しています。

CDI有効化のための、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>

最低限、ここまでです。

ConfigResolverを使う

それでは、ConfigResolverから使ってみましょう。

ConfigResolverには、単純に使う方法とタイプセーフに使う方法があります。それぞれ、見ていきましょう。

設定ファイルを定義する

と、実際にAPIを使う前に、読み込む設定ファイルを用意します。

Configuration APIが読み込むデフォルトのファイルは、「META-INF/apache-deltaspike.properties」となります。ここに、任意の項目を定義します。

今回は、データベース接続情報っぽいものを定義してみました。同じような項目が、ProjectStageごとに定義されているので、なんとなく挙動に予測はつくかもしれませんが…。
src/main/resources/META-INF/apache-deltaspike.properties

database.server=localhost
database.port=3306
database.name=test

jdbc.url=jdbc:mysql://${database.server}:${database.port}/${database.name}

## for UnitTest ProjectStage
database.server.UnitTest=ut-server
database.port.UnitTest=13306
database.name.UnitTest=ut-test

jdbc.url.UnitTest=jdbc:mysql://${database.server}:${database.port}/${database.name}

## for Development ProjectStage
database.server.Development=development-server
database.port.Development=23306
database.name.Development=development-test

jdbc.url.Development=jdbc:mysql://${database.server}:${database.port}/${database.name}
単純にConfigResolverを使う

それでは、最初にConfigResolverを単純に使うケースを書いてみます。

ConfigResolver methods

テストコードで確認しますが、とりあえず大枠だけ書いて、以降は順次見ていきます。
src/test/scala/org/littlewings/javaee7/config/ConfigResolverSpec.scala

package org.littlewings.javaee7.config

import javax.inject.Inject

import org.apache.deltaspike.core.api.config.ConfigResolver
import org.apache.deltaspike.core.api.projectstage.ProjectStage
import org.apache.deltaspike.testcontrol.api.junit.CdiTestRunner
import org.junit.runner.RunWith
import org.junit.{Before, Test}
import org.scalatest.Matchers
import org.scalatest.junit.JUnitSuite

@RunWith(classOf[CdiTestRunner])
class ConfigResolverSpec extends JUnitSuite with Matchers {
  // ここに、テストを書く!
}

基本となる例。

  @Test
  def configResolver(): Unit = {
    ConfigResolver.getPropertyValue("database.server") should be("localhost")
    ConfigResolver.getPropertyValue("database.port") should be("3306")
    ConfigResolver.getPropertyValue("database.name") should be("test")

    ConfigResolver.getPropertyValue("missing.key") should be(null)
    ConfigResolver.getPropertyValue("missing.key", "defaultValue") should be("defaultValue")

    ConfigResolver.getPropertyValue("jdbc.url") should be("jdbc:mysql://localhost:3306/test")
  }

設定ファイルに記載した内容が、素直に返ってきます。

設定ファイルに定義がない項目についてはnullが返りますが、デフォルト値を設定することもできます。

    ConfigResolver.getPropertyValue("missing.key") should be(null)
    ConfigResolver.getPropertyValue("missing.key", "defaultValue") should be("defaultValue")

また、変数展開することも可能で、今回の例だと以下のような定義は

jdbc.url=jdbc:mysql://${database.server}:${database.port}/${database.name}

このように変数展開された状態で返却されます。

    ConfigResolver.getPropertyValue("jdbc.url") should be("jdbc:mysql://localhost:3306/test")

便利ですね。

ところで、このコードはJUnitテストとして動かしているので、Test-Control上はUnitTestとなっているはずですが、設定ファイル上このように書いた定義は完全に無視されています。

## for UnitTest ProjectStage
database.server.UnitTest=ut-server
database.port.UnitTest=13306
database.name.UnitTest=ut-test

jdbc.url.UnitTest=jdbc:mysql://${database.server}:${database.port}/${database.name}

ConfigResolver#getPropertyValueは、ProjectStageを意識しないからです。

ProjectStageを意識する場合は、ConfigResolver#getProjectStageAwarePropertyValueを使用します。

  @Test
  def configResolverProjectStageAware(): Unit = {
    ConfigResolver.getProjectStageAwarePropertyValue("database.server") should be("ut-server")
    ConfigResolver.getProjectStageAwarePropertyValue("database.port") should be("13306")
    ConfigResolver.getProjectStageAwarePropertyValue("database.name") should be("ut-test")

    ConfigResolver.getProjectStageAwarePropertyValue("jdbc.url") should be("jdbc:mysql://ut-server:13306/ut-test")
  }

ですが、このコードはそのままだと動作しません。

Test-Control上、確かにProjectStageはUnitTestになっているのですが、ConfigResolver内で見ているProjectStageがUnitTestとなる前にデフォルト値(Production)で固定されてしまっているからです…。微妙…。

CdiTestRunnerのstaticイニシャライザーでTestBaseConfigの初期設定を行う時に、一緒に固定されてしまうんですよね…。
https://github.com/apache/deltaspike/blob/deltaspike-1.7.1/deltaspike/modules/test-control/api/src/main/java/org/apache/deltaspike/testcontrol/api/junit/CdiTestRunner.java#L85-L89

これを回避してProjectStageをUnitTestにしようと思うと、システムプロパティ「org.apache.deltaspike.ProjectStage」で起動時に指定するか、こんな感じの小細工をすることになるでしょう。

  @Inject
  var projectStage: ProjectStage = _

  @Before
  def setUp(): Unit = {
    val configResolverProjectStageField = classOf[ConfigResolver].getDeclaredField("projectStage")
    configResolverProjectStageField.setAccessible(true)
    configResolverProjectStageField.set(null, projectStage)
  }

さらに、変数展開しているパターンにも注意があります。これですね。

    ConfigResolver.getProjectStageAwarePropertyValue("jdbc.url") should be("jdbc:mysql://ut-server:13306/ut-test")

設定ファイル上、ProjectStageが異なるだけのまったく同じ項目が2つ並んでいますが、

jdbc.url=jdbc:mysql://${database.server}:${database.port}/${database.name}

jdbc.url.UnitTest=jdbc:mysql://${database.server}:${database.port}/${database.name}

これで、UnitTest向けの設定を省略してしまうと、結果が変わってこうなります。

    ConfigResolver.getProjectStageAwarePropertyValue("jdbc.url") should be("jdbc:mysql://localhost:3306/test")

内容が、ProjectStageに特化したものではなくなっていますね…。

これは、特定のProjectStage向けのキー項目が見つからなかった時に、デフォルトのものにフォールバックするのですが、この時に変数展開するものについてもProjectStageを意識しない形に設定されてしまうからです。
https://github.com/apache/deltaspike/blob/deltaspike-1.7.1/deltaspike/core/api/src/main/java/org/apache/deltaspike/core/api/config/ConfigResolver.java#L245-L250

なので、内容が一緒でも変数展開される内容がついてきて欲しければ、冗長かもしれませんが定義しておきましょうと。

そうそう、キーの解決のルールについて書いていませんでしたね。

ルールは、以下の「Property value resolution sequence」という箇所に書いてあります。
getPropertyAwarePropertyValue()

ざっくりと、

  • propertyValue = propertyKey + "." + ProjectStage
  • 見つからなければ、propertyValue = propertyKey

となるルールで覚えておけばOKです。ConfigResolver#getPropertyAwarePropertyValueを使う場合は、さらに追加のプロパティを指定することもできますが、ここでは載せません。興味のある方は、「Property value resolution sequence」へ。

こういうやり方だと、基本的にはProjectStageごとの設定は同じファイルに書くのでしょうか?ファイルを分ける場合は、名前解決のための実装が必要そう…。

TypedResolver APIを使う

続いて、TypedResolver APIを使ってみます。

TypedResolver API

なお、以下の設定は入っているものとします。

  @Inject
  var projectStage: ProjectStage = _

  @Before
  def setUp(): Unit = {
    val configResolverProjectStageField = classOf[ConfigResolver].getDeclaredField("projectStage")
    configResolverProjectStageField.setAccessible(true)
    configResolverProjectStageField.set(null, projectStage)
  }

こちらは、ConfigResolver#resolveからメソッドチェーンでつなげて表現するものになります。

  @Test
  def typedResolver(): Unit = {
    ConfigResolver
      .resolve("database.server")
      .as(classOf[String])
      .getValue should be("ut-server")

    ConfigResolver
      .resolve("database.port")
      .as(classOf[Integer])
      .getValue should be(13306)

    ConfigResolver
      .resolve("missing.key")
      .as(classOf[String])
      .getValue should be(null)

    ConfigResolver
      .resolve("missing.key")
      .as(classOf[String])
      .withDefault("defaultValue")
      .getValue should be("defaultValue")

    ConfigResolver
      .resolve("jdbc.url")
      .as(classOf[String])
      .getValue should be("jdbc:mysql://${database.server}:${database.port}/${database.name}")

    ConfigResolver
      .resolve("jdbc.url")
      .as(classOf[String])
      .evaluateVariables(true)
      .getValue should be("jdbc:mysql://localhost:3306/test")
  }

asメソッドでプロパティの型を指定することができ、またwithDefaultメソッドでキーに対応する定義がなかった場合のデフォルト値を指定したりできます。

また、ProjectStageはデフォルトで意識してくれるようです。

変数展開はデフォルトではやってくれないようなので、evaluateVariablesにtrueを設定する必要があります。

    ConfigResolver
      .resolve("jdbc.url")
      .as(classOf[String])
      .getValue should be("jdbc:mysql://${database.server}:${database.port}/${database.name}")

    ConfigResolver
      .resolve("jdbc.url")
      .as(classOf[String])
      .evaluateVariables(true)
      .getValue should be("jdbc:mysql://localhost:3306/test")

が、よく見ると変数展開したところがProjectStageを意識していません。

    ConfigResolver
      .resolve("jdbc.url")
      .as(classOf[String])
      .evaluateVariables(true)
      .getValue should be("jdbc:mysql://localhost:3306/test")

これは、どうなっているかというと変数展開している部分については、事実上ProjectStageを意識しないConfigResolver#getPropertyValueの挙動だからです。
https://github.com/apache/deltaspike/blob/deltaspike-1.7.1/deltaspike/core/api/src/main/java/org/apache/deltaspike/core/api/config/ConfigResolver.java#L1016

うーん…。

あと、型変換については今回はやっていませんが、変換ルールを自分で定義することも可能なようです。

@ConfigPropertyを使う

続いて、@ConfigPropertyを使ってみます。

Injection of configured values into beans using @ConfigProperty

@ConfigPropertyを使うと、設定ファイルの内容を@Injectで注入できるようになります。このAPIでは、TypedResolver APIと同じ型変換をサポートしています。

…ということは、変数展開におけるProjectStageの扱いも同じなんですけど。

まあ、例を書いてみましょう。先ほどConfigResolver#getPropertyValueなどで見ていた設定を、@Injectと@ConfigPropertyでインジェクションします。
src/main/scala/org/littlewings/javaee7/config/DeltaSpikeConfig.scala

package org.littlewings.javaee7.config

import javax.enterprise.context.ApplicationScoped
import javax.inject.Inject

import org.apache.deltaspike.core.api.config.ConfigProperty

@ApplicationScoped
class DeltaSpikeConfig {
  @Inject
  @ConfigProperty(name = "database.server")
  var databaseServer: String = _

  @Inject
  @ConfigProperty(name = "database.port")
  var databasePort: Int = _

  @Inject
  @ConfigProperty(name = "database.name")
  var databaseName: String = _

  @Inject
  @ConfigProperty(name = "missing.key", defaultValue = "defaultValue")
  var missingKey: String = _

  @Inject
  @ConfigProperty(name = "missing.key.without.default.value")
  var missingKeyWithoutDefaultValue: String = _

  @Inject
  @ConfigProperty(name = "jdbc.url")
  var jdbcUrl: String = _
}

テストコード。ProjectStageは、強引に変えています。
src/test/scala/org/littlewings/javaee7/config/DeltaSpikeConfigSpec.scala

package org.littlewings.javaee7.config

import javax.inject.Inject

import org.apache.deltaspike.core.api.config.ConfigResolver
import org.apache.deltaspike.core.api.projectstage.ProjectStage
import org.apache.deltaspike.testcontrol.api.junit.CdiTestRunner
import org.junit.{Before, Test}
import org.junit.runner.RunWith
import org.scalatest.Matchers
import org.scalatest.junit.JUnitSuite

@RunWith(classOf[CdiTestRunner])
class DeltaSpikeConfigSpec extends JUnitSuite with Matchers {
  @Inject
  var config: DeltaSpikeConfig = _

  @Inject
  var projectStage: ProjectStage = _

  @Before
  def setUp(): Unit = {
    val configResolverProjectStageField = classOf[ConfigResolver].getDeclaredField("projectStage")
    configResolverProjectStageField.setAccessible(true)
    configResolverProjectStageField.set(null, projectStage)
  }

  @Test
  def configTest(): Unit = {
    config.databaseServer should be("ut-server")
    config.databasePort should be(13306)
    config.databaseName should be("ut-test")
    config.missingKey should be("defaultValue")
    config.missingKeyWithoutDefaultValue should be(null)
    config.jdbcUrl should be("jdbc:mysql://localhost:3306/test")
  }
}

で、やっぱり変数展開している部分についてはTypedResolver APIと同じ挙動になりましたね。

    config.jdbcUrl should be("jdbc:mysql://localhost:3306/test")

また、@ConfigPropertyで指定したキーに対応する定義がなかった場合は、単にnullが注入されたりするだけで、特にインジェクションに失敗するようなことにはなりません。これもちょっと微妙…。

独自の設定ファイルを追加する

ここまでは、「META-INF/apache-deltaspike.properties」に定義した内容を見ていっていましたが、独自の設定ファイルを読み込むようにしてみましょう。

ドキュメントでは、ConfigSourceについての記述とPropertyFileConfigについての記述があります。

Custom ConfigSources

ConfigSourceについては、サンプルとかはないですね。ConfigSourceは、「どこから」設定情報を取得するかを定義するものになります。今回はpropertiesファイルを使用していますが、JNDIやシステムプロパティ、環境変数の利用も実は可能だったりします。

また、一緒に出てくるConfigSourceProviderという単語がありますが、これはどのConfigSourceを使って取得するかを定義します。デフォルトの実装としては、DefaultConfigSourceProviderがあったりします。
https://github.com/apache/deltaspike/blob/deltaspike-1.7.1/deltaspike/core/impl/src/main/java/org/apache/deltaspike/core/impl/config/DefaultConfigSourceProvider.java

が、ここではPropertyFileConfigを追加してみましょう。読み込み自体は、DefaultConfigSourceProviderに頑張ってもらいます。

設定ファイルとして、「my-application.properties」というファイルを用意。
src/main/resources/my-application.properties

application.name=Default
application.name.Production=for Production
application.name.Development=for Development

このファイルを指定するための、PropertyFileConfigの実装を用意します。
src/main/scala/org/littlewings/javaee7/config/MyApplicationPropertyFileConfig.scala

package org.littlewings.javaee7.config

import org.apache.deltaspike.core.api.config.PropertyFileConfig

class MyApplicationPropertyFileConfig extends PropertyFileConfig {
  override def getPropertyFileName: String = "my-application.properties"

  override def isOptional: Boolean = false
}

getPropertyFileNameは対象のpropertiesファイル名前を返すように実装し、isOptionalはこのファイルが必須かどうかを表します。isOptionalをtrueにした場合、getPropertyFileNameで返却した名前のファイルが見つからない場合、アプリケーションが起動できなくなります。

この作成したクラスのFQCNを「META-INF/services/org.apache.deltaspike.core.api.config.PropertyFileConfig」に記載しておくと、DefaultConfigSourceProviderを経由してService Providerの仕組みでロードしてくれます。
src/main/resources/META-INF/services/org.apache.deltaspike.core.api.config.PropertyFileConfig

org.littlewings.javaee7.config.MyApplicationPropertyFileConfig

では、こちらを@ConfigPropertyでインジェクションするクラスも用意してみましょう。
src/main/scala/org/littlewings/javaee7/config/MyApplicationConfig.scala

package org.littlewings.javaee7.config

import javax.enterprise.context.ApplicationScoped
import javax.inject.Inject

import org.apache.deltaspike.core.api.config.ConfigProperty

@ApplicationScoped
class MyApplicationConfig {
  @Inject
  @ConfigProperty(name = "application.name")
  var applicationName: String = _
}

テストコードで確認。
src/test/scala/org/littlewings/javaee7/config/CustomConfigSpec.scala

package org.littlewings.javaee7.config

import javax.inject.Inject

import org.apache.deltaspike.core.api.config.ConfigResolver
import org.apache.deltaspike.core.api.projectstage.ProjectStage
import org.apache.deltaspike.testcontrol.api.junit.CdiTestRunner
import org.junit.{Before, Test}
import org.junit.runner.RunWith
import org.scalatest.Matchers
import org.scalatest.junit.JUnitSuite

@RunWith(classOf[CdiTestRunner])
class CustomConfigSpec extends JUnitSuite with Matchers {
  @Inject
  var myApplicationConfig: MyApplicationConfig = _

  @Inject
  var projectStage: ProjectStage = _

  @Before
  def setUp(): Unit = {
    val configResolverProjectStageField = classOf[ConfigResolver].getDeclaredField("projectStage")
    configResolverProjectStageField.setAccessible(true)
    configResolverProjectStageField.set(null, projectStage)
  }

  @Test
  def test(): Unit = {
    myApplicationConfig.applicationName should be("Default")
  }
}

UnitTest向けの設定を用意していなかったので、デフォルト値になっていますが…。

アプリケーションサーバーにデプロイして確認

最後に、@ConfigPropertyでインジェクションしているクラスを使って、WARファイルをアプリケーションサーバーにデプロイして確認してみましょう。確認にはJAX-RSアプリケーションサーバーはWildFlyを利用します。

確認用の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

作成したConfigクラスの中身をMapに返却する、リソースクラス。
src/main/scala/org/littlewings/javaee7/rest/ConfigResource.scala

package org.littlewings.javaee7.rest

import scala.collection.JavaConverters._

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

import org.littlewings.javaee7.config.{DeltaSpikeConfig, MyApplicationConfig}

@Path("config")
@RequestScoped
class ConfigResource {
  @Inject
  var deltaSpikeConfig: DeltaSpikeConfig = _

  @Inject
  var myApplicationConfig: MyApplicationConfig = _

  @GET
  @Produces(Array(MediaType.APPLICATION_JSON))
  def config(): java.util.Map[String, java.util.Map[String, String]] =
    Map(
      "apache-deltaspike" ->
        Map("database.server" -> deltaSpikeConfig.databaseName,
          "database.port" -> deltaSpikeConfig.databasePort.toString,
          "database.name" -> deltaSpikeConfig.databaseName,
          "jdbc.url" -> deltaSpikeConfig.jdbcUrl).asJava,
      "my-application" ->
        Map("application.name" -> myApplicationConfig.applicationName).asJava
    ).asJava
}

では、WildFlyを起動してデプロイしてみます。

$ bin/standalone.sh
$ cp /path/to/scala-2.11/ROOT.war /path/to/wildfly-10.0.0.Final/standalone/deployments/

確認。

$ curl http://localhost:8080/rest/config
{"apache-deltaspike":{"database.server":"test","database.port":"3306","database.name":"test","jdbc.url":"jdbc:mysql://localhost:3306/test"},"my-application":{"application.name":"for Production"}}

続いて、ProjectStageをDevelopmentにしてみます。

$ bin/standalone.sh  -Dorg.apache.deltaspike.ProjectStage=Development

確認。

$ curl http://localhost:8080/rest/config
{"apache-deltaspike":{"database.server":"development-test","database.port":"23306","database.name":"development-test","jdbc.url":"jdbc:mysql://localhost:3306/test"},"my-application":{"application.name":"for Development"}}

内容が、Developmentのものに切り替わりましたね。

変数展開が入るところだけは、やっぱりついてこれていませんが…。

"jdbc.url":"jdbc:mysql://localhost:3306/test"

まとめ

Apache DeltaSpikeのConfiguration Mechanism、ConfigResolverや@ConfigPropertyを試してみました。

個人的には、先にSpringの@PropertySourceや@Valueを使ったことがあったので、こちらに比べるとちょっとかゆいところに手が届いていない印象が…。

特に、変数展開のところの挙動が微妙な感じがします。TypedResolver APIもしくは@ConfigPropertyを使う場合は、混乱するかもなのであんまり使わない方がいいかもしれません。

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