← コース一覧
Spring
Java のサーバーサイドフレームワーク Spring と Spring Boot を、使い方ではなく仕組みから学ぶコースです。最初に JVM・JAR・クラスパス・Bean などの用語をそろえ、次にプロジェクトの構成、Ant・Maven・Gradle の設定ファイルの読み方と依存の解決、成果物の単位、ant や Maven が何をしているか、パスが深くなる理由という地図を渡し、そのあとで Java のソースが JVM で動くまで、Servlet と EJB からの歴史、DI コンテナとプロキシ、自動構成、リクエストとトランザクションの通り道、そして最終成果物の JAR とビルド時間の内訳までを、実機で確かめた出力と一緒に1テーマずつ追います。NestJS や Rust と何が違うのかも、成果物と実行時の仕組みの違いとして見ていきます。
公式ドキュメント: Spring Boot Reference Documentation ↗用語集(434語)用語をそろえる
- 1実行可Java・JDK・JRE・JVM: 何を入れて、何が動くのかJVM(Java Virtual Machine)は、Java のプログラムを動かすプログラムです。java コマンドを実行すると動き出します。読むのは人が書いたソースではなく、javac というコンパイラで先に変換しておいた .class というファイルです。Node で近いのは JavaScript のエンジンの V8 で、Node.js そのものではありません。このセクションでは、後のレッスンに出てくる言葉を先にそろえます。
- 2実行可.java・.class・バイトコード・javac.class は、javac(Java のコンパイラ)がソースの .java から作るファイルで、JVM が読むのはこのファイルです。中身は JVM 向けの命令列で、バイトコードと呼びます。TypeScript で言うと、javac が tsc、.class が tsc の出す .js に近い位置にあります。ただし .js は人が読めるソースですが、.class は人が読む形ではないバイナリです。
- 3実行可コマンド行の読み方: $ とコマンドとオプションと引数コマンド行は、道具の名前と、その道具への指示を1行に並べたものです。このコースの図やサンプルには $ java -cp out app.Main のような行が何度も出ます。左から、$(ここからがコマンドだという印で、自分では打ちません)、コマンド(道具の名前)、オプション(- や -- で始まる指示)、引数(対象)の順に読みます。npm run build や node dist/main.js と同じ読み方です。
- 4実行可java・javac・jar・javap: JDK のコマンドjavac は、ソースの .java から .class を作るコンパイラです。java は、JVM を起動して .class を動かすコマンドです。jar は、.class をまとめて JAR というファイルを作り、その中を見るコマンドです。javap は、.class の中身を人が読める形で表示するコマンドです。4つとも JDK に入っています。アプリを作って動かすのに使うのは前の3つで、javap は中を確かめるための道具です。
- 5実行可JAR・WAR・マニフェスト・実行可能 JARJAR(Java Archive、拡張子 .jar)は、.class や設定ファイルを1つにまとめた zip 形式のファイルです。ライブラリを配る単位であり、アプリを配る単位でもあります。Node で近いのは、npm パッケージを1つにまとめたファイルです。ただし npm は node_modules に展開してから使いますが、JAR は展開せず、JVM がそのまま中を読みます。入っているのもコンパイル済みの .class です。
- 6実行可パッケージ・完全修飾名・import・クラスパス・クラスローダパッケージは、クラスをまとめる名前空間です。ソースの先頭に package com.example.demo; のように宣言します。パッケージ名とクラス名を . でつないだ com.example.demo.Hello が完全修飾名で、Java のコードは他のクラスを、場所ではなくこの名前で指します。Node で近いものを挙げるならフォルダとモジュールのパスですが、相対パスという考え方が無いので、対応は弱いです。
- 7実行可Ant・Maven・Gradle・pom.xml・依存・Maven Centralビルドツールは、ソースをコンパイルして JAR や WAR を作るまでの作業を、コマンド1つで順に実行する道具です。このコースに出てくるのは Ant、Maven、Gradle の3つです。Node で役割が近いのは npm と、package.json の scripts に書いた tsc や nest build です。ただし npm は Node に付いてきますが、Maven や Gradle は JDK に入っていません。
- 8実行可mvn・./mvnw・ant・./gradlew: ビルドのコマンドmvn は Maven を実行するコマンドで、ant は Ant を実行するコマンドです。./mvnw と ./gradlew は、プロジェクトに付いているスクリプト(ラッパー)で、決まった版の Maven や Gradle を取ってきて実行します。Node で言うと、./mvnw package は nest build に近い位置ですが、テストの実行と JAR 作りまで1回で進みます。ラッパーに近いのは Corepack です。
- 9実行可Servlet・Tomcat・Servlet コンテナ・Jakarta EEServlet(サーブレット)は、HTTP の要求を受けて応答を返す Java のクラスの、決まった形です。要求と応答の2つを受け取って処理する点は、Express のハンドラに近いものです。ただしハンドラは関数で、Servlet はクラスです。作られたインスタンス1つを、複数のスレッド(プログラムの中で同時に進む処理の流れ)が同時に呼ぶことがあります。
- 10実行可Spring・Spring Framework・Spring Boot・Bean・DI コンテナSpring Framework は、Java のアプリを組み立てる土台のフレームワークです。単に Spring と言うときは、これを土台にしたプロジェクト群の総称であることが多いです。役割が近いのは NestJS で、部品を作って互いに渡す DI コンテナと、Web などの機能を持ちます。HTTP の受け付けを下の層に任せる点も同じで、NestJS では既定で Express、Spring MVC では Servlet コンテナです。
- 11実行可JDBC・JPA・Hibernate と、略語の早見表JDBC は、Java から SQL でデータベースを使うための標準の API で、JDK に入っています。Node で言えば、pg や mysql2 のようなドライバを直接使う層に近いものです。違うのは、JDBC が決めているのは共通の API だけで、個々のデータベースと実際に話すドライバは別の JAR として足す点です。接続を取り出す窓口は、DataSource というインターフェースで表されます。
全体像と構成
- 12ソースから起動まで: 全体の流れSpring Boot の雛形をそのままビルドすると、成果物は JAR というファイル1個になり、java -jar の1行で起動します。JAR は、コンパイル結果をまとめた zip 形式のファイルです。このレッスンでは、ソースから HTTP の応答が返るまでを1枚の図で通します。このセクションは先に全体の地図を渡し、個々の部品の細部は後のレッスンで扱います。
- 13実行可JVM は何を読んで実行するのかJVM が読むのは .class というファイルです。クラス1つにつき1ファイルで、中身は javac が先に作っておいたバイトコード(JVM 向けの命令列)です。OS が起動するのは java コマンドだけで、.class も JAR も実行ファイルではなく、java が開いて読む入力です。手元の Linux で、開かれたファイルを順に記録したものが図です。
- 14実行可成果物の単位: .class・JAR・WAR・実行可能 JARJava の成果物は、小さい順に .class、JAR、そして WAR か実行可能 JAR という単位で作られます。最小の .class は、クラスやインターフェースなどの型1つにつき1ファイルで、ソース1ファイルから複数できることもあります。JAR は .class をフォルダの階層ごと zip にまとめたもので、ライブラリを配る単位でもあります。ただし jar コマンドで普通に作る JAR には、依存ライブラリは入りません。
- 15ツール無しで手でビルドするビルドツールが無くても、JDK に入っている3つのコマンドでビルドできます。javac -d out でソースを .class にし、jar で JAR にまとめ、java -jar で起動します。javac は、ソース先頭の package 宣言(クラスが属する名前空間の名前。後のレッスンで扱います)どおりのフォルダを out の下に作ります。jar は、入口のクラス名を JAR の中の設定ファイル(マニフェスト)に書き込みます。
- 16ant を実行すると何が起きるかant と打つと、ant という起動用のスクリプトが java を起動し、その JVM の中で Ant 本体(Java で書かれたプログラム)が動きます。Ant はカレントディレクトリの build.xml を読み、そこに書かれた手順を順に実行します。手順のまとまりを target と呼びます。レッスン「ソースから起動まで」に出てきた Maven の target/ フォルダとは無関係の、同じ綴りの別の言葉です。
- 17Ant で Spring の WAR を作るSpring Boot を使わず、Ant で Spring の Web アプリをビルドすると、この回の例では成果物は WAR 1個になります。ルートには build.xml、ソースの src、依存の JAR を人が置く lib、Web 用のファイルを置く WebContent を並べました。ant を実行すると、コンパイルの後、war という task が .class と lib の JAR と設定ファイルを WAR に詰めます。
- 18実行可Maven は何を変えたかMaven が Ant から主に変えたのは3つです。ソースや出力の置き場所を規約で固定したこと、ビルドの手順と順序を決まったものにしたこと、依存ライブラリを座標(グループ名・名前・版の3つ組)で宣言すれば取ってくるようにしたことです。設定ファイルの pom.xml には場所も手順も書かないので、build.xml を読まなくても構成が分かります。
- 19最小プロジェクトのファイル一覧start.spring.io が作る雛形で、自分が書き足していくファイルは3つです。依存とビルドを宣言する pom.xml、起動の入口になる DemoApplication.java、設定を書く application.properties です。残りは、Maven を取ってくるラッパー(mvnw)、テストの雛形、Git 用の設定、説明書きで、Maven 版は全部で10ファイルでした。
- 20実行可パスが深い理由: 15階層を分解するフォルダに責務が無いように見えるのは、正しい見方です。深いパスの大半は設計上の区切りではなく、決めた人の違う取り決めが、1本につながったものです。区切りが15個(ディレクトリ14段とファイル名)のパスを実際に作って分けると、リポジトリとモジュール(pom.xml を1つずつ持つビルドの単位)の区分が3段、ビルドツールの規約が3段、パッケージ名を写した部分が8段でした。Spring が決めた段は1つもありません。
- 21実行可パッケージはフォルダではなく名前Java としては src/service でも動きます。package service; と書いて src/service に置いたクラスは、javac でも Maven でもビルドと実行ができました。途中の jp/co/example は必須ではなく、言語仕様が広く配布するパッケージ向けに勧める慣習です。ただし Spring Boot は起動クラスのパッケージの配下しか探さないので、全部の上に共通の根のパッケージが1つ要ります。
- 22実行可この設計の代償と、後の言語がしたことこの不便さは Spring が持ち込んだものではなく、1996年の最初の Java 言語仕様に既にある決め方から来ています。逆さにしたドメイン名をパッケージ名の先頭に付ける慣習と、その名前をディレクトリに写す置き方です。後者は仕様が「ごく単純な例」として示し、JDK のコンパイラと標準のクラス読み込みがそのとおりに実装しました。ドメイン名を使う理由は、仕様によれば「既にある一意な名前の登録簿に相乗りし、別の登録簿を作らない」ためです。
- 23Spring が決めていること、決めていないことアプリのソースの置き方について、Spring Boot が既定で決めているのは2つだけです。起動クラス(main を持ち、@SpringBootApplication を付けたクラス)のパッケージとその配下から部品を探すことと、application.properties や static などリソースの既定の置き場所です。controller や service というフォルダ名は見ず、アノテーション(クラスに付ける印)で見分けます。
- 24既存プロジェクトの読み方知らない Java のプロジェクトを開いたら、ソースより先に5つを順に確かめます。ビルドファイルは何か、成果物は何か、起動の入口はどこか、設定はどこか、自分たちのコードの根はどこか、です。ルートに build.xml があれば Ant、pom.xml なら Maven、build.gradle なら Gradle です。これが分かると、ソースと出力の場所の見当がつきます。
ビルドツールを読む
- 25実行可ビルドとは何か: 手作業なら何をするかビルドとは、ソースから動く形(JAR や WAR)を作るまでの手作業の列のことで、ビルドツールはその手作業を代わりにやる道具です。手作業は5段あります。javac でコンパイルする、.properties などのリソースをコピーする、テストをコンパイルして実行する、jar で JAR にまとめる、依存の JAR をクラスパスに並べる、の5つです。どの段も、あるファイル群を読んで別のファイル群を書く操作です。
- 26Ant: build.xml を1行ずつ読むbuild.xml は、project の default、その target の depends、target の中の task、property の値、の順に読みます。project はファイルに1つで、default は引数なしの ant で動く target、basedir はパス計算の基準(省略時は build.xml の場所)です。target は task の集まりで、depends に先に行う target を書きます。
- 27実行可Ant: 依存の扱いと、古いプロジェクトの読み方Ant 本体は依存の JAR を取ってきません。このコースで扱うのは、lib/ に人が置く方式と、Ivy という道具に取らせる方式の2つです。手で置く場合、Maven Central の URL は座標から決まるので、そこから取って lib/ に置き、<fileset dir="lib" includes="*.jar"/> で lib/ 直下の JAR 全部をクラスパスにします。推移依存(使うライブラリが使うもの)も人が置きます。
- 28実行可Maven: pom.xml を1行ずつ読むこの pom.xml の54行は、親の指定、自分の座標、properties、依存、プラグインの宣言と、名前や説明の欄(14行は空のタグ)でできています。modelVersion は書式の版で、Maven 3 系(ここでは 3.9.16)では 4.0.0 だけが使えます。parent は親 POM の座標で、relativePath が空なので親フォルダの pom.xml を探さずリポジトリから取ります。
- 29実行可Maven: ライフサイクル・フェーズ・ゴール・プラグインの関係mvn package で動くのは package という作業ではなく、validate から package までのフェーズに割り当てられたゴールです。ライフサイクルは default・clean・site の3つで、default には validate、compile、test、package、install、deploy を含む23のフェーズが並びます。jar のプロジェクトでは、そのうち8つにだけゴールが割り当てられています。
- 30実行可Maven: 依存解決(推移依存・nearest wins・dependencyManagement・exclusions・スコープ)pom.xml に2本しか書いていないのに72本の依存が並ぶのは、依存の POM に書かれた依存を Maven が自動でたどるからです(推移依存)。Boot 雛形の dependency:tree は深さが最大5でした。同じライブラリの別の版に2つの経路で出会うと、根に近い方が選ばれます。古い 3.12.0 を直接書き、別の依存が 3.20.0 を要求する最小の pom では、直接書いた 3.12.0 が勝ちました。
- 31実行可Gradle: build.gradle を1行ずつ読むbuild.gradle は、pom.xml と同じことを XML ではなくスクリプトで書いたファイルです。雛形は28行で、上から plugins(使う部品)、group と version(座標)、java の toolchain(使う Java の版)、repositories(取得元)、dependencies(依存)、test の設定の6つです。場所は Maven の規約を借り、手順はプラグインが足すので、どちらも書きません。
- 32実行可Gradle: タスクの依存関係と、変更が無ければ飛ばす仕組み./gradlew build と打つと、Gradle は実行の前にタスクの依存関係から実行順を決めます。--dry-run を付けると実行せず順だけが出て、雛形では compileJava から build まで13行でした。Maven のフェーズのような固定の列はなく、「bootJar は classes の出力を使う」のような依存関係をたどった結果が順番になります。build のように、他を束ねるだけのタスクもあります。
- 33実行可3つの比較と Spring Boot との関係同じ Boot のアプリを3つの道具でビルドすると、どれも JarLauncher を Main-Class に持つ JAR ですが、中身は同じではありません。Maven(repackage)と Gradle(bootJar)の BOOT-INF/lib は34本で名前も版も一致しました。Ant+Ivy+antlib では38本で、クラスの無い starter 5本が入り、3本の版が BOM の上書き前のままでした。
Javaが動く仕組み
- 34実行可ソースが動くまで: javac と .class と JVMここからは、前のセクションの地図に出てきた部品を1つずつ掘り下げます。出発点は Java そのものです。Java のソース(Hello.java)は javac というコンパイラで Hello.class に変換され、java コマンドが起動する JVM(Java 仮想マシン)がそれを読み込んで実行します。
- 35実行可JVM の起動とクラスロード、JITjava Hello を実行すると、JVM は必要なクラスを読み込みます(クラスロード)。JDK 25 でログを出すと 609 行あり、自分の Hello は 401 行目でした。その前の 400 個は String など JDK 自身のクラスです。Hello の後にも、文字列の連結に使う StringConcatFactory などが、使う段になってから読み込まれていました。
- 36実行可クラスパスと JARJVM は、クラスが必要になるとクラスパス(探しに行く場所の一覧)から .class を探します。場所に指定するのはディレクトリか JAR です。JAR は .class をまとめた zip で、先頭のバイトも zip と同じです。中の META-INF/MANIFEST.MF に入口のクラス名(Main-Class)を書いておくと、java -jar で起動できます。
- 37実行可アノテーションとリフレクション@ で始まるアノテーションは、クラスやメソッドに情報を貼り付ける目印です。それ自体は何も実行しません。実行時にクラスの中身を調べる仕組み(リフレクション)を使い、目印の付いたメソッドを探して呼ぶコードを誰かが書いて、初めて意味が生まれます。下のコードでは @RunMe(times = 3) を付けても、直接呼べば1回しか動きません。
- 38実行可動的プロキシ動的プロキシは、指定したインターフェース(持つべきメソッドだけを書いた型)を実装するクラスを、実行時に作る JDK 標準の仕組みです。そのインスタンスに対するインターフェースのメソッドの呼び出しは、toString、equals、hashCode と合わせて、ハンドラという1つの関数に届きます。ハンドラの中で本物のメソッドを呼べば、その前後にログ出力などの処理を挟めます。呼び出す側のコードは変わりません。
歴史的背景
- 39Servlet・WAR・アプリケーションサーバServlet(サーブレット)は、HTTP リクエストに応答する Java のクラスです。HttpServlet を継承して doGet を書きますが、main はありません。.class と対応表の web.xml を WAR という1つのファイルにまとめ、Tomcat などのサーブレットコンテナに置くと、コンテナが読み込みます。コンテナが呼ぶのは service メソッドで、GET ならそこから doGet が呼ばれます。
- 40EJB が要求したものと Spring の登場EJB(Enterprise JavaBeans)は、J2EE でサーバ側の業務部品を書くための仕様です。EJB 2.1(2003年11月)でセッション Bean という部品を1つ作るには、Bean クラスのほか、リモートから呼ばせるならリモートとホームの2つのインターフェース、さらに XML のデプロイ記述子が要りました。Bean クラスは javax.ejb.SessionBean を実装する決まりです。
- 41XML・アノテーション・JavaConfigSpring に「どのクラスを作り、誰に誰を渡すか」という配線の書き方のうち、3通りを扱います。XML の bean 要素(1.0 より前からある)、@Component と @Autowired(2.5、2007年)、@Configuration クラスの @Bean メソッド(3.0、2009年)です。同じ2つの部品をそれぞれで配線すると、Spring Framework 7.0.9 と JDK 25 で結果は同じでした。
- 42Spring Boot の登場2012年10月17日、Spring Framework にコンテナレスな構成への対応を求める issue(Jira の SPR-9888、今の GitHub #14521)が立ちました。web.xml や WAR の構造などを学習の坂として挙げ、main から起動した Spring にサーブレットコンテナを埋め込む形を提案しています。2013年8月6日、Phil Webb はこれが Spring Boot の発端だとコメントしました。
コンテナの仕組み
- 43実行可DI コンテナを素の Java で作るDI コンテナの芯は、素の Java の短いコードで書けます。欲しいクラスを渡されたら、まず作成済みの置き場を見ます。無ければコンストラクタの引数の型をリフレクションで読み、その型それぞれについて同じ手順を先に行い、そろった引数で生成して置き場に入れます。下のコードでは Clock が2か所から要求されますが、作られるのは1回だけです。
- 44ApplicationContext と Bean 定義Spring のコンテナは ApplicationContext で表されます。コンテナが生成して組み立て、管理するオブジェクトを Bean、何をどう作るかの情報を Bean 定義と呼びます。@SpringBootApplication は、設定クラスの印・自動構成・コンポーネントスキャンの3つをまとめたものです。スキャンは、これを付けたクラスのパッケージを起点に、配下の @Component 付きクラスを探します。
- 45Bean のライフサイクルとスコープBean はコンテナが作り、コンテナが片付けます。実測した順序は、コンストラクタ、セッターへの注入、@PostConstruct を付けたメソッド、利用、コンテキストを閉じたときの @PreDestroy でした。コンストラクタの時点では、セッターで入る依存はまだ null です。@PostConstruct の時点では入っているので、依存がそろってから行いたい初期化はここに書けます。
- 46AOP プロキシと自己呼び出しメソッドの前後に共通の処理(アドバイス)を挟む Spring の仕組みを Spring AOP と呼びます(AOP はアスペクト指向プログラミングの略)。対象の Bean は、元のクラスのインスタンスではなく代理(プロキシ)に置き換わります。実測でのクラス名は ReportService$$SpringCGLIB$$0 で、親が ReportService でした。動的プロキシの回の差し込みを、Spring が自動で行っています。
- 47NestJS の DI と何が違うかNestJS も Spring も、コンストラクタに型を書けば依存が渡されますが、読む場所が違います。TypeScript の型注釈は出力に残りません。旧式デコレータ(experimentalDecorators)と emitDecoratorMetadata を有効にすると、コンパイラがクラスに design:paramtypes の1行を足し、NestJS はそれを読みます。Java では型が .class に残り、直接読めます。
Spring Boot
- 48SpringApplication.run がやることmain メソッドに書くのは SpringApplication.run の1行です。Boot 4.1.1 のソースでは、この中で環境(設定値の集まり)を用意し、ApplicationContext を作り、refresh で singleton の Bean を生成し、最後に Runner(起動の最後に呼ばれる自分の処理)を呼びます。組み込み Tomcat が立つのは refresh の途中で、起動ログの各行もこの段階に対応します。
- 49スターターと BOMスターターは、依存の宣言だけを運ぶほぼ空の JAR です。spring-boot-starter-webmvc 4.1.1 の JAR は 4749 バイトで、中身はマニフェストとライセンス文書だけ、クラスは1つもありません。実体は pom に書かれた5つの依存で、そこから先の依存をたどった結果、実行時の依存を1つ書いただけのこのプロジェクトでは実行時クラスパスに 39 個の JAR が並びました。
- 50自動構成は候補を条件でふるいにかける自動構成は、候補の一覧を条件でふるいにかける仕組みです。自動構成を提供する JAR の META-INF/spring に、名前が AutoConfiguration.imports で終わるファイルがあり、構成クラスの名前が1行ずつ並んでいます。starter-webmvc だけの場合は 39 個中6つの JAR に合計 30 行でした。Boot は起動時にこれを読み、各クラスの条件を評価して、通った Bean だけを登録します。
- 51設定の優先順位Spring Boot は設定値を1か所から読むのではなく、重ねて持った複数の設定元から探します。同じキーが複数あれば、順位の高い設定元の値が返ります。実測では、コマンドライン引数、システムプロパティ、環境変数、application-dev.properties、application.properties の順に強く、server.port を5か所に書くと Tomcat は引数の 8084 で待ち受けました。
Web とデータ
- 52DispatcherServlet と組み込み TomcatSpring Boot 4.1.1 に spring-boot-starter-webmvc を入れて main から起動すると、同じプロセスで Tomcat 11.0.24 がポートを開きます。登録される Servlet は DispatcherServlet 1つで、対応づけは / です。リクエストは Tomcat のスレッド、フィルタ、DispatcherServlet と通り、最後に自分のメソッドがリフレクションで呼ばれます。
- 531リクエスト1スレッドと仮想スレッド既定では、Tomcat の作業スレッド1本が1つのリクエストを最初から最後まで受け持ちます。ハンドラの中で Thread.sleep を呼べば、そのスレッドごと止まります。作業スレッドの上限は server.tomcat.threads.max で、既定は 200 です。1秒眠るだけのハンドラへ同時に 400 件送ると、8回とも全体で約 2.1〜2.2 秒かかり、数えた5回では毎回ちょうど 200 件が後回しになっていました。
- 54@Transactional はプロキシとスレッドでできている@Transactional を付けたメソッドを持つクラスの Bean は、実体ではなくプロキシです。実測では AccountService$$SpringCGLIB$$0 というクラスでした。外からメソッドを呼ぶと、プロキシが先にトランザクションを始め、戻るときにコミットします。始めたトランザクションと DB 接続は、ThreadLocal(スレッドごとに別の値を持つ変数)で今のスレッドに結び付けられます。
- 55JPA の永続化コンテキストJPA(Jakarta Persistence)は、Java のオブジェクトと表の行を対応づける標準仕様です。ここでは実装の Hibernate ORM 7.4.5 と、メモリ内で動く DB の H2 を使います。永続化コンテキストは、読んだエンティティ(行に対応するオブジェクト)を載せる作業机で、仕様は、同じ ID につきインスタンスが1つだけになる集まりと定義しています。机はトランザクションの始まりに開かれ、終わると片付きました。
ビルドと成果物
- 56実行可能 JAR の中身Spring Boot 4.1.1 の最小アプリで mvn package を実行すると、19,905,070 バイトの JAR が1つできます。中身は、自分のクラス(2つで計1,504バイト)、依存 JAR 34本、それを読むためのローダのクラス99個です。これが最終的な成果物で、動かすには JAR の外に JRE(Java の実行環境)が要ります。
- 57Maven と Gradle がビルドでやっていることmvn package は「package までのフェーズ(段階)を順に進める」という指示です。各フェーズにはプラグインのゴール(実際の作業)が割り当てられ、実ログでは clean、resources、compile、testResources、testCompile、test、jar、repackage の順に8つ動きました。javac を呼ぶのはそのうち2つで、最後の repackage だけが Spring Boot 由来です。
- 58ビルドが遅いと言われる理由を測って分ける依存を取得済みの状態で、mvn clean package は5回中4回が2.32秒でした。ログの時刻から分けると、Spring を起動するテストが約1.1秒、Maven の起動と POM(設定ファイル)の読み込みが約0.7秒、コンパイルが約0.3秒、JAR の作成が約0.2秒です。javac だけを直接呼ぶと0.31〜0.33秒でした。
- 59起動を速くする方向と失うもの依存を中に入れた実行可能 JAR(fat JAR)は、起動のたびにクラスの読み込みと Bean の定義集めを行います(JVM の起動とクラスロード、JIT の回)。この仕事を前倒しするほど起動は短くなりました。自己申告の起動時間は、fat JAR 約0.6秒、JAR をほどいて置く展開で約0.54秒、AOT キャッシュで約0.25秒、Docker 再起動後に別環境で測った native image で約0.04秒です。
- 60コンテナイメージと3者の比較fat JAR をそのまま1つの層としてイメージに入れると、コードを1行変えるたびに19,914,752バイトの層が作り直しになります。JAR に付いている展開用の機能(-Djarmode=tools)で依存とアプリを別の層に分けると、変わる層は16,384バイトだけでした。イメージ全体は展開後でどちらも約396MBと変わりません。小さくなるのは変更のたびに作り直す部分です。