Dev Study
← 解説「ビルドとは何か: 手作業なら何をするか」に戻る

サンプルコードで身につける: ビルドとは何か: 手作業なら何をするか

解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。

1手作業のビルド5段: コマンドと出力の全文

25 のコマンドだけで、コンパイル、のコピー、テスト、 化、実行の5段を順に行った記録です。(c2) のテストは の JAR 自体を で起動し、を引数で渡しています。どの段も、あるファイル群を読んで、別のファイル群を書いています。

$ sh manual-build.sh(JDK 25、抜粋。ディレクトリ行と JUnit の containers 行は省略)コマンド出力
$ find . -type f | sort # 出発点
./lib/commons-lang3-3.21.0.jar
./lib-test/junit-platform-console-standalone-6.1.3.jar
./resources/app.properties
./src/com/example/app/Main.java
./src/com/example/greet/Greeter.java
./test/com/example/greet/GreeterTest.java
 
##### (a) compile: 依存の JAR を -cp に
$ javac -d build/classes -cp lib/commons-lang3-3.21.0.jar $(find src -name "*.java")
[exit 0]
build/classes/com/example/app/Main.class
build/classes/com/example/greet/Greeter.class
 
##### (b) copy resources: javac は .properties を触らない
$ cp -r resources/. build/classes/
[exit 0]
build/classes/app.properties
build/classes/com/example/app/Main.class
build/classes/com/example/greet/Greeter.class
$ java -cp build/classes:lib/commons-lang3-3.21.0.jar com.example.app.Main
Hello, World!
[exit 0]
 
##### (c) compile tests: main の .class と JUnit の JAR も -cp に
$ javac -d build/test-classes \
-cp build/classes:lib/commons-lang3-3.21.0.jar:lib-test/junit-platform-console-standalone-6.1.3.jar \
$(find test -name "*.java")
[exit 0]
build/test-classes/com/example/greet/GreeterTest.class
 
##### (c2) run tests: JUnit の JAR 自体を java -jar で起動する
$ java -jar lib-test/junit-platform-console-standalone-6.1.3.jar execute \
--class-path build/test-classes:build/classes:lib/commons-lang3-3.21.0.jar \
--scan-class-path --disable-banner --disable-ansi-colors --details=tree
╷
├─ JUnit Platform Suite ✔
├─ JUnit Jupiter ✔
│ └─ GreeterTest ✔
│ └─ capitalizesTheName() ✔
└─ JUnit Vintage ✔
 
Test run finished after 31 ms
...
[ 1 tests found ]
[ 1 tests successful ]
[ 0 tests failed ]
[exit 0]
 
##### (d) package: 依存は入らない
$ mkdir -p dist && jar --create --file dist/app.jar --main-class com.example.app.Main -C build/classes .
[exit 0]
 
##### (e) run: 依存は JAR の外に置いたまま、クラスパスに並べる
$ java -cp dist/app.jar:lib/commons-lang3-3.21.0.jar com.example.app.Main
Hello, World!
[exit 0]
$ java -cp dist/app.jar:lib/commons-lang3-3.21.0.jar com.example.app.Main ant
Hello, Ant!
[exit 0]

2題材のソース3ファイル

2と JUnit(Jupiter)のテスト1本の最小の題材です。Greeter は の StringUtils を使い、Main はクラスパス上の app.properties を読みます。この2つが、(a) で依存が要る理由と、(b) でリソースのコピーが要る理由になっています。

// src/com/example/greet/Greeter.java — 依存 commons-lang3 の StringUtils を使う
package com.example.greet;

import org.apache.commons.lang3.StringUtils;

public class Greeter {
    private final String greeting;

    public Greeter(String greeting) {
        this.greeting = greeting;
    }

    public String greet(String name) {
        return greeting + ", " + StringUtils.capitalize(name) + "!";
    }
}

// src/com/example/app/Main.java — クラスパス上の /app.properties を読む。無ければ例外
package com.example.app;

import java.io.InputStream;
import java.util.Properties;

import com.example.greet.Greeter;

public class Main {
    public static void main(String[] args) throws Exception {
        Properties p = new Properties();
        try (InputStream in = Main.class.getResourceAsStream("/app.properties")) {
            if (in == null) {
                throw new IllegalStateException("app.properties not found on the classpath");
            }
            p.load(in);
        }
        Greeter g = new Greeter(p.getProperty("greeting", "Hi"));
        System.out.println(g.greet(args.length > 0 ? args[0] : "world"));
    }
}

// test/com/example/greet/GreeterTest.java — JUnit(Jupiter)のテスト1本
package com.example.greet;

import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.Test;

class GreeterTest {
    @Test
    void capitalizesTheName() {
        assertEquals("Hello, World!", new Greeter("Hello").greet("world"));
    }
}

// resources/app.properties は greeting=Hello の1行

3出来上がったファイルの一覧と JAR の中身

5段を終えた後の と、 で見た app.jar の中身です。app.jar は 2,489 B で、自分の 2個と app.properties と だけが入っています。749,083 B の commons-lang3 は入っていないので、実行時には別に並べる必要があります。

$ find . -type f | sort / jar tf dist/app.jar / ls -lコマンド出力
$ find . -type f | sort # 5段すべて終えた後(スクリプトとログを除く)
./build/classes/app.properties ← (b) がコピー
./build/classes/com/example/app/Main.class ← (a) が書く
./build/classes/com/example/greet/Greeter.class ← (a) が書く
./build/test-classes/com/example/greet/GreeterTest.class ← (c) が書く
./dist/app.jar ← (d) が書く
./lib/commons-lang3-3.21.0.jar ← 人が置いた
./lib-test/junit-platform-console-standalone-6.1.3.jar ← 人が置いた
./resources/app.properties
./src/com/example/app/Main.java
./src/com/example/greet/Greeter.java
./test/com/example/greet/GreeterTest.java
 
$ jar tf dist/app.jar # JAR の中身(ディレクトリ行は省略)
META-INF/MANIFEST.MF
app.properties
com/example/app/Main.class
com/example/greet/Greeter.class
 
$ ls -l dist/app.jar lib/*.jar lib-test/*.jar
-rw-r--r-- 1 root root 2489 Oct 3 00:20 dist/app.jar
-rw-r--r-- 1 root root 2997949 Oct 3 00:19 lib-test/junit-platform-console-standalone-6.1.3.jar
-rw-r--r-- 1 root root 749083 Oct 3 00:19 lib/commons-lang3-3.21.0.jar

4段を飛ばしたときの失敗4つ

同じ題材で、段を1つ飛ばすか失敗させたときの実物です。依存なしのコンパイルは が止めますが、リソースのコピー忘れと依存の配置忘れは、コンパイルが通ってから実行時にで分かります。テストが落ちると終了コードが 1 になり、手作業ならここで次の段に進まない判断を人がします。

4つの失敗(各1回。JDK 25)コマンド出力
##### (a) を依存なしで: -cp に commons-lang3 が無い
$ javac -d /tmp/x $(find src -name "*.java")
src/com/example/greet/Greeter.java:3: error: package org.apache.commons.lang3 does not exist
import org.apache.commons.lang3.StringUtils;
^
src/com/example/greet/Greeter.java:13: error: cannot find symbol
return greeting + ", " + StringUtils.capitalize(name) + "!";
^
symbol: variable StringUtils
location: class Greeter
2 errors
[exit 1]
 
##### (b) を飛ばして実行: app.properties がコピーされていない
$ java -cp build/classes:lib/commons-lang3-3.21.0.jar com.example.app.Main
Exception in thread "main" java.lang.IllegalStateException: app.properties not found on the classpath
at com.example.app.Main.main(Main.java:13)
[exit 1]
 
##### (c2) でテストが落ちる: 期待値を1文字変えた版(コマンドは (c2) と同じ)
$ java -jar lib-test/junit-platform-console-standalone-6.1.3.jar execute ...
│ └─ capitalizesTheName() ✘ expected: <Hello, world!> but was: <Hello, World!>
...
=> org.opentest4j.AssertionFailedError: expected: <Hello, world!> but was: <Hello, World!>
com.example.greet.GreeterTest.capitalizesTheName(GreeterTest.java:10)
...
[ 0 tests successful ]
[ 1 tests failed ]
[exit 1]
 
##### (e) を java -jar だけで: 依存は JAR の中に無い
$ java -jar dist/app.jar
Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/commons/lang3/StringUtils
at com.example.greet.Greeter.greet(Greeter.java:13)
at com.example.app.Main.main(Main.java:18)
Caused by: java.lang.ClassNotFoundException: org.apache.commons.lang3.StringUtils
...
[exit 1]

5Node/NestJS との対応表

5段と、済んでいるかの判定、依存の取得を、Node 側で近いものと並べました。対応は役割が近いという意味で、当たらない点を右端に書いています。いちばん大きな違いは、Java では依存の場所をコンパイル時にも実行時にも人(またはツール)が並べる点です。

手作業のビルドと Node 側の対応(当たらない点つき)表
(a) コンパイル
Java : javac -d build/classes -cp lib/…
Node : tsc(公式: tsconfig.json のプロジェクトをコンパイル)
違い : javac は依存の JAR を -cp に書かないと型を調べられない
(package … does not exist)
 
(b) リソースのコピー
Java : cp -r resources/. build/classes/
Node : ビルド時のアセットのコピー
違い : 対応する Node 公式の一文は確かめていない
 
(c) テスト
Java : javac でテストをコンパイル →
java -jar junit-platform-console-standalone-6.1.3.jar execute …
Node : jest
違い : JUnit はコマンドではなく JAR。java -jar で起動し、
クラスパスを引数で渡す
 
(d) まとめる
Java : jar --create … → dist/app.jar
Node : npm pack(公式: Create a tarball from a package)
違い : app.jar に依存は入らない
 
(e) 依存の配置
Java : java -cp dist/app.jar:lib/… com.example.app.Main
Node : node dist/main.js
違い : Java では実行時にも依存の場所を人が -cp で並べる
 
済んでいるかの判定
Java : 無い(手作業では毎回全部やる)
Node : (この表では対応を取っていない)
違い : Ant/Maven/Gradle が足すもの
 
依存を取ってくる
Java : 人が Maven Central から curl で取り、
.sha1 と照合して lib/ に置く
Node : npm install
違い : Ant には無い。Maven/Gradle と Ivy が受け持つ

図やコードに出てくる用語(タップで説明)