Typesafe Config 「ライブラリを使用する」を読む
というわけでTypesafe ConfigのGithubで公開されているREADME.mdのなかから"Using the Library"のセクションを読んでみました。
ただしどうも記載内容に古い部分があるらしく現状の事実に即していない部分があったのと、意味をとることができないセンテンスがあったりしたのとで、訳出をスキップした箇所があります(2015/08/15取得)。
* * *
API使用例
import com.typesafe.config.ConfigFactory Config conf = ConfigFactory.load(); int bar1 = conf.getInt("foo.bar"); Config foo = conf.getConfig("foo"); int bar2 = foo.getInt("bar");
もっと長めの例
プロジェクトのexamples
ディレクトリ配下にある例を参照してください。
sbtコンソールからproject config-simple-app-java
コマンドを実行しさらにrun
コマンドを実行することでそれらのプログラムを起動することができます。
例として示されていることを要約すれば次のようになります:
- ライブラリはアプリケーション〔ユーザーコード〕から提供された
Config
インスタンスを使用します。もしそれが提供されない場合はConfigFactory.load()
を使用します。ライブラリ開発者はデフォルトの設定をクラスパス上のreference.conf
ファイルに記載します。 - アプリケーションは
Config
インスタンスを好きな方法で生成して(ConfigFactory.load()
を使って生成する方法はもっとも簡単かつ平凡な方法です)、それをライブラリに渡します。Config
はConfigFactory
のパーサー・メソッドを通じて生成することも、ConfigValueFactory
のメソッドを使用して各種ファイル形式やデータ・ソースから設定値を組み立てることもできます〔訳注:この説明は性格ではありません。ConfigValueFactory
はObject
やMap
からConfigValue
(Config
のサブタイプ)のインスタンスを組み立てるものであり、File
その他の入力ソースからの設定情報の読み取りはConfigFactory
が担当するようです〕。
「不変」性
オブジェクトは「不変」です。Config
に格納された設定情報の変更操作を行うと新しいConfig
インスタンスが返されます。 ConfigParseOptions
やConfigResolveOptions
、ConfigObject
その他も「不変」です。詳細についてはJavadocを参照してください。
スキーマとバリデーション
スキーマ言語やそれに類するものは存在しません。しかしながら2つの代替案が用意されています:
checkValue()
メソッドを使用する- 設定情報にアクセスするためのフィールドを持つクラスを用意して、アプリケーション起動時にインスタンス化を行う(そしてもし何らかのミスが見つかったら即座に例外をスローする)。これをScalaで書く場合次のようになるでしょう:
class Settings(config: Config) { // reference.confとの比較によるバリデーション config.checkValid(ConfigFactory.defaultReference(), "simple-lib") // 遅延評価でないフィールドを使い、もし問題があればSettingsクラスの // インスタンス化のタイミングで例外として検知されるようにする val foo = config.getString("simple-lib.foo") val bar = config.getInt("simple-lib.bar") }
examples
ディレクトリ配下にコンパイル可能なプログラムの例が用意されています。詳しくはそちらも参照してください。
標準的な動作
便利なメソッドConfigFactory.load()
は次に挙げる一連のデータを読み込む(優先度の高い順に並べている)〔訳注:どれかが読み込まれるのではなく、すべてが、ただし優先度の低いほうから順番に読み込まれます。そして優先度の高いものと低いものとで同じキーの項目があった場合、前者の値で後者の値が上書きされます〕:
- システム・プロパティ
application.conf
(クラスパス上にある同名のリソースすべて)application.json
(同上)application.properties
(同上)reference.conf
(同上)
このような読み込みを行う意図はこうです。ライブラリもしくはフレームワーク側では reference.conf
を自身のjarファイルに詰め込んでおきます。アプリケーション(ユーザーコード)側ではapplication.conf
を用意するか、あるいはひとつのJVM上で複数の設定情報を利用したい場合ConfigFactory.load("myapp")
メソッドにより myapp.conf
をロードします(アプリケーションはそれが必要なら自分自身でreference.conf
を提供することも可能です。ただしapplication.conf
とは別にこうしたファイルを用意することに意味はないでしょう)。
アプリケーション側からカスタムメイドのConfig
オブジェクトが提供されない場合、ライブラリもしくはフレームワーク側はデフォルト設定をロードするため ConfigFactory.load()
を使用することになります。この場合、ライブラリはその設定情報を application.conf
からロードすることになります。そしてユーザー側は単一の application.conf
ファイルでライブラリとアプリケーション全体の設定を行うことになります〔訳注:ここで「単一の…」(single application.conf file)とありますが、実際には前述のように5種類の入力ソースからロードされます〕。
ライブラリやフレームワーク側では、デフォルトのそれではなくカスタムメイドのConfig
オブジェクトをアプリケーションの側から提供させることも可能です。この場合、アプリケーション側ではJVM上で〔つまりクラスパス上で〕複数の設定ファイル〔というより「applicationというデフォルトの名前でない設定ファイル」〕を用意するか、他のどこか別の場所にある設定ファイルをロードするということになります。examples
ディレクトリ配下のプログラムにはカスタムメイドのConfig
を受け入れる方法を示したものがあります。
application.{conf,json,properties}
とシステム・プロパティを使用するアプリケーションのため、任意の設定情報ソースを使用することを強制する方法があります:
config.resource
− クラスパス上のリソース名を指定。ただしベース名ではない。つまり、application.conf
であってapplication
ではない。config.file
− ファイルシステム上のパスで指定。こちらも拡張子を含める必要がある。ベース名ではNG。config.url
– URLで指定。
これらのシステム・プロパティで指定した設定情報ソースはapplication.{conf,json,properties}
を置き換えてしまいます。追加ではありません。またこの方法が通用するのは ConfigFactory.load()
を使ってデフォルトの設定ファイルを〔つまりapplication.{conf,json,properties}
を〕使用しているアプリケーションに対してだけです。設定ファイルの置き換えにあたり、include "application"
ステートメントを使用することで、オリジナルのデフォルトの設定ファイルの内容を取り込むことができます。このステートメントのあとであればデフォルトの設定を上書きしたりすることが可能になります。
config.resource
や config.file
、config.url
の指定をプログラムのなかで(例えばSystem.setProperty()
を使って)行う場合、ConfigFactory
が持つキャッシュのメカニズムにより新しいシステム・プロパティの値が参照されない可能性のあることに注意する必要があります。ConfigFactory.invalidateCaches()
によりシステム・プロパティのリロードを強制することができます。
reference.conf
とapplication.con
fの中で行われる置換処理について
置換構文 ${foo.bar}
は2回処理されます。まずすべてのreference.conf
ファイルの設定項目がマージされ続いて置換処理が行われます。次にすべてのapplication.conf
の設定項目がreference.conf
の設定項目に上書きされます。ここでもまた置換処理が行われます。
このことは次のことを意味します。 reference.conf
のスタックは自己完結的でなければなりません。application.conf
で定義されている項目〔つまりreference.conf
の段階では未定義の項目〕を使用することはできません。application.conf
で上書きされた後の値を参照するというのもできません。反対にapplication.conf
からはreference.conf
で定義されている項目を参照することが可能です。
このことでたまにもどかしい思いをされるかもしれませんが、回避策はあります:
- ライブラリのjarファイルの中に
reference.conf
とともにapplication.conf
も格納しておき、そこに遅延評価されるべき項目を定義しておく。 - 設定ファイルだけで値を組み上げるかわりにプログラム・ロジックでなんとかする。
設定情報ツリーのマージ
どんなConfig
オブジェクトであれ、withFallback
メソッドを使ってマージすることができます。 merged = firstConfig.withFallback(secondConfig)
といったふうに。
withFallbackの処理はライブラリの内部で、同一ファイル内の重複したキーの値どうしをマージしたり、複数の設定ファイルのマージをしたりするのに使用されています。ConfigFactory.load()
はこのメソッドをreference.conf
の設定情報の上にapplication.conf
の設定情報を、そしてそのまた上にシステム・プロパティをというふうにスタックするのに使用しています。
もちろんwithFallback
メソッドは〔直接それを呼び出して〕ハードコードされた複数の値をマージしたり、サブツリーの項目をルートの項目に「持ち上げ」るために使用したりできます。例えば次のような設定があったとします:
foo=42 dev.foo=57 prod.foo=10
すると次のようなコードを書くことができます:
Config devConfig = originalConfig
.getConfig("dev")
.withFallback(originalConfig)
withFallback
メソッドにはもっといろいろな使用方法があります。
デフォルト値をどう処理するか
多くの他のコンフィギュレーションAPIではデフォルト値を指定することが可能なgetterメソッドが用意されています。例えば次のように:
boolean getBoolean(String path, boolean fallback)
さて、ここで仮に当該の項目が設定ファイルになかった場合、フォールバックとして指定した値が返されることになります。APIによっては値が設定されていない場合にnull
を返すので、ユーザーの側ではnull
チェックをすることになります:
// returns null on unset, check for null and fall back Boolean
getBoolean(String path)
Config
インターフェースにはこれに該当するものがありません。それには2つの大きな理由があります:
- もしアプリケーション内の複数の場所で設定項目を使用している場合、デフォルト値はハードコードされ、互いに同期されない状態となるのが一般的でしょう。これはとても厄介な事態を引き起こす可能性があります。
- もしgetterメソッドが
null
(あるいはScalaではNone
)を返すなら、その設定項目のために毎回null
/None
を処理するコードを書くことになります。そしてその処理はというと大概は例外のスローということになるでしょう。あるいはもっとよくありそうなこととして、アプリケーションの側でnullチェックを忘れてしまい、結果当該の設定項目を欠いた状態で起動したアプリケーションがNullPointerException
をスローするということもあるでしょう。
ほとんどの場合、設定項目の欠落は単に修正すべきバグ(アプリケーションのコードによるものかそれが配備された環境によるものかのいずれか)です。そういうわけですから、もしある項目の設定が漏れていたとすれば、Config
インターフェースのgetterメソッドは例外をスローすべきなのです。
もし特殊なケースとして、application.conf
における設定項目の欠落を許容したい場合には、いくつかの選択肢があります:
- ライブラリもしくはアプリケーションのjarファイルに
reference.conf
を同梱しておき、そこにデフォルト値を記載しておく。 Config.hasPath()
メソッドを使ってあるパス〔キーもしくはドット区切りで連結されたキー〕が存在するかを事前にチェックする(他のAPIのように事後的にnull
/None
のチェックを行うよりはマシです)。ConfigException.Missing
をキャッチして処理する。ノート:例外を使用してフローを制御する方法はConfig.hasPath()
を使用する方法より速度の面で劣ります。例外をスローするためにJVMは多くのことをなす必要があるからです。- 初期化コードのなかで、(
ConfigFactory.parseMap()
などを使用して)デフォルト値で初期化されたConfig
を生成し、そのデフォルト設定情報をwithFallback()
を使用して設定ファイルからロードされた設定情報に混ぜ込んで、それをプログラム内で使用する。このような「インライン化された」デフォルト設定は たぶんreference.conf
によるデフォルト設定よりも便利さに欠ける。しかしそうしなくてはならない理由があるかもしれない。 Config.root()
を使ってConfig
のためConfigObject
を取得する。ConfigObject
はjava.util.Map<String,?>
を実装しており、そのget()
メソッドは存在しないキーに対してnull
を返す。Config
とConfigObject
のちがいについてより詳しくはJavadocを参照のこと。reference.conf
のなかで当該の項目について明示的にnullを設定し、Config.getIsNull
とConfig.hasPathOrNull
でnull
に対処する。これにより設定項目が存在しないというときに例外スローするのとは異なるアプリケーション固有の方法で対応ができる。
推奨される方法(ほとんどのケース、ほとんどのアプリケーションのために)はreference.conf
もしくはapplication.conf
のなかにすべての設定項目が存在することを必須とし、その前提が満たされない場合にはConfigException.Missing
をスローするというものです。それこそConfig APIの設計者の意図したものです。
すべての設定項目がそろっていることを検証するためのcheckValid()
の使用を伴う「設定クラス」パターン〔"Settings class" pattern 〕の採用を検討してください。これについては「スキーマとバリデーション」のセクションで触れています。
ある設定項目を任意の項目としたい場合は: Javaプログラムの場合、null
値に対する事後検証との代わりに、それと同じ数だけhasPath()
による事前検証のコードを書くことになります。そうでなければその項目が欠落していたときにNullPointerException
がスローされるリスクを負うことになります。 ScalaではOption
を使った独特の構文でより高機能なConfig
を実現できます:
implicit class RichConfig(val underlying: Config) extends AnyVal { def getOptionalBoolean(path: String): Option[Boolean] = if (underlying.hasPath(path)) { Some(underlying.getBoolean(path)) } else { None } }
もちろん、このライブラリはJavaで実装されたものですので、上記のようなクラスをライブラリ側で提供することはできません。
〔訳注:このあとの数パラグラフは意味がうまく汲めなかったのでスキップします。〕
Config とConfigObjectを理解する
設定情報を読み込み・書き込みするため、 Config
インターフェースを利用するはずです。Config
インターフェースはJSON相当のデータ構造を、パスとそれに対応する値とからなるフラット化された単一レベルのマップとして捉えます。そのため例えば次のようなJSONがあった場合:
"foo" : { "bar" : 42 "baz" : 43 }
Config
インターフェースを使用してconf.getInt("foo.bar")
というコードを書くことができます。このfoo.bar
という文字列をパス式〔path expression〕と呼びます(HOCON.md にはこれらの式の構文について詳しい説明があります)。この Config
に対するイテレーションにより、2つのエントリー── "foo.bar" : 42
と"foo.baz" : 43
が得られるでしょう。このときネストされたConfig
は得られません(なぜなら〔前述のとおり〕何事も単一レベルのマップ構造のなかで捉えられるからです)。
JSONのツリー構造をConfig
として見た場合、null
値はそれが存在しないかのように扱われます。つまりイテレーションではスキップされます。
Config
インターフェースを多くのJSON APIがするような方法で扱うこともできます。これにはConfigObject
インターフェースを用います。このインターフェースはJSONのツリー構造におけるオブジェクト・ノードを表します。ConfigObject
は複数レベルのツリー構造をとります。そしてそのキーはいかなる構文ルールも持ちません(つまりそれは単なる文字列であり、パス式ではありません)。先ほど例示したデータをConfigObject
としてイテレーションした場合、単一のエントリー"foo" : { "bar" : 42, "baz" : 43 }
が得られます。このとき"foo"
の値はネストしたConfigObject
インスタンスとなります。
ConfigObject
インターフェースでは、 null
値はスキップされません(つまり「項目が欠落している」のとは区別されます)。これはJSONにおけるのと同じです。
〔訳注:次の1パラグラフは明らかに事実に即していない記述のためスキップします。〕
Config
とConfigObject
は同じ内部的なデータ構造にアクセスするための2つの異なる方法を提供します。インターフェース相互に変換を行うためConfig.root()
とConfigObject.toConfig()
を使用することができます。
ConfigBeanFactory
バージョン1.3.0では、Config
を使ってJavaBean規約(引数なしのコンストラクタとgetter・setter)に準拠したオブジェクトを初期化することができるようになりました。
ConfigBeanFactory.create(config.getConfig("subtree-that-matches-bean"), MyBean.class)
というふうにすればこれが実現できます。
Config
をもとにオブジェクトを初期化するとき、そのビーンが示唆するスキーマに基づき自動的にバリデーションが実行されます。ビーンのフィールドにはプリミティブ型やList<Integer>
のように〔ジェネリクスにより〕型付けされたリスト、java.time.Duration
、ConfigMemorySize
のほか、Config
や ConfigObject
、はたまたConfigValue
(もし任意の値を手動で処理するのを望まれるなら)もまた使用可能です。
* * *
原典はTypesafe ConfigのGithubで公開されているREADME.mdのなかから"Using the Library"のセクションです(2015/08/15取得)。