← 解説「ビルドとは何か: 手作業なら何をするか」に戻る
サンプルコードで身につける: ビルドとは何か: 手作業なら何をするか
解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。
1手作業のビルド5段: コマンドと出力の全文
25 のコマンドだけで、コンパイル、のコピー、テスト、 化、実行の5段を順に行った記録です。(c2) のテストは の JAR 自体を で起動し、を引数で渡しています。どの段も、あるファイル群を読んで、別のファイル群を書いています。
$ 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.classbuild/classes/com/example/greet/Greeter.class##### (b) copy resources: javac は .properties を触らない$ cp -r resources/. build/classes/[exit 0]build/classes/app.propertiesbuild/classes/com/example/app/Main.classbuild/classes/com/example/greet/Greeter.class$ java -cp build/classes:lib/commons-lang3-3.21.0.jar com.example.app.MainHello, 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.MainHello, World![exit 0]$ java -cp dist/app.jar:lib/commons-lang3-3.21.0.jar com.example.app.Main antHello, 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 # 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.MFapp.propertiescom/example/app/Main.classcom/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 になり、手作業ならここで次の段に進まない判断を人がします。
##### (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 existimport org.apache.commons.lang3.StringUtils;^src/com/example/greet/Greeter.java:13: error: cannot find symbolreturn greeting + ", " + StringUtils.capitalize(name) + "!";^symbol: variable StringUtilslocation: class Greeter2 errors[exit 1]##### (b) を飛ばして実行: app.properties がコピーされていない$ java -cp build/classes:lib/commons-lang3-3.21.0.jar com.example.app.MainException in thread "main" java.lang.IllegalStateException: app.properties not found on the classpathat 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.jarException in thread "main" java.lang.NoClassDefFoundError: org/apache/commons/lang3/StringUtilsat 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 では依存の場所をコンパイル時にも実行時にも人(またはツール)が並べる点です。
(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.jarNode : npm pack(公式: Create a tarball from a package)違い : app.jar に依存は入らない(e) 依存の配置Java : java -cp dist/app.jar:lib/… com.example.app.MainNode : node dist/main.js違い : Java では実行時にも依存の場所を人が -cp で並べる済んでいるかの判定Java : 無い(手作業では毎回全部やる)Node : (この表では対応を取っていない)違い : Ant/Maven/Gradle が足すもの依存を取ってくるJava : 人が Maven Central から curl で取り、.sha1 と照合して lib/ に置くNode : npm install違い : Ant には無い。Maven/Gradle と Ivy が受け持つ