Spring Framework モノレポを読み解く:アーキテクチャ、モジュール、ビルドシステム
前提知識
- ›Java と Gradle ビルドシステムの基礎知識
- ›Spring Framework のユーザーとしての基本的な利用経験
Spring Framework モノレポを読み解く:アーキテクチャ、モジュール、ビルドシステム
Spring Framework は 2002 年から継続的に開発されており、Java のオープンソースプロジェクトのなかでも最長クラスの歴史を誇ります。現在のバージョン 7.x では、JDK 25 ツールチェーンでビルドしながら Java 17 バイトコードをターゲットとする Gradle モノレポに、22 のモジュールが収められています。IoC コンテナや AOP、Web レイヤーの仕組みを理解する前に、まずコードベース自体がどのように構成されているかを把握する必要があります。本記事では、その全体像を整理します。
リポジトリの全体像
リポジトリのルートは、一見するとシンプルな構成です。settings.gradle で全モジュールを宣言し、build.gradle で共通の規約を設定しています。各モジュールのビルドファイルは、標準の build.gradle ではなく ${project.name}.gradle という名前になっています。この命名の工夫は settings.gradle の 34〜36 行目で適用されており、複数のビルドファイルを同時に開いていても、どのモジュールを編集しているかがすぐにわかります。
rootProject.name = "spring"
rootProject.children.each { project ->
project.buildFileName = "${project.name}.gradle"
}
| ディレクトリ | 役割 |
|---|---|
spring-core |
基盤となるユーティリティ、型システム、リソース読み込み、ASM/CGLIB/JavaPoet の組み込み |
spring-beans |
IoC コンテナ:BeanFactory、BeanDefinition、依存性注入 |
spring-aop |
AOP アライアンスモデル、プロキシ基盤 |
spring-expression |
SpEL(Spring Expression Language) |
spring-context |
ApplicationContext、アノテーション設定、イベントシステム、スケジューリング |
spring-tx |
トランザクション抽象化、PlatformTransactionManager |
spring-jdbc, spring-r2dbc |
JDBC/R2DBC データアクセステンプレート |
spring-orm |
JPA/Hibernate 統合 |
spring-web |
MVC と WebFlux で共有される HTTP 抽象化 |
spring-webmvc |
Servlet ベースの MVC(DispatcherServlet) |
spring-webflux |
リアクティブ Web(DispatcherHandler、Reactor ベース) |
spring-websocket |
WebSocket サポート |
spring-messaging |
メッセージング抽象化(STOMP など) |
spring-jms |
JMS 統合 |
spring-test |
テストユーティリティ(MockMvc、TestContext) |
spring-aspects |
AspectJ 統合 |
spring-instrument |
クラスインストルメンテーション用 JVM エージェント |
spring-context-indexer |
ビルド時アノテーションインデクサー |
spring-context-support |
キャッシュマネージャー、メール、FreeMarker |
spring-core-test |
spring-core 用テストフィクスチャ |
spring-oxm |
オブジェクト/XML マーシャリング |
ヒント: ルートの
build.gradleでは、13〜14 行目でサブプロジェクトをmoduleProjects(spring-で始まるもの)とjavaProjects(framework-*ユーティリティプロジェクトを除く全プロジェクト)の 2 つに分類しています。これらのセットはビルド全体でプラグインを選択的に適用するために使われており、コードを読み進めるときの重要な手がかりになります。
22 モジュールのレイヤー化された依存グラフ
Spring のモジュール群は、厳格なレイヤードアーキテクチャを形成しています。依存関係は上位モジュールから基盤モジュールへと一方向にのみ流れ、循環依存は一切許されません(この制約は ArchUnit によって強制されています。詳細は後述します)。
flowchart TD
subgraph "Layer 1: Foundation"
core["spring-core"]
end
subgraph "Layer 2: IoC"
beans["spring-beans"]
end
subgraph "Layer 3: AOP & Expression"
aop["spring-aop"]
expression["spring-expression"]
end
subgraph "Layer 4: Application Context"
context["spring-context"]
ctxSupport["spring-context-support"]
end
subgraph "Layer 5: Data Access"
tx["spring-tx"]
jdbc["spring-jdbc"]
orm["spring-orm"]
r2dbc["spring-r2dbc"]
end
subgraph "Layer 6: Web Foundation"
web["spring-web"]
end
subgraph "Layer 7: Web Frameworks"
webmvc["spring-webmvc"]
webflux["spring-webflux"]
websocket["spring-websocket"]
end
beans --> core
aop --> beans
aop --> core
expression --> core
context --> aop
context --> beans
context --> expression
context --> core
ctxSupport --> context
tx --> beans
tx --> core
jdbc --> tx
jdbc --> beans
orm --> jdbc
orm --> tx
r2dbc --> tx
r2dbc --> core
web --> beans
web --> core
webmvc --> web
webmvc --> aop
webmvc --> context
webmvc --> expression
webflux --> web
webflux --> beans
webflux --> core
websocket --> web
websocket --> context
ここで重要なのは、spring-web が HTTP 抽象化の共有レイヤーとして機能している点です。spring-webmvc と spring-webflux はどちらも spring-web に依存していますが、互いには依存していません。MVC モジュールは Servlet API を、WebFlux モジュールは Reactor を利用します。この明確な分離により、Spring は 2 つのプログラミングモデルを相互干渉なくサポートできています。
カスタム Gradle ビルドシステム:buildSrc プラグイン
buildSrc ディレクトリには、22 モジュール全体で一貫したビルド規約を強制するカスタム Gradle プラグインが格納されています。エントリーポイントは ConventionsPlugin です。
buildSrc/src/main/java/org/springframework/build/ConventionsPlugin.java#L38-L50
このプラグインはルートの build.gradle を通じてすべての Java プロジェクトに適用されます。
apply plugin: 'org.springframework.build.conventions'
このプラグインは、それぞれ特定の関心事を担う 5 つの Convention クラスを取りまとめます。
flowchart LR
CP[ConventionsPlugin] --> AP[ArchitecturePlugin]
CP --> CC[CheckstyleConventions]
CP --> JC[JavaConventions]
CP --> KC[KotlinConventions]
CP --> TC[TestConventions]
コンパイル戦略を定義しているのが JavaConventions です。--release フラグを使って Java 17 バイトコードをターゲットとしつつ、JDK 25 ツールチェーンでのビルドを設定しています。これにより、Spring は Multi-Release JAR のスライスで JDK 25 のコンパイラ改善や API を活用しながら、Java 17 を最低限の実行時要件として維持できます。
buildSrc/src/main/java/org/springframework/build/JavaConventions.java#L48-L54
67 行目のコンパイラ引数にある -Werror フラグにも注目してください。プロダクションコードでは警告をエラーとして扱いますが、テストコードではその制約を緩和しています。問題を早期に発見するためのこだわりの設計ですが、テストコードに同じ厳しさを求めないバランス感覚も備えています。
ArchUnit によるアーキテクチャの強制
Spring はモジュールの健全性を期待するだけでなく、ビルド時に ArchUnit を使ってアーキテクチャルールを強制しています。ArchitectureRules クラスは、すべてのモジュールが満たすべき構造上の制約を定義しています。
buildSrc/src/main/java/org/springframework/build/architecture/ArchitectureRules.java#L28-L100
ルールは大きく 3 つのカテゴリに分かれています。
flowchart TD
AR[ArchitectureRules] --> PT[Package Tangle Detection]
AR --> FA[Forbidden API Calls]
AR --> FT[Forbidden Type Dependencies]
PT -->|"SlicesRuleDefinition"| NoCycles["No package cycles allowed"]
FA --> NoLower["No String.toLowerCase without Locale"]
FA --> NoUpper["No String.toUpperCase without Locale"]
FT --> NoSLF4J["No SLF4J LoggerFactory"]
FT --> NoSpringNullable["No org.springframework.lang.Nullable"]
FT --> NoSpringNonNull["No org.springframework.lang.NonNull"]
禁止型リストは、現在進行中のマイグレーションの状況を反映しています。Spring は org.springframework.lang 配下にある独自の @Nullable/@NonNull アノテーションから JSpecify アノテーションへの移行を進めています。56〜58 行目の ArchUnit ルールが旧アノテーションのインポートを禁止し、移行を強制しています。同様に org.slf4j.LoggerFactory が禁止されているのは、Spring がロギングのファサードとして Apache Commons Logging を採用しているためです。ユーザーに特定のロギングフレームワークを強制しない意図的な設計です。
内部クラス SpringSlices(76〜99 行目)も見落とせません。ここでは、Spring がコントロールできないリロケーション済みのサードパーティコード(org.springframework.asm、org.springframework.cglib など)を、循環検出の対象から明示的に除外しています。
ヒント: 大規模なモノレポを構築する際、ArchUnit ルールをテストクラスではなくビルドプラグイン内に直接組み込む Spring のアプローチは、コピー&ペーストなしにルールをすべてのモジュールで一貫して適用できるという大きな利点があります。
Shadow/Repack と Multi-Release JAR 戦略
spring-core のビルド設定で最も特徴的なのが、4 つのサードパーティライブラリを Spring の名前空間のもとで直接 JAR に組み込む(ベンダリングする)仕組みです。これにより、アプリケーションが同じライブラリの異なるバージョンを使用していてもクラスパスの競合を防げます。
spring-core/spring-core.gradle#L30-L35
repack 戦略では Shadow Gradle プラグインを使ってパッケージ名前空間を移動します。
| 元のパッケージ | 移動後のパッケージ | ライブラリ |
|---|---|---|
com.palantir.javapoet |
org.springframework.javapoet |
JavaPoet(コード生成) |
org.objenesis |
org.springframework.objenesis |
Objenesis(オブジェクトのインスタンス化) |
| ASM(ソースに組み込み済み) | org.springframework.asm |
ASM(バイトコード操作) |
| CGLIB(ソースに組み込み済み) | org.springframework.cglib |
CGLIB(サブクラス生成) |
flowchart LR
subgraph "Third-Party JARs"
JP["javapoet-0.10.0.jar"]
OB["objenesis-3.5.jar"]
end
subgraph "ShadowJar Tasks"
JPRJ["javapoetRepackJar"]
OBRJ["objenesisRepackJar"]
end
subgraph "spring-core.jar"
SJP["org.springframework.javapoet.*"]
SOB["org.springframework.objenesis.*"]
SASM["org.springframework.asm.*"]
SCGLIB["org.springframework.cglib.*"]
end
JP --> JPRJ --> SJP
OB --> OBRJ --> SOB
ASM と CGLIB は Shadow による repack ではなく、spring-core のソースツリーにリロケーション済みのソースコードとして直接管理されています。これにより、Spring チームがパッチや最適化を自由に加えられる完全な制御を維持しています。
MultiReleaseJarPlugin は Java バージョンごとの最適化を可能にします。spring-core モジュールでは Java 21 と 24 向けの Multi-Release サポートを宣言しています。
spring-core/spring-core.gradle#L13-L15
multiRelease {
releaseVersions 21, 24
}
つまり spring-core は、標準の Java 17 コードベースに加えて、最適化されたクラスバリアントを META-INF/versions/21/ および META-INF/versions/24/ 配下に収めた JAR を提供します。たとえば JDK 24 上で実行する場合、JVM はそれらのクラスの最適化バージョンを自動的に選択します。
Framework Platform BOM と依存関係の管理
22 モジュールにわたって多数のサードパーティ依存関係を抱えると、バージョン管理はすぐに混乱しがちです。Spring はこの問題を、framework-platform.gradle で定義した中央集権的な BOM(Bill of Materials)で解決しています。
framework-platform/framework-platform.gradle#L1-L25
このファイルは Gradle の java-platform プラグインを使い、Jackson 2.21.2 から JUnit 6.0.3、Reactor BOM 2025.0.4 まで、すべてのサードパーティ依存バージョンを固定しています。全モジュールはルートビルドの enforcedPlatform() を通じてこの platform を参照します。
flowchart TD
BOM["framework-platform.gradle<br/>(java-platform)"]
ROOT["build.gradle<br/>enforcedPlatform()"]
MOD1["spring-core"]
MOD2["spring-beans"]
MOD3["spring-web"]
MODN["... all other modules"]
BOM --> ROOT
ROOT --> MOD1
ROOT --> MOD2
ROOT --> MOD3
ROOT --> MODN
enforcedPlatform() の呼び出しがポイントです。通常の platform() と異なり、推移的依存のバージョンを強制的に上書きします。あるライブラリが推移的に Jackson 2.18 を引き込んだとしても、enforcedPlatform() によって 2.21.2 が強制されます。これがフレームワーク全体でのバージョン一貫性を保証する仕組みです。
spring-module.gradle 共有スクリプトは、ルートビルドの 88〜90 行目で全 spring-* モジュールへ適用されます。java-library プラグイン、JMH ベンチマークサポート、JSpecify 検証用の io.spring.nullability プラグイン、標準の publish 設定を追加します。
gradle/spring-module.gradle#L1-L9
コードベースを読み進めるための実践的なヒント
ビルド構成を理解したところで、ソースコードを効率よく読み進めるためのヒントをいくつか紹介します。
-
実装ではなくインターフェースから読み始める。 Spring のインターフェースには充実した Javadoc が添えられています。
BeanFactory.javaを読むだけで、どんな実装クラスを読むよりも設計の本質が見えてきます。 -
extendsの連鎖をたどる。 Spring の実装階層は深く掘り下げられていますが、それには理由があります。各クラスが特定の責務レイヤーを担っており、「そのクラスが何をするか」より「なぜそのクラスが存在するか」を理解することが重要です。 -
モジュール境界を活用する。 Web まわりの問題を調査しているなら、
spring-web、spring-webmvc、spring-webfluxのいずれかにいるはずです。DI の問題ならspring-beansです。モジュール構成はデバッグの最良の道案内になります。 -
まず
.gradleファイルを確認する。 各モジュールの${module-name}.gradleファイルを見れば依存関係がわかり、自分がどの抽象レイヤーにいるか、どの外部 API が利用可能かがすぐに把握できます。
ヒント: Spring のソースコードを読む際、Gradle ファイルの
optional()依存宣言は「ユーザーがそのライブラリを提供した場合にのみ有効になる機能」を意味します。これは Spring Boot のオートコンフィギュレーションと対応しており、ライブラリがクラスパスに存在すれば、その機能が自動的に有効化されます。
次のステップ
モジュール全体の地図が手に入ったので、次はいよいよ最も重要なモジュール spring-beans に踏み込みます。次の記事では、BeanFactory インターフェース階層を、シンプルな getBean() を起点として 7 つのサブインターフェースをたどり、2,800 行に及ぶ DefaultListableBeanFactory の実装まで追いかけます。そして、生のクラスから完全に初期化・依存注入済みのシングルトンが生成されるまでの Bean 生成パイプライン全体を歩いていきましょう。