読み方
本文は Spring Boot 2.7.18 の実装を基準に、ConfigDataEnvironment.processAndApply() の実際の実行経路をたどります。
制御フローを追いやすくするため、コードでは主に方法名、条件分岐、データ構造を明記し、ログ、例外のラップアップ、メインラインと無関係な細部は省いています。
全文では、次の5つの動作を明確に区別します。
- resolve:
ConfigDataLocationをConfigDataResourceに解決する。 - load:Loader が Resource を読み込み、
ConfigDataとPropertySourceに変換する。 - bind:単一の
PropertySourceからspring.config.importとspring.config.activateをバインドする。 - active:Contributor が現在の
ActivationContextで有効化条件を満たす。 - apply:有効な
PropertySourceを最終的に実際のEnvironmentへ追加する。
ConfigData は単なる「application.yml を読んだら終わり」のファイルローダーではなく、設定依存グラフを段階的に解く処理です。各段階では、その時点で特定できた import を展開し続け、Contributor Tree が安定した状態に到達するまで続けます。Profile が確定してから Profile 固有のリソースを追加し、最後にまとめて Environment へ適用します。
2. 起動の入口と初期 Contributor Tree
ConfigData の実行開始点は Environment の準備段階です。
SpringApplication は ApplicationContext を作成してリフレッシュする前に、先に ConfigurableEnvironment を準備します。EnvironmentPostProcessorApplicationListener が ConfigDataEnvironmentPostProcessor を呼び出し、後者が ConfigDataEnvironment を構築して processAndApply() を実行します。
この時点では、次の処理はいずれも始まっていません。
- BeanDefinition のスキャン
- 自動設定クラスの解決
- シングルトン Bean の生成
ApplicationContextのリフレッシュ
入口となる呼び出しチェーンは次のとおりです。
2.1 初期 Contributor の由来
ConfigDataEnvironment のコンストラクタは、現在の Environment にすでに存在する PropertySource を読み込み、Kind が Kind.EXISTING の Contributor にラップします。
通常、次のようなソースが含まれます。
commandLineArgssystemPropertiessystemEnvironmentdefaultProperties
これらは早期の Binder クエリやプレースホルダー解決には参加しますが、ConfigData の仕組みによって再インポートされることはありません。
そのうえで、フレームワークは次の設定に基づいて INITIAL_IMPORT ノードを作成します。
spring.config.importspring.config.additional-locationspring.config.location
spring.config.location を明示的に設定していない場合は、デフォルトの検索位置を使用します。
classpath:/
classpath:/config/
file:./
file:./config/
file:./config/*/
初期位置を Resolver にすぐ渡すのではなく Contributor として表すのは、初期位置と後続の spring.config.import の位置を同じ処理フローに通すためです。
選択する待機中のノード
↓
resolveAndLoad
↓
子 Contributor を生成
↓
不変のツリーを置換
初期ツリーの論理構造は次のとおりです。
INITIAL_IMPORT は読み込み済みの特定のファイルを表すものではなく、PropertySource も提供しません。保持するのは ConfigDataLocation の集合だけで、withProcessedImports() の中で最初の resolveAndLoad() を起動します。
4. 全体を通して使う設定例
次の設定には、以下の要素がすべて含まれています。
- 条件指定のない import
- CloudPlatform 条件
- Profile 条件
- 再帰的な import
- Profile 固有のファイル
実行環境が最終的に Kubernetes と判定され、基本ドキュメントが dev Profile を有効にする場合を前提とします。
application.yml のドキュメント0です。
spring:
profiles:
active: dev
config:
import: classpath:common.yml
app:
source: base
application.yml のドキュメント1です。
spring:
config:
activate:
on-cloud-platform: kubernetes
import: classpath:k8s.yml
platform:
mode: kubernetes
application.yml のドキュメント2です。
spring:
config:
activate:
on-profile: dev
import: classpath:feature.yml
feature:
mode: dev-document
common.yml です。
spring:
config:
import: classpath:db.yml
common:
enabled: true
db.yml です。
db:
url: jdbc:mysql://localhost/base
k8s.yml です。
platform:
service-discovery: dns
feature.yml です。
feature:
enabled: true
application-dev.yml です。
spring:
config:
import: classpath:dev-db.yml
app:
source: application-dev
dev-db.yml です。
db:
pool-size: 20
「ファイル」と「ドキュメント」は明確に区別する必要があります。
application.yml は Loader が一度に読み込みます。ただし、その中の3つの YAML ドキュメントはそれぞれ独立した PropertySource となり、3つの独立した Contributor も生成されます。
各 Contributor には、imports、activate、active 状態、children がそれぞれあります。
そのため、Loader がファイルを読み込んだ時点で、ファイル内のすべてのドキュメントが現在の段階で有効になっているわけではありません。
6. processAndApply における3回の import 処理
processAndApply() は withProcessedImports() を3回呼びますが、ImportPhase 自体は2種類しかありません。
BEFORE_PROFILE_ACTIVATIONAFTER_PROFILE_ACTIVATION
最初の2回はどちらも BEFORE に対応します。違いは ActivationContext の完成度です。3回目は Profiles を作成した後に AFTER に入ります。
main の流れは次のとおりです。
6.1 1回目:context が null の BEFORE
processInitial() には null の ActivationContext が渡されます。
ImportPhase.get(null) は BEFORE_PROFILE_ACTIVATION を返します。
spring.config.activate を宣言していないドキュメントは、activate フィールドが null なので、properties.isActive(null) でも active のままです。
1つでも activate 条件を宣言したドキュメントは Activate.isActive(null) に進み、そのまま false を返します。
この段階では application.yml を1階層読むだけではありません。
無条件ドキュメントの imports は、次のように展開され続けます。
application.yml
↓ import
common.yml
↓ import
db.yml
外側の while は、その時点で確認できる無条件の依存チェーン全体で、bind と import の処理がすべて終わるまで動き続けます。
6.2 2回目:CloudPlatform は判明、Profiles は null の BEFORE
1回目に安定した後、createActivationContext() は現在の Environment と、Contributor をバックエンドとする Binder を使って CloudPlatform を推論します。profiles は明示的に null に設定されます。
推論は、まず設定で CloudPlatform が強制指定されていないかを確認し、その後 CloudPlatform.getActive(environment) を呼び出すという順序です。
processWithoutProfiles() は再び withProcessedImports() を呼び出します。
現在の phase は STILL BEFORE ですが、前回は on-cloud-platform の条件で inactive だったドキュメントも再判定できます。
プラットフォームが一致すると、これらのドキュメントの import は展開されます。
on-profile のみを宣言したドキュメントは、引き続き inactive です。
プラットフォームと Profile の両方を宣言したドキュメントは、両方の条件が同時に満たされる必要があるため、3段階目まで待つ必要があります。
6.3 Profile の推論と3回目の AFTER
withProfiles() は現在の Contributor Tree から Binder を構成し、IGNORE_PROFILES が付いた PropertySource を除外したうえで、次の設定を読み取ります。
spring.profiles.activespring.profiles.defaultspring.profiles.includespring.profiles.groupadditionalProfiles
Profiles のコンストラクタは Profile group も展開し、順序を保ち循環を停止するために LinkedHashSet を使います。
その後、activationContext.withProfiles(profiles) によって CloudPlatform と Profiles を同時に含む新しいコンテキストが生成されます。
processWithProfiles() は3回目の withProcessedImports() を呼び出します。
この時点では activationContext.getProfiles() != null なので、ImportPhase.get() は AFTER_PROFILE_ACTIVATION を返します。
この段階では、次の2つの新しい機能が使えます。
on-profileドキュメントを active と判定できる- Resolver が通常の
resolve()に加えてresolveProfileSpecific()も実行できる
そのため、標準のファイル用 Resolver は application-dev.yml、common-dev.yml、application-prod.yml を検出できます。
8. 実際のデータ構造で全実行過程を追う
8.1 初期ツリー
EXISTING ノードを無視すると、初期ツリーは次のとおりです。
ROOT
└─ INITIAL_IMPORT
imports = [classpath:/]
children = {}
8.2 1回目の BEFORE:基本ドキュメントの読み込みと無条件 import の展開
INITIAL_IMPORT が選択されます。有効化条件はなく、imports は空ではなく、children に BEFORE の key もありません。
profiles が null の間、Resolver は通常の resolve だけを実行し、application.yml を検出します。
Loader は3つの YAML ドキュメントを読み込み、1つの ConfigData を生成します。
asContributors() は PropertySource の逆順で3つの UNBOUND_IMPORT を生成し、INITIAL_IMPORT.withChildren(BEFORE, children) によって新しいツリーへ組み込みます。
INITIAL_IMPORT
└─ BEFORE
├─ UNBOUND application.yml#2
├─ UNBOUND application.yml#1
└─ UNBOUND application.yml#0
この時点では3つのドキュメントはすでに物理的に読み込まれていますが、activate と import はまだ bind されていません。
後順の priority-order 走査では、まず application.yml#2 に到達します。Kind が UNBOUND_IMPORT なので active は判定せず、withBoundProperties() を実行して次の状態になります。
BOUND application.yml#2
activate.on-profile = dev
imports = [feature.yml]
現在の context は null なので isActive(null) == false となり、feature.yml はまだ展開されません。
続いて application.yml#1 を bind します。
BOUND application.yml#1
activate.on-cloud-platform = kubernetes
imports = [k8s.yml]
context は引き続き null なので、ここも inactive で、k8s.yml はまだ展開されません。
最後に application.yml#0 を bind します。
BOUND application.yml#0
activate = null
imports = [common.yml]
spring.profiles.active = dev
null context でも active なので、次のラウンドで選択され、common.yml を読み込み、BEFORE children に追加します。
common.yml はまず UNBOUND common.yml となり、その後、次のように bind されます。
BOUND common.yml
imports = [db.yml]
有効化条件がないため、db.yml の読み込みも続きます。db.yml を bind した後は imports がないため、待処理ノードにはなりません。
1回目の BEFORE で安定した時点のツリーは次のとおりです。
この時点では application.yml#1 と application.yml#2 は load と bind が完了していますが、内部の import はまだ展開されていません。
第1段階の終了条件は、「すべてのファイルが active になった」ことではありません。現在のツリーに次のものが存在しないことです。
UNBOUND_IMPORT- active かつ BEFORE が未処理の Contributor
8.3 2回目の BEFORE:プラットフォーム条件の設定を追加
createActivationContext() が CloudPlatform.KUBERNETES と profiles = null を推論します。
2回目の後順走査で、application.yml#1 のプラットフォーム条件が一致します。
activate.on-cloud-platform = kubernetes
現在のプラットフォーム = Kubernetes
したがって isActive(context) == true となり、BEFORE の key はまだないため、k8s.yml を読み込みます。
application.yml#1
activate.on-cloud-platform = kubernetes
isActive(context) = true
children[BEFORE] = [k8s.yml]
k8s.yml は bind 完了後に imports がありません。
application.yml#2 は引き続き dev Profile を必要としており、profiles == null です。
application.yml#2
activate.on-profile = dev
isActive(context) = false
そのため inactive のままです。
8.4 withProfiles:active な設定から dev を推論
withProfiles() が作成する Binder は IGNORE_PROFILES を除外し、重要な bind には FAIL_ON_BIND_TO_INACTIVE_SOURCE を有効にします。
基本ドキュメント application.yml#0 は現在 active で、その中の spring.profiles.active: dev が読み取られます。
Profiles は同時に次を処理します。
spring.profiles.activespring.profiles.defaultspring.profiles.includespring.profiles.groupadditionalProfiles
group の展開後、最終的なコンテキストは次のようになります。
CloudPlatform = KUBERNETES
Profiles.active = [dev]
8.5 3回目の AFTER:Profile 条件と Profile 固有のファイルを追加
application.yml#2 を再判定すると、次のようになります。
on-profile = dev
Profiles が dev を受理
したがって active になります。AFTER の key がないため、feature.yml は AFTER 段階で読み込まれ、bind されます。
BEFORE で imports を処理済みのノードでも、AFTER の key はまだありません。そのため、同じ imports を Profiles 付きで再度 Resolver に渡します。
Resolver は次を実行します。
resolve(location)
resolveProfileSpecific(location, profiles)
通常の Resource はすでに ConfigDataImporter.loaded に存在するため、重複は除去されます。新しく現れた Profile 固有の Resource だけが読み込み結果に入ります。
INITIAL_IMPORT は AFTER 段階で classpath:/ を再解決します。通常の application.yml はすでに読み込み済みなので重複除去されます。
StandardConfigDataLocationResolver は dev に対して application-dev.yml を生成します。
StandardConfigDataLoader はこの PropertySource に PROFILE_SPECIFIC を設定します。
application-dev.yml はまず AFTER children に UNBOUND_IMPORT として追加され、その後、次のように bind されます。
imports = [dev-db.yml]
現在は Profile が確定し、このドキュメントも active なので、dev-db.yml は AFTER 段階で展開され続けます。
最終ツリーの大まかな構造は次のとおりです。
application.yml の条件付きドキュメントは、最初の通常の解決で得られたため、最初は INITIAL_IMPORT の BEFORE children に置かれます。内部の import が後で active になったとき、対応する phase の children へ書き込まれます。
application-dev.yml は resolveProfileSpecific() による Profile 固有のリソースであり、ツリー構造の調整後は AFTER の高優先度領域に置かれます。
10. BEFORE と AFTER の本当の違い
ImportPhase は、現在のノードの imports が Profile 推論の前と後のどちらで実行されるかを示します。
ノードが active か inactive かを直接表すものではありません。
ImportPhase.get() のロジックは、次の1つの判定だけです。
phase = activationContext != null
&& activationContext.getProfiles() != null
? AFTER_PROFILE_ACTIVATION
: BEFORE_PROFILE_ACTIVATION;
したがって、BEFORE は Profiles が未確定、AFTER は Profiles が確定済みという意味です。
10.1 Resolver の動作の違い
ConfigDataImporter は ActivationContext から Profiles を取り出し、ConfigDataLocationResolvers.resolve(...) に渡します。
Resolver はまず通常の解決 resolver.resolve(context, location) を必ず呼びます。
Profiles が null なら、通常の解決結果をそのまま返します。
Profiles が null でなければ、さらに resolver.resolveProfileSpecific(context, location, profiles) を実行します。
したがって、次のようになります。
- 通常の解決:
resolve(location) - Profile 確定後の解決:
resolve(location)+resolveProfileSpecific(location, profiles)
標準のファイル位置では、resolveProfileSpecific() が受理済みの Profile を走査し、Profile を StandardConfigDataReference に渡します。
たとえば、次のような候補が生成されます。
- ディレクトリ
classpath:/からapplication-dev.yml - 明示的なファイル
common.ymlからcommon-dev.yml
StandardConfigDataLoader は resource.getProfile() != null を検出すると、対応する PropertySource に PROFILE_SPECIFIC を付けます。
10.2 同じ imports を2回処理する理由
BOUND Contributor の imports は、BEFORE の処理完了後には children[BEFORE] だけを持ちます。
AFTER に入っても hasUnprocessedImports(AFTER) は true のままです。そのため、同じ Location は2つのコンテキストでそれぞれ1回ずつ解決されます。
1回目は Profiles = null なので、通常の Resource だけを解決します。
2回目は Profiles = [dev] なので、通常の Resource と Profile 固有の Resource を解決します。
ConfigDataImporter.loaded は ConfigDataResource を使って重複除去するため、同じ通常の Resource に対して Loader.load() が再度呼ばれることはありません。
2回目の解決の価値は、以前は存在しなかった Profile 固有の Resource を検出できることにあります。
「2回処理」は、同じ設定位置を2つのコンテキストでそれぞれ1回ずつ resolve するという意味であり、同じファイルを物理的に2回読むという意味ではありません。
10.3 PROFILE_SPECIFIC によるツリー構造の調整
withChildren(AFTER, children) は moveProfileSpecific() を呼び出します。
このメソッドは BEFORE のサブツリーから PROFILE_SPECIFIC Option を持つ子ノードを探し、元の位置から取り除き、一時的な Option を外したうえで、現在のノードの AFTER children へ追加します。
この調整によって、Profile 固有のソースは Profile の有効化後の高優先度領域に入ります。Profile 固有ではない import の元の親子関係は保たれます。
次の2つは関連.state していますが、意味は異なります。
fromProfileSpecificImport:ConfigDataResolutionResult.isProfileSpecific()から得られ、Resource がresolveProfileSpecific()によって生成されたことを示します。PROFILE_SPECIFICOption:PropertySourceに付くもので、後のツリー構造と優先度の調整に使われます。
12. Options、重複除去、例外、最終的な優先順位
12.1 ConfigData.Option の由来と役割
Options は ConfigDataLoader が ConfigData を構築するときに渡され、PropertySource ごとに ofUnboundImport() へ引き渡されます。
Spring Boot は、「ローカル設定」「リモート設定」「設定センター」などの種類に基づいてこれらのマークを一括で推論しません。カスタム Loader が返す Options AZYazy取决于其实现。
| Option | 設定箇所 | 使用箇所 | 役割 |
|---|---|---|---|
IGNORE_IMPORTS | Loader または EMPTY_LOCATION の固定オプション | withBoundProperties() | bind 後に spring.config.import を取り除く。ただし通常のプロパティは引き続き提供できる |
IGNORE_PROFILES | Loader が決定 | withProfiles()、include の走査、不正なプロパティの検証 | その PropertySource を active、default、include、group の推論に参加させない |
PROFILE_SPECIFIC | StandardConfigDataLoader が resource.profile != null の場合 | moveProfileSpecific() | Profile 固有のソースを AFTER の高優先度領域へ移動する |
EMPTY_LOCATION は IGNORE_IMPORTS を使います。Location 自体は有効ですが実際の PropertySource がないことを意味し、空のノードから import を読むべきではありません。
IGNORE_PROFILES の正確な定義は、その PropertySource 内にある Profile 推論に影響するプロパティを無視することです。「リモート設定は必ず Profile を無視する」とは同義ではありません。
12.2 Resource の重複除去と循環 import
ConfigDataImporter は Set<ConfigDataResource> loaded を維持します。
候補の Resource がすでに存在すれば、Loader は再度呼ばれません。
該当する Location は loadedLocations に記録されます。
A import B、B import A だとします。
重複判定の基準は文字列の Location ではなく、ConfigDataResource.equals() と ConfigDataResource.hashCode() です。
異なる Location が同じ Resource に解決された場合も、読み込みは1回だけです。
同じ Location から AFTER 段階で新たな Profile 固有の Resource が得られた場合、Resource が異なるため読み込みできます。
12.3 optional、空の位置、必須位置の検査
optional: プレフィックスと Resource 自身の optional 状態は、ConfigDataImporter に記録されます。
ConfigDataNotFoundException は optional の位置では無視されます。optional でない位置は、spring.config.on-not-found の設定に従って処理されます。
Resolver は有効だが空のディレクトリ Resource を返すことがあります。Loader は ConfigData.EMPTY を返し、続いて EMPTY_LOCATION Contributor が作られます。
最終 apply の前、checkMandatoryLocations() は active Contributor に含まれる optional でない imports を集め、次を除外します。
- ツリーにすでに存在する Location
loadedLocationsoptionalLocations
残った Location は、強制 import として正常に解決されなかったものとみなされます。
12.4 最終 apply とプロパティの優先順位
applyToEnvironment() は最初に次を実行します。
- 不正な Profile プロパティの検証
- 必須位置の検査
その後、ContributorIterator でツリー全体を走査します。
次の条件を満たすノードだけが Environment に追加されます。
Kind == BOUND_IMPORT
PropertySource != null
最終 ActivationContext のもとで active
構造化すると次のようになります。
inactive ノードも Contributor Tree には残りますが、Environment には入りません。
コマンドライン引数や systemProperties などの EXISTING PropertySource はすでに Environment にあり、ConfigData の結果より前に置かれているため、優先度の高いままです。
ConfigData ノード内部では、AFTER → BEFORE → parent の走査順序が優先順位を決めます。
最後に DefaultPropertiesPropertySource は末尾へ移動し、デフォルトのプロパティが引き続き最低の優先度になるようにします。
14. 完全な実行モデル
ConfigData の完全な実行過程は、次の連続したステップにまとめられます。
第1ステップ、既存の Environment PropertySource をラップし、INITIAL_IMPORT を作成して ROOT を構成します。
ROOT
├─ EXISTING
├─ INITIAL_IMPORT
└─ EXISTING
第2ステップ、1回目の BEFORE を実行します。
context = null
Profiles = null
CloudPlatform = null
無条件ドキュメントを読み込み、bind しながら継続し、無条件 imports を再帰的に展開します。
第3ステップ、ActivationContext を作成します。Environment と Contributor Binder から CloudPlatform を推論し、Profiles は引き続き null にします。
第4ステップ、2回目の BEFORE を実行します。プラットフォーム条件を再判定し、on-cloud-platform ドキュメントの imports を展開し続けます。
第5ステップ、Profiles を計算します。
IGNORE_PROFILESを除外する- active を読み取る
- default を読み取る
- include を読み取る
- additionalProfiles を読み取る
- group を展開する
第6ステップ、3回目の AFTER を実行します。
- on-profile 条件を再判定する
- 各 imports を Profiles 付きで再解決する
application-dev.yml、common-dev.yml、その他の Profile 固有 Resource を検出する- 読み込み済み Resource の集合で重複を除去し、新しく生成されたノードも再帰的に展開し続ける
第7ステップ、各段階の内部では、同じ不動点アルゴリズムが実行されます。
第8ステップ、最終 apply を実行します。
- 必須位置を検証する
- 不正な Profile プロパティを検証する
- active な BOUND PropertySource だけを追加する
AFTER → BEFORE → parentの順に並べる- Environment の defaultProfiles と activeProfiles を設定する
最終的なメンタルモデルは次のとおりです。
- Contributor Tree は ConfigData の中間表現です
- ActivationContext は、その時点で展開できるノードを決めます
- ImportPhase は、位置の解決時に Profiles が使えるかどうかを決めます
- ContributorIterator は、優先度付きの後順走査を提供します
withProcessedImports()の while ループは、現在解ける設定依存を展開し、安定した状態まで続けますapplyToEnvironment()が、最終的な結果を実際の Environment に渡します