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を単純に使うケースを書いてみます。
テストコードで確認しますが、とりあえず大枠だけ書いて、以降は順次見ていきます。
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を使ってみます。
なお、以下の設定は入っているものとします。
@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についての記述があります。
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