Read OSS

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 行目で適用されており、複数のビルドファイルを同時に開いていても、どのモジュールを編集しているかがすぐにわかります。

settings.gradle#L33-L36

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 行目でサブプロジェクトを moduleProjectsspring- で始まるもの)と javaProjectsframework-* ユーティリティプロジェクトを除く全プロジェクト)の 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-webmvcspring-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.asmorg.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 を参照します。

build.gradle#L39-L51

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

コードベースを読み進めるための実践的なヒント

ビルド構成を理解したところで、ソースコードを効率よく読み進めるためのヒントをいくつか紹介します。

  1. 実装ではなくインターフェースから読み始める。 Spring のインターフェースには充実した Javadoc が添えられています。BeanFactory.java を読むだけで、どんな実装クラスを読むよりも設計の本質が見えてきます。

  2. extends の連鎖をたどる。 Spring の実装階層は深く掘り下げられていますが、それには理由があります。各クラスが特定の責務レイヤーを担っており、「そのクラスが何をするか」より「なぜそのクラスが存在するか」を理解することが重要です。

  3. モジュール境界を活用する。 Web まわりの問題を調査しているなら、spring-webspring-webmvcspring-webflux のいずれかにいるはずです。DI の問題なら spring-beans です。モジュール構成はデバッグの最良の道案内になります。

  4. まず .gradle ファイルを確認する。 各モジュールの ${module-name}.gradle ファイルを見れば依存関係がわかり、自分がどの抽象レイヤーにいるか、どの外部 API が利用可能かがすぐに把握できます。

ヒント: Spring のソースコードを読む際、Gradle ファイルの optional() 依存宣言は「ユーザーがそのライブラリを提供した場合にのみ有効になる機能」を意味します。これは Spring Boot のオートコンフィギュレーションと対応しており、ライブラリがクラスパスに存在すれば、その機能が自動的に有効化されます。

次のステップ

モジュール全体の地図が手に入ったので、次はいよいよ最も重要なモジュール spring-beans に踏み込みます。次の記事では、BeanFactory インターフェース階層を、シンプルな getBean() を起点として 7 つのサブインターフェースをたどり、2,800 行に及ぶ DefaultListableBeanFactory の実装まで追いかけます。そして、生のクラスから完全に初期化・依存注入済みのシングルトンが生成されるまでの Bean 生成パイプライン全体を歩いていきましょう。