読者です 読者をやめる 読者になる 読者になる

M12i.

学術書・マンガ・アニメ・映画の消費活動とプログラミングについて

Typesafe Config 「ライブラリを使用する」を読む

Java Scala

というわけで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()を使って生成する方法はもっとも簡単かつ平凡な方法です)、それをライブラリに渡します。ConfigConfigFactory のパーサー・メソッドを通じて生成することも、ConfigValueFactoryメソッドを使用して各種ファイル形式やデータ・ソースから設定値を組み立てることもできます〔訳注:この説明は性格ではありません。ConfigValueFactoryObjectMapからConfigValueConfigのサブタイプ)のインスタンスを組み立てるものであり、Fileその他の入力ソースからの設定情報の読み取りはConfigFactory が担当するようです〕。

「不変」性

オブジェクトは「不変」です。Configに格納された設定情報の変更操作を行うと新しいConfigインスタンスが返されます。 ConfigParseOptionsConfigResolveOptionsConfigObjectその他も「不変」です。詳細については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.resourceconfig.fileconfig.urlの指定をプログラムのなかで(例えばSystem.setProperty()を使って)行う場合、ConfigFactory が持つキャッシュのメカニズムにより新しいシステム・プロパティの値が参照されない可能性のあることに注意する必要があります。ConfigFactory.invalidateCaches()によりシステム・プロパティのリロードを強制することができます。

reference.confapplication.confの中で行われる置換処理について

置換構文 ${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つの大きな理由があります:

  1. もしアプリケーション内の複数の場所で設定項目を使用している場合、デフォルト値はハードコードされ、互いに同期されない状態となるのが一般的でしょう。これはとても厄介な事態を引き起こす可能性があります。
  2. もしgetterメソッドnull(あるいはScalaではNone)を返すなら、その設定項目のために毎回null/Noneを処理するコードを書くことになります。そしてその処理はというと大概は例外のスローということになるでしょう。あるいはもっとよくありそうなこととして、アプリケーションの側でnullチェックを忘れてしまい、結果当該の設定項目を欠いた状態で起動したアプリケーションがNullPointerExceptionをスローするということもあるでしょう。

ほとんどの場合、設定項目の欠落は単に修正すべきバグ(アプリケーションのコードによるものかそれが配備された環境によるものかのいずれか)です。そういうわけですから、もしある項目の設定が漏れていたとすれば、Configインターフェースのgetterメソッドは例外をスローすべきなのです。

もし特殊なケースとして、application.confにおける設定項目の欠落を許容したい場合には、いくつかの選択肢があります:

  1. ライブラリもしくはアプリケーションのjarファイルにreference.confを同梱しておき、そこにデフォルト値を記載しておく。
  2. Config.hasPath()メソッドを使ってあるパス〔キーもしくはドット区切りで連結されたキー〕が存在するかを事前にチェックする(他のAPIのように事後的にnull/Noneのチェックを行うよりはマシです)。
  3. ConfigException.Missingをキャッチして処理する。ノート:例外を使用してフローを制御する方法はConfig.hasPath()を使用する方法より速度の面で劣ります。例外をスローするためにJVMは多くのことをなす必要があるからです。
  4. 初期化コードのなかで、(ConfigFactory.parseMap()などを使用して)デフォルト値で初期化されたConfigを生成し、そのデフォルト設定情報をwithFallback()を使用して設定ファイルからロードされた設定情報に混ぜ込んで、それをプログラム内で使用する。このような「インライン化された」デフォルト設定は たぶんreference.confによるデフォルト設定よりも便利さに欠ける。しかしそうしなくてはならない理由があるかもしれない。
  5. Config.root()を使ってConfigのためConfigObject を取得する。ConfigObject java.util.Map<String,?> を実装しており、そのget()メソッドは存在しないキーに対してnullを返す。Config ConfigObjectのちがいについてより詳しくはJavadocを参照のこと。
  6. reference.confのなかで当該の項目について明示的にnullを設定し、Config.getIsNullConfig.hasPathOrNullnullに対処する。これにより設定項目が存在しないというときに例外スローするのとは異なるアプリケーション固有の方法で対応ができる。

推奨される方法(ほとんどのケース、ほとんどのアプリケーションのために)は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パラグラフは明らかに事実に即していない記述のためスキップします。〕

ConfigConfigObjectは同じ内部的なデータ構造にアクセスするための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.DurationConfigMemorySizeのほか、Config ConfigObject、はたまたConfigValue(もし任意の値を手動で処理するのを望まれるなら)もまた使用可能です。

          * * *

原典はTypesafe ConfigのGithubで公開されているREADME.mdのなかから"Using the Library"のセクションです(2015/08/15取得)。