なぜこの記事を書こうと思ったのですか?
昨年の Log4j コアのセキュリティ問題により、サプライ チェーンのセキュリティは再び最高潮に達しました。サプライ チェーン セキュリティの観点から、Ant Group の 2 つの製品、静的コード スキャン プラットフォーム - STC と資産脅威パースペクティブ プラットフォーム - Hubble は、互いの利点を補完し、直接依存と間接依存のシナリオを効果的に解決します。
ただし、STCは事前ベースであるためスキャン効率の限界により欠落のリスクがあり、ハッブルは事後ベースであるため修復時間のリスクがあります。これに基づいて、著者は 2 つの製品の欠点を同時に解決する方法を見つけようとしました。著者は、Maven がプロジェクト内の直接依存関係と間接依存関係をどのように処理するか、また同じ依存関係に遭遇したときに Maven がどのように決定を下すかを研究しようとしました。ここでの決定は、実際には Maven の調停メカニズムです。これらの疑問を念頭に置いて、著者は Maven のソース コードを調査しようとし、ローカル テスト実験をいくつか行いました。これで記事は終わります。
座標は何ですか?
空間座標系では、xyz を通じて点を表すことができます。同様に、Maven の世界では、次のような GAV のセットを通じて依存世界の依存関係を明確に表すことができます。
<groupId> : com.alibaba 一般是公司的名称
<artifactId> : fastjson 项目名称
<version> : 1.2.24 版本号
依存関係に影響を与えるタグは何ですか?
1.<依存関係>
特定の依存関係情報を直接導入します。これは <dependencyManagement> タグ内にないことに注意してください。<dependencyManagement> 内にある場合は、ラベル No.2 を参照してください。
2.<依存関係の管理>
依存関係の管理として、宣言のみが行われ、実際の導入は行われません。依存関係管理とは、実際に依存関係が発生した際に、依存関係管理データを参照することをいう。
-
この方法で依存関係を使用する場合、デフォルトでバージョンを使用できます。
-
さらに、<dependencyManagement> はすべての間接的な依存関係を制御することもでき、間接的な依存関係でバージョンが宣言されている場合でも、それは上書きされます。
3.<親>
父に宣言しておきますが、Maven 自体も Java で実装されており、単一継承を満たすため、Maven の継承哲学は Java と非常に似ています。
-
子 pom が親 pom を継承すると、親 pom の <dependency>、<dependencyManagement> およびその他の属性を継承します。もちろん、継承プロセス中に同じ要素が出現した場合、Java と同様に、子が親を上書きします。
-
相続する場合、相続分は分類されます。依存関係は依存関係を継承し、dependencyManagement の依存関係管理は dependencyManagement のスコープ内でのみ依存関係管理を継承できます。
-
すべての pom ファイルには親があり、Parent が宣言されていない場合でも、デフォルトで親が存在します。これは Java のオブジェクト設計哲学に似ています。これについては、後のソース コード分析で説明します。
4.<プロパティ>
現在の独自のプロジェクトを表すプロパティのコレクション。
プロパティはプロパティの宣言を表すだけであり、プロパティが宣言されると、それが参照されるかどうかには関係がありません。誰も使用しない一連のプロパティを宣言できます。
依存スコープとは何ですか?
依存関係が導入されると、依存関係のスコープを宣言できます。たとえば、この依存関係はローカルでのみ機能します。たとえば、テストの場合のみ機能します。スコープには、コンパイル値、提供値、システム値、テスト値、インポート値、ランタイム値の合計が含まれます。
簡単に要約すると、次のようになります。
-
コンパイルとランタイムは最終的なパッケージ化プロセスに参加しますが、残りは参加しません。コンパイルを記述する必要はありません。
-
test は、src/test ディレクトリ内のテスト コードに対してのみ機能します。
-
提供されているとは、Jar パッケージがオンラインで提供されており、パッケージ化の際に考慮する必要はありませんが、一般にサーブレットなど多くのパッケージが提供されています。
-
システムと提供されているものに大きな違いはありません。
-
Import は、Maven の単一継承を解決するために、dependencyManagement タグ内の依存関係にのみ表示されます。このスコープが導入されると、Maven はこの依存関係の dependencyManagement 内のすべての要素を現在の pom にロードしますが、現在のノードは導入しません。以下の図に示すように、依存関係管理の要素として fastjson は導入されませんが、fastjson ファイルで定義された依存関係管理が導入されます。
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
<scope>import</scope>
</dependency>
<dependencies>
<dependencyManagement>
2. 単一の Pom ツリー内での依存関係の競合
ポムファイルエッセンス
Pom ファイルの本質はツリーです。
人間の観点から Pom ファイルを観察すると、それは線形の依存関係リストとして考えることになり、下図の Pom ファイルの抽象的な結果は、C が A、B、D に依存していると考えることになります。しかし、私たちの視点は不完全であり、Maven の視点から見ると、Maven はこの Pom ファイルを依存関係ツリーに直接抽象化します。Maven のパースペクティブでは、ABD 以外のノードが表示されます。ユーザーには 3 つのノード ABD のみが表示されます。
ツリー上にあるため、同じノード間で競合が発生するはずです。この競争関係が、私たちが仲裁メカニズムとして言及したものです。
Maven 調停メカニズムの原則
1. 競合に依存する場合、主幹に近いものが優先されます。
2. 単一のツリーが競合 (依存関係) に依存する場合 (注: dependencyManagement の依存関係ではありません):
deep=1 の場合、直接依存します。同レベルの方を優先させていただきます。
deep>1 の場合、間接的な依存関係を意味します。同レベルの方を優先させていただきます。
3. 依存関係管理が競合する場合、単一のツリーが優先されます (注: それは dependencyManagement の依存関係です)。
4. Maven の 2 つの最も重要な関係は、継承と依存関係です。私たちのすべての法律は、これら 2 つの関係からのみ開始されるべきです。
以下の図は、2 つのサブ pom ファイルをそれぞれ示しています (四角は依存ノードを表し、A-1 はノード A がバージョン 1 を使用することを意味し、文字はノードを表し、数字はバージョンを表します)。
左側のサブポムによって生成されたツリーは、D-1、D-2、および D-5 に依存します。これは依存関係の競合原理 1、つまりツリーの左側にあるほど優先度が高いという原則を満たしているため、D-5 は正常に競合します。
しかし、B-1 と B-2 は同時にツリーの同じ深さに位置し、深さは 1 です。B-2 はさらに後方にあるため、B-2 は正常に競合します。
右側のsub-pomで生成されたツリーはD-1とD-2に依存しており、同じ深さにありますが、D-1とD-2は間接依存の範囲に属しているため、深さは異なります。が 1 より大きい場合は、前が優先されるため、D-1 も正常に競合します。
一般的なシナリオ
これを見れば、Maven の仲裁原則を理解できたはずです。しかし、実際の仕事では原則があるだけで、それをコードに柔軟に適用して理解する必要があります。ここでは 5 つのシナリオを用意し、それぞれのシナリオに対する答えは巻末にあります。 Maven の原則を使用して推論し、期待を満たさないものがないかどうかを確認してみてください。
シナリオ1の難易度(※)
シーンの説明
メイン POM には <fastjson.version> があり、この属性は 1.2.24 です。
親は spring-boot-starter-parent-3.13.0 です。親の <fastjson.version> は 1.2.77 です。
そしてメインpomでは、この属性が消費されます。
それでは、メインの POM ツリーでは、最終的にどの fastjson を使用するのでしょうか?
シナリオ例
構造図
答え:
1.2.24 がついに発効します。
というのも、子は父親の属性を引き継ぐことになりますが、この属性を持っているので上書きされてしまいます!
継承には必ず上書きが伴いますが、この設計はプログラミング言語では比較的一般的です。
シナリオ 2 の難易度 (**)
Fastjson は同じメイン POM またはサブ POM の依存関係で使用されており、最初のものはバージョン 1.2.24 を宣言し、2 つ目はバージョン 1.2.25 を宣言します。それでは、メイン POM ツリーまたはサブ POM ツリーについては、最終的に fastjson 1.2.24 または 1.2.25 を選択しますか?
シナリオ例
構造図
答え:
1.2.25 がついに発効します。
依存関係が競合する場合は単一のツリーを参照します。deep=1 の場合、直接依存します。同レベルの方を優先させていただきます。
Maven の中核となる競争依存戦略をご紹介します。
シナリオ 3 難易度 (***)
下の左の図では、メイン POM ファイルの dependencyManagement 内の fastjson は 1.2.77 ですが、この時点で、子 POM には独自のバージョン 1.2.78 が表示されます。それでは、子POMツリーにとって、子POMは父親の命令に従うことを選択するのでしょうか、それとも父親の心に従うことを選択するのでしょうか?
シナリオ例
構造図
答え:
1.2.78 がついに発効します。
プロジェクト内のDependencyManagementは、バージョンを宣言しない依存関係および間接依存関係に対してのみ有効です。
シナリオ 4 難易度 (****)
主要な POM 依存関係Fastjson:1.2.24主要な POM 依存関係管理Fastjson:1.2.77
メイン POM の親 (スプリングブート) 依存関係Fastjson 1.2.78
子 POM Fastjson 1.2.25の依存関係
この場合、サブポンポンとして 4 つのバージョンのうちどれを選択しますか?
シナリオ例
構造図
答え:
1.2.25 がついに発効します。これはさらに複雑です。
〇:まず、父と子の相続関係から、1.2.24は1.2.78を上書きします。したがって、バージョン 78 は削除されます
1: プロジェクト内の dependencyManagement は、バージョンを宣言しない依存関係および間接依存関係に対してのみ有効であるため、
1.2.77 は 1.2.25 では動作しません。
2: 父と子の継承関係により、1.2.25 は 1.2.24 を上書きします。
ということで最終的には1.2.25が勝ちました!
シナリオ 5 難易度 (*****)
主要な POM 依存関係Fastjson:1.2.24主要な POM 依存関係管理Fastjson:1.2.77
メイン POM の親 (スプリングブート) 依存関係Fastjson 1.2.78
サブ POM の依存関係にはバージョンが書き込まれません。
サブ Pom の依存関係バージョンがデフォルトであることを除いて、シナリオ 5 とシナリオ 4 の間に全体的な違いはありません。
この場合、サブポンポンとして 3 つのバージョンのうちどれを選択しますか?
シナリオ例
構造図
答え:
1.2.77 がついに発効します。
〇:まず、父と子の相続関係から、1.2.24は1.2.78を上書きします。したがって、バージョン 78 は削除されます
1: プロジェクト内の dependencyManagement は未宣言のバージョンで動作できるため、サブ Pom のバージョンは 1.2.77 です。
2: 父と子の継承関係により、1.2.77 は 1.2.24 を上書きします。
ということで最終的には1.2.77が勝ちました!
3 つ以上の Pom ツリーをマージしてパッケージ化する
複数のツリー構築シーケンスの原則
現在のプロジェクトは通常、複数のモジュールによって管理されており、大量の pom ファイルが存在します。複数のツリーがある場合、各ツリーの出現順序は事前に計算されています。
この関数は、Reactor と呼ばれるデバイスによって Maven のソース コードに実装されます。主な作業の 1 つは、プロジェクトで複数のサブ Pom が最初に構築される順序を決定することです。この工場出荷時の順序は非常に重要です。マージおよびパッケージ化する場合、多くの場合、最終的に複数の POM の中で誰が勝つかを決定します。 。
リアクターの原理
複数のツリー (複数のサブポム) が構築される順序は、依存当事者が最初、依存当事者が最後という原則に基づいています。
プロジェクトでは、循環依存関係が存在しないことを確認する必要があります。
リアクトル原理図
以下の図に示すように、sub-pom1 は sub-pom2 と sub-pom3 の両方に依存しているため、sub-pom1 が最初に構築され、sub-pom3 には依存する人がいないため、最後に構築されます。
SpringBoot Fatjar パッケージ化戦略
SpringBoot は Fatjar にパッケージ化され、すべての依存関係は BOOT-INF/lib/ ディレクトリに配置されます。SpringBoot パッケージ化の場合、POM のビルドが遅くなるほど優先順位が高くなります。これは、SpringBoot パッケージ化プラグインは通常、最も依存性の低いモジュール (上の図の Pom3 など) に配置されるためです。(SpringBoot のパッケージング プラグインは、通常、ブートストラップ pom に配置されます。この名前は自分で選択でき、通常は最も依存関係が高いモジュールです。複数のモジュールで管理されている Springboot アプリケーションでは、ブートストラップは最も信頼度の低いモジュールであることがよくあります。 )
sub-pom3 は最終的に構築に参加し、SpringBoot パッケージ化プラグインは通常このモジュールを使用します。最終的に SpringBoot パッケージ製品に参入したのは、A-2、B-2、E-2、F-2、D-1 です。A-2 と B-2 は、他のいくつかの同一ノードよりもツリーの幹に近いためです。E-2やF-2も同様です。後ろの木は自然に幹に近いため、このルールでは後ろが優先されます。
Maven ソース コードでの 4 つの調停メカニズムの実装
解析に Maven バージョン 3.6.3 のソース コードを使用して、ソース コード レベルから調停メカニズムの正確性を積極的に証明できるように、Maven の依存関係処理のいくつかの原則を解析することを試みます。さらに、Maven のメカニズムが何であるかだけでなく、ソース コードから、Maven の一部のメカニズムがなぜこのようになっているかを確認することもできます。著者は、いかなるメカニズムも時代の進歩を保証できないと信じているため、上記のすべての仲裁メカニズムはいつか変更される可能性があると信じています。これらの結論は最も重要ではありませんが、これらの結論をどのように調査するかがより重要です。は重要!
Maven はどのように継承を実装し、子が同じ属性で親を上書きできるようにするのでしょうか?
Maven には 2 つの非常に重要な主要な行があります。1 つは依存関係、もう 1 つは継承です。Maven は次のようにソース コードで継承を実装します。readParent を使用して下の図の父親のモデルを取得すると、このループに陥ります。このサイクルから抜け出す唯一の方法は、父親に追いつけなくなるまでです。そして毎回取得したモデルデータをlinegaオブジェクトに入れます。以下の図の下部の AssemblyInheritance では、実際の継承と上書きを完了する目的で、オブジェクト linega を消費していることがわかります。
AssemblyInheritance で非常に興味深い現象が見つかります. Lingage は逆方向にトラバースされ、最後から 2 番目の要素から開始されます. これはまさに上記で述べた Maven 設計哲学の 1 つです。Maven は、世界中のすべての pom ファイルには Java の Object と同様の父親があると信じています。この哲学的扱いの簡単な論理を次に示します。
また、Maven は上から下にトラバースするため、同じ要素の子が親を上書きできることをより便利に認識できるようになります。これも、著者が考えるコーディング上の小さな工夫です。
ソースコードでの Reactor リアクターの実装
上で非常に重要な概念であるリアクターについても触れました。リアクターは、各サブ Pom がビルド順序をどのように決定するかを直接決定します。Maven のソース コードでは、getProjectsForMavenReactor 関数に実装されています。また、下の図から、Maven のリアクターは循環依存関係を解決できず、この例外を直接キャッチすることがわかります。
リアクター アルゴリズムの実際の実装は、ProjectSorter のコンストラクターの Dag を通じて実装されます。Dag (有向非巡回グラフ) と幅優先検索は、依存関係のシナリオを解決する良い方法です。
有向非巡回グラフでは、毎回入出力次数が 0 のノードが選択され、そのノードとその隣接エッジが削除され、上記の手順が繰り返されます。DAG 上のすべてのノードの依存関係の順序を効率的に計算できます。Maven もこのアイデアを使用しています。
このソース コードの観点から見ると、Maven が各サブポムの前に循環依存関係が出現しないようにする必要がある理由も説明できます。
同じ Pom ファイル内の依存関係の後に宣言された優先実装
依存関係を処理する場合、Maven は特別な処理を実行せず、Map メソッドを直接使用して上書きします。なぜこのようなデザインになっているのか分かりません。著者はかつて、この設計は開発学生がより良く書けるようにするためのものであると推測していました。なぜなら、最後の優先順位は多くの場合、ほとんどの人のコーディング習慣と一致するからです。しかし、ここで作成者からのコメント行が表示されます。これは、Maven2.x はファイルに同じ GA の一意の依存関係が 1 つだけあるかどうかを検証しないため、おそらくこの設計が Maven2.x との下位互換性を目的としていることを意味します。したがって、後続の Maven バージョンでもこのスタイルを継続する必要があります。
ループが 1.2.25 まで処理されると、依然として正規化されたマップに対して put 操作が実行されるため、キー値が同じ場合は上書きされます。
5 つのセキュリティの観点: 間接的な依存を回避する方法
分析する
著者はセキュリティの学生として、この種のマルチモジュール Maven プロジェクトについて、間接的な依存の問題を回避する方法についての経験を整理できることを望んでいます。
上記の分析の後、次の 3 つの結論を導き出すことができます。
1. 子 pom の宣言されたバージョンはセキュリティの観点から非常に危険であるため、子 pom は宣言されたバージョンを表示すべきではありません。
子pomはメインpomの要素を継承し、継承中に上書きシナリオが発生するためです。すると、CEやSpringBoot向けにパッケージ化する際、当然サブPOMビルドの発注順位が非常に有利になる可能性があり、最終パッケージ化された製品にサブPOM版が入り込みやすい。
2. メイン POM の dependencyManagent は、間接的な依存関係と、明示的にバージョンを宣言しない直接的な依存関係を制御できます。
3. 危険なバージョンはメイン POM の依存関係に表示されません。そうしないと、子 pom はこの危険なバージョンを自然に継承し、パッケージ化に参加することになります。
結論は
上記の条件が同時に満たされれば、間接依存の問題は解決できます。
今すぐ:
SpringBoot の場合、子 pom は宣言されたバージョンを表示すべきではなく、メイン pom の dependencyManagent は安全なバージョンの依存関係を制御する必要があり、メイン pom には危険なバージョンがあってはなりません。(依存する父親に安全でない依存関係が残らないように、メインの Pom 依存関係に安全なバージョンを強制的に作成することをお勧めします)
最後に6つ
Maven ソースコードのアドレス
https://archive.apache.org/dist/maven/maven-3/
どうやって分析したのか
SpringBoot をローカルで複数回テストしました。ルートディレクトリで mvn clean package を実行するだけです。
mvn clean org.apache.maven.plugins:maven-dependency-plugin:3.3.0:tree -Dverbose=true は、特定のノードの分析に役立ちます。
もう 1 つは、ソース コードの中でこの実装を見つけて理解を深めてみることです。
よく使用される分析コマンド
0. mvn clean package -DSkipTest は結果を直接パッケージ化して分析します
1. mvn dependency:tree は Maven ツリー構造全体を出力します。
2.mvn help:popular-pom -Dverbose このコマンドは、より完全な情報を出力します。出力は、effectpom です。
3.mvn clean org.apache.maven.plugins:maven-dependency-plugin:3.3.0:tree -Dverbose=true
4.mvn -D maven.repo.local = ディレクトリのコンパイル段階で使用される依存関係。