ข้าม​ไป​ยัง​เนื้อหา

จาก launcher สู่ Maven และ JSON ที่ JDK ไม่มี​ให้

บท​ที่​แล้ว orderctl รัน​ได้​ด้วย​คำ​สั่ง​เดียว​ที่​ไม่​ต้อง​ตั้ง​ค่า​อะไร​เลย

บท​นี้​เรา​ทิ้ง​ความ​สะดวก​นั้น แล้ว​แลก​มา​ด้วย​สาม​อย่าง​ที่​บท​ที่​แล้ว​ให้​ไม่​ได้ — เทสต์ ของ​ที่​ห่อ​แล้ว และ library จาก​ข้าง​นอก

📦 บท​นี้​เปลี่ยน​วิธี​รัน​ทั้ง​คอร์ส

ตั้งแต่​บรรทัด​นี้​ไป ทุก​ตัวอย่าง​อยู่​ใน package orderctl และ​รัน​ด้วย mvn -q compile exec:java · รูป​แบบ​ของ​บท​ที่ 1 กับ 2 จะ​ไม่​กลับ​มา​อีก

ผลลัพธ์​ที่​พิมพ์​ใน​บท​นี้​ทั้งหมด​มา​จาก​วิธี​รัน​แบบ​ใหม่ · ข้อความ​ความ​ผิด​พลาด​ของ​สอง​วิธี​นี้ ไม่​เหมือน​กัน จึง​คัด​ลอก​ข้าม​บท​ไม่​ได้

ทุก​ผลลัพธ์​ใน​บท​นี้​วัด​บน OpenJDK 25.0.4.1 · macOS arm64 · Apache Maven 3.9.16 เมื่อ 2026-08-27

สาม​ข้อ​ที่​วิธี​ของ​บท​ที่​แล้ว​ทำ​ไม่​ได้ · บท​นี้​ปิด​สอง​ข้อ และ​เปิด​ทาง​ให้​ข้อ​แรก​ที่​บท​ที่ 7 มา​ปิด

ข้อ​แรก เทสต์​รัน​ไม่​ได้​เลย ไม่ใช่​ว่ายาก แต่​คือ​รัน​ไม่​ได้ · JUnit เป็น library ที่​โหลด​มา​จาก class path ส่วน​โปรแกรม​ของ​บท​ที่​แล้ว​ถูก​แปล​ไว้​ใน​หน่วย​ความ​จำ · สอง​ฝั่ง​นี้​มอง​ไม่​เห็น​กัน

ข้อ​ที่​สอง ไม่มี​อะไร​เหลือ​ไว้​ให้​ส่ง​ต่อ รัน​เสร็จ​แล้ว​ไม่มี .class ไม่มี jar ไม่มี​อะไร​ที่ copy ไป​เครื่อง​อื่น​ได้ · สิ่ง​เดียว​ที่​ส่ง​ต่อ​ได้​คือ source กับ​คำ​สั่ง

ข้อ​ที่​สาม ความ​ผิด​พลาด​ตอน​แปล​โผล่​กลาง​ทาง​ได้ ตัว​รัน​แปล class เท่า​ที่​จำเป็น​ก่อน แล้ว​แปล ตัว​ที่​เหลือ​ตอน​ที่​มัน​ถูก​โหลด​จริง · class ที่​โปรแกรม​เรียก​ทีหลัง จึง​ถูก​ตรวจ​ทีหลัง​ด้วย

โปรแกรม​สอง file ที่​ตัว​ที่​สอง​ถูก​โหลด​ตอน​รัน​ด้วย Class.forName ให้​ผล​แบบ​นี้

$ java Probe.java
บรรทัดนี้พิมพ์ออกมาแล้ว
/…/Late.java:2: error: incompatible types: int cannot be converted to String
String text() { return 1; }
^
1 error
error: compilation failed

บรรทัด​แรก​คือ output ของ​โปรแกรม บรรทัด​ที่​เหลือ​คือ​ความ​ผิด​พลาด​ตอน​แปล · มัน​ออก​มา​หลัง​จาก โปรแกรม​ทำงาน​ไป​แล้ว​ส่วน​หนึ่ง

อย่า​ออกแบบ​อะไร​บน​พฤติกรรม​ข้อ​สาม

JEP 458 เขียน​ไว้​เอง​ว่า​ตัว​รัน​เลือก​ได้​ว่า​จะ​แปล class ไหน​ตอน​ไหน และ​วิธี​เลือก​นั้น​เปลี่ยน​ได้​ระหว่าง​รุ่น

บน​เครื่อง​ที่​วัด​บท​นี้ ถ้า class ตัว​ที่​สอง​ถูก​อ้าง​ถึง​ตรงๆ ใน​โปรแกรม มัน​ถูก​ตรวจ​ตั้งแต่​แรก และ​ความ​ผิด​พลาด​ออก​มา​ก่อน​บรรทัด output ทุก​ครั้ง · ต้อง​เลี่ยง​การ​อ้าง​ตรงๆ ด้วย Class.forName ถึง​จะ​เห็น​แบบ​ข้าง​บน

สิ่ง​ที่​เชื่อ​ได้​จึง​เป็น​ข้อ​สรุป​เชิง​ทิศทาง — วิธี​นี้​ไม่​รับ​ประกัน​ว่า​ทั้ง​โปรแกรม​ถูก​ตรวจ​ก่อน​เริ่ม​ทำงาน · ส่วน build tool แปล​ทั้ง​โปร​เจ​กต์​ให้​จบ​ก่อน​เสมอ

ทั้ง file มี​สี่​ก้อน และ​ก้อน​ที่​ตัด​ออก​ไม่​ได้​คือ​ก้อน​ที่​หน้าตา​เหมือน​ของ​ไม่​สำคัญ​ที่สุด

สร้าง​ไดเรกทอรี orderctl/ แล้ว​ใส่ pom.xmlpom.xmlคำ​ประกาศ​โปร​เจ​กต์​ของ Maven · ถ้า​ไม่​ตั้ง `maven.compiler.release` มัน​ย้อน​ไป target 1.8 แล้ว record แดง​ทันที ไว้​ที่​ราก

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>dev.kaen</groupId>
<artifactId>orderctl</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.release>25</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>3.2.2</version>
</dependency>
</dependencies>
</project>
ก้อนทำ​อะไรตัด​ออก​ได้​ไหม
modelVersionบอก​ว่า pom นี้​เขียน​ตาม​รูป​แบบ 4.0.0ไม่​ได้ Maven ปฏิเสธ​ทันที
groupId artifactId versionพิกัด​สาม​ชั้น​ที่​ระบุ​โปร​เจ​กต์​นี้​ใน​โลกไม่​ได้ ทั้ง​สาม​ตัว​บังคับ
maven.compiler.releaseบอก​ว่า​แปล​ให้ Java 25ตัด​แล้ว​พัง ดู​ข้าง​ล่าง
project.build.sourceEncodingบอก​ว่า source เป็น UTF-8ตัด​ได้ แต่​จะ​มี​คำ​เตือน​ขึ้น​สอง​บรรทัด
dependenciesราย​ชื่อ library ที่​เรา​ประกาศ​เองตัด​ได้ ถ้า​ไม่​ใช้​ของ​ข้าง​นอก​เลย

ก้อน​ที่​กัด​คน​มาก​ที่สุด​คือ maven.compiler.release · มัน​ดู​เหมือน​ค่าที่​ไม่​ใส่​ก็ได้ แต่ plugin ที่​ทำ​หน้าที่​แปล​มี​ค่า​เริ่ม​ต้น​ของ​ตัว​เอง​รอ​อยู่ และ​ค่า​นั้น​เก่า​กว่า​ที่​คุณ​คิดมาก

ลบ​บรรทัด​นั้น​ออก​แล้ว​สั่ง mvn -q compile จะ​ได้​แบบ​นี้

[ERROR] .../src/main/java/orderctl/Line.java:[6,8] records are not supported in -source 8
(use -source 16 or higher to enable records)

-source 8 ไม่​ได้​โผล่​มา​จาก​ไหน​เลย · มัน​คือ​ค่า​เริ่ม​ต้น​ของ source และ target ใน plugin maven-compiler-plugin 3.15.0 ส่วน release ไม่มี​ค่า​เริ่ม​ต้น เมื่อ​ไม่​ตั้ง มัน​จึง​ตก​กลับ​ไป​ที่​คู่​เก่า

ส่วน​ก้อน project.build.sourceEncoding ที่​ตัด​ออก​ได้ ราคา​ของ​การ​ตัด​คือ​คำ​เตือน​สอง​บรรทัด​นี้ ทุก​ครั้ง​ที่ build · -q กลืน​คำ​เตือน​ไป​หมด ต้อง​สั่ง mvn compile เปล่าๆ ถึง​จะ​เห็น

[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[WARNING] File encoding has not been set, using platform encoding UTF-8, i.e. build is platform dependent!

คำ​ว่า platform dependent คือ​คำ​สำคัญ · บน​เครื่อง​ที่​ตั้ง locale ไว้​อีก​แบบ ข้อความ​ไทย​ใน​โปรแกรม จะ​ถูก​อ่าน​ด้วย charset คนละ​ตัว แล้ว​ผลลัพธ์​เพี้ยน​โดย​ไม่มี​อะไร​แดง

ใช้ release เสมอ ไม่ใช่ source กับ target

คุณ​จะ​เจอ pom จำนวน​มาก​บน​เน็ต​ที่​เขียน maven.compiler.source กับ maven.compiler.target เป็น​คู่ · นั่น​คือ​สำนวน​ก่อน​ที่ --release จะ​มี​ใน Java 9

ต่าง​กัน​ตรง​นี้ — source กับ target คุม​แค่​ไวยากรณ์​กับ​รูป​แบบ​ของ .class แต่​ยัง​ปล่อย​ให้ คุณ​เรียก API ที่​ไม่มี​อยู่​ใน​รุ่น​เป้า​หมาย​ได้ · โปรแกรม​จึง​แปล​ผ่าน​แล้วไป​พัง​ตอน​รัน​บน​รุ่น​เก่า

--release ผูก​ไวยากรณ์ รูป​แบบ และ​รายการ API เข้า​ด้วย​กัน · คอร์ส​นี้​ใช้ maven.compiler.release ทุก​ที่ และ​ไม่มี​ที่ไหน​ใช้​คู่​เก่า​เลย

ทำไม​เรา​ไม่​สั่ง mvn archetype:generate

คำ​สั่ง​สร้าง​โครง​อัตโนมัติ​ของ Maven เป็น​ทาง​ลัด​ที่​คน​แนะนำ​กัน​มาก · เรา​ไม่​ใช้​มัน​ด้วย​เหตุผล​ที่​วัด​ได้

โหมด​ไม่​ถาม​คำถาม​คาย junit:junit:3.8.1 ลง​มา​เป็น dependency และ ไม่​ใส่ maven.compiler.release ให้ · pom ที่​ได้​จึง​แปล record ไม่​ผ่าน​ตั้งแต่ file แรก​ที่​คุณ​เขียน​เอง

ส่วน​โหมด​ที่​ถาม​คำถาม​คาย pom ยาว​เก้า​สิบ​บรรทัด ซึ่ง​ยาว​กว่า​ที่​บท​นี้​ต้องการ​สี่​เท่า​ครึ่ง · ยี่สิบ​บรรทัด​ที่​คุณ​พิมพ์​เอง​แล้ว​อ่าน​ออก​ทุก​บรรทัด มี​ค่า​มากกว่า​เก้า​สิบ​บรรทัด​ที่​คุณ​ไม่​กล้า​แตะ

การ​ย้าย​เข้า Maven เปลี่ยน​หก​อย่าง​พร้อม​กัน และ​ห้า​อย่าง​ใน​นั้น​เป็น​ผล​ของ​การ​มี package

โครง​ที่ Maven คาด​หวัง​ลึก​สี่​ชั้น​ก่อน​จะ​ถึง source ตัว​แรก

orderctl/
pom.xml
order.json ออเดอร์ตัวอย่างที่โปรแกรมรับเป็น argument
src/main/java/orderctl/Line.java
src/main/java/orderctl/Order.java
src/main/java/orderctl/Main.java
target/ Maven สร้างเอง ห้ามใส่ใน git

สี่​ชั้น src/main/java/orderctl/ คือ​ราคา​ที่​คอร์ส Go ไม่​ต้อง​จ่าย · module ของ Go วาง package ไว้​ที่ internal/order/ ซึ่ง​ลึก​สอง​ชั้น และ​ตั้ง​ชื่อ​ชั้น​เอง​ได้​เกือบ​ทั้งหมด

Maven กวาด​เฉพาะ​สิ่ง​ที่​อยู่​ใต้ src/main/java · ย้าย Line.java ออก​ไป​ไว้​ที่​อื่น​แล้ว​มัน​จะ ไม่​ถูก​แปล​เลย และ​เรา​รู้ตัว​ก็​ต่อ​เมื่อ Order.java แดง​ด้วย cannot find symbol: class Line

มี​อีก​ชั้น​ชื่อ src/main/resources ที่​เรา​ไม่​ได้​ใช้​ใน​บท​นี้ · ของ​ที่​วาง​ไว้​ตรง​นั้น​จะ​ถูก copy เข้าไป​อยู่​ใน jar ด้วย ซึ่ง​เหมาะ​กับ file ที่​ต้อง​เดิน​ทาง​ไป​พร้อม​โปรแกรม ไม่ใช่​ข้อมูล​นำ​เข้า

บท​ที่ 2 เขียน​แบบ​นี้บท​นี้​เขียน​แบบ​นี้เพราะ​อะไร
ไม่มี​บรรทัด packagepackage orderctl; บรรทัด​แรกรูป​ย่อ​ของ​บท​ที่​แล้ว​ประกาศ package ไม่​ได้​เลย
void main()public static void main(String[] args)ไม่มี class ที่​ตัว​รัน​สร้าง instance ให้​อีก​แล้ว
IO.println ของ​บท​ที่ 2System.out.printlnclass ช่วย​ตัว​นั้น​ผูก​กับ​รูป​ย่อ
record Line อยู่​ใน Domain.java ร่วม​กับ mainLine.java แยก​ออก​มาpublic type หนึ่ง​ตัว​ต่อ​หนึ่ง file
ไม่​ต้อง​เขียน import ของ java.baseเขียน import java.util.List; เองรูป​ย่อ​ได้ java.base มา​ให้​ฟรี รูป​เต็ม​ไม่​ได้
ไม่มี​อะไร​ตกค้างtarget/classes/orderctl/*.classนี่​คือ​ของ​ที่​บท​ที่​แล้ว​ไม่มี

แถว​ที่​สี่​คือ​แถว​ที่​บท​ที่ 2 ปู​ทาง​ไว้​ให้​แล้ว · javac -d out Domain.java ตรง​นั้น​คาย Domain$Line.class ออก​มา ไม่ใช่ Line.class

ชื่อ Domain$Line เป็น​ส่วน​หนึ่ง​ของ​สัญญา​ที่ class อื่น​ต้อง​เรียก​ให้​ถูก · การ​ย้าย​มา​อยู่ file ของ​ตัว​เอง​จึง​ไม่ใช่​เรื่อง​ความ​สวยงาม มัน​เปลี่ยน​ชื่อ​จริง​ของ type

และ file ที่​สี่​ที่​ต้อง​ย้าย​ตาม​มา​ด้วย เพราะ​บท​หลัง​ใช้​มัน

Line.java ยืน​อยู่​ได้​ด้วย​ตัว​เอง ไม่​ต้อง​พึ่ง type อื่น​ใน​โปร​เจ​กต์​เลย

src/main/java/orderctl/Line.java
package orderctl;
import java.util.Locale;
import java.util.Objects;
public record Line(String sku, int qty, long unitSatang) {
public Line {
Objects.requireNonNull(sku, "sku");
if (qty <= 0) {
throw new IllegalArgumentException("qty ต้องมากกว่า 0 แต่ได้ " + qty);
}
if (unitSatang < 0) {
throw new IllegalArgumentException("ราคาติดลบไม่ได้");
}
sku = sku.trim().toUpperCase(Locale.ROOT);
}
public long subtotalSatang() {
return qty * unitSatang;
}
}

เนื้อ​ใน​เหมือน​บท​ที่ 2 ทุก​บรรทัด · สิ่ง​ที่​เพิ่ม​มา​คือ package public และ import สอง​บรรทัด ที่​รูป​ย่อ​เคย​ได้​มา​ฟรี

Order.java เป็น​ที่​แรก​ที่​ต้อง​เขียน import ของ collection เอง

src/main/java/orderctl/Order.java
// ชิ้นส่วน ไม่ใช่โปรแกรมสมบูรณ์ · ต้องมี Line.java อยู่ข้างกันจึงจะแปลผ่าน
package orderctl;
import java.util.List;
import java.util.Objects;
public record Order(String id, List<Line> lines) {
public Order {
Objects.requireNonNull(id, "id");
lines = List.copyOf(lines);
}
public long grossSatang() {
long sum = 0;
for (Line line : lines) {
sum += line.subtotalSatang();
}
return sum;
}
}

Line ไม่​ต้อง import เพราะ​อยู่ package เดียวกัน · ส่วน List กับ Objects อยู่​คนละ​ที่ จึง​ต้อง​เขียน​ออก​มา​ทั้ง​คู่

Main.java คือ​จุด​เริ่ม​ของ​โปรแกรม และ​เป็น type เดียว​ใน​สาม​ตัว​นี้​ที่​ไม่ใช่ record

src/main/java/orderctl/Main.java
// ชิ้นส่วน ไม่ใช่โปรแกรมสมบูรณ์ · ต้องมี Line.java กับ Order.java อยู่ข้างกัน
package orderctl;
import java.nio.file.Path;
import tools.jackson.databind.json.JsonMapper;
public final class Main {
static final JsonMapper MAPPER = JsonMapper.builder().build();
public static void main(String[] args) {
if (args.length < 1) {
System.err.println("ใช้: orderctl <order.json>");
System.exit(2);
}
Order order = MAPPER.readValue(Path.of(args[0]), Order.class);
System.out.println(order.id() + " รวม " + order.grossSatang() + " สตางค์");
}
}
`main` ไม่มี `throws` เลย และ​มัน​แปล​ผ่าน

นี่​คือ​จุด​ที่ Jackson 3 ต่าง​จาก Jackson 2 มาก​ที่สุด และ​เป็น​จุด​ที่ tutorial บน​เน็ต​จะ​พา​คุณ​หลง​ทาง

สำนวน​ของ Jackson 2 ครอบ readValue ด้วย catch (IOException e) เพราะ​มัน​ประกาศ throws IOException ไว้ · ตัวอย่าง​เกือบ​ทุก​อัน​ที่​คุณ​ค้น​เจอ​เขียน​แบบ​นั้น

สำนวน​ของ Jackson 3 ไม่มี throws เลย เพราะ​ความ​ผิด​พลาด​ของ​มัน​เป็น unchecked ทั้งหมด · ถ้า​คุณ​คัด​ลอก catch (IOException e) มา​ใส่ มัน​จะ ไม่​แปล​ผ่าน เพราะ​ไม่มี​อะไร​ใน try โยน checked IOException ออก​มา​ให้​จับ

readValue(Path, Class) ก็​เป็น​อีก​อย่าง​ที่ Jackson 2 ไม่มี · ของ​เก่า​รับ File ไม่​รับ Path

flowchart LR
  P["pom.xml<br/>ประกาศ jackson-databind หนึ่งบรรทัด"] --> M["Maven"]
  S["src/main/java/orderctl/<br/>Line · Order · Main"] --> M
  M -->|"ดึงมาเก็บที่ ~/.m2"| J["jackson-databind<br/>jackson-core<br/>jackson-annotations"]
  M -->|"compile"| C["target/classes/orderctl/"]
  J -.->|"อยู่บน class path ทั้งตอนแปลและตอนรัน"| C
  C -->|"package"| A["target/orderctl-1.0.0.jar<br/>มีแต่ class ของเราเอง"]

คำ​บรรยาย​ภาพ: Maven รับ​สอง​ทาง​เข้า คือ pom กับ source แล้ว​คาย class ออก​มา · jar ที่​ได้​ปลาย​ทาง​มี​แต่ class ของ​โปร​เจ​กต์​เอง ไม่มี​ของ​ที่​ดึง​มา​จาก​ข้าง​นอก​อยู่​ข้าง​ใน

Discount.java ย้าย​ตาม​มา​จาก​บท​ที่ 2 โดย​ไม่​เปลี่ยน​รูป​เลย ได้ package เพิ่ม​บรรทัด​เดียว

src/main/java/orderctl/Discount.java
package orderctl;
public sealed interface Discount {
record Percent(int pct) implements Discount {}
record Fixed(long satang) implements Discount {}
record None() implements Discount {}
}

บท​ที่ 5 จะ​เอา​ชนิด​นี้​ไป​แปลง​เป็น​กฎ​ส่วนลด​ที่​ต่อ​เพิ่ม​ได้ · ถ้า​ไม่​ย้าย​มัน​มา​ตอน​นี้ บท​นั้น​จะ​อ้าง​ชนิด​ที่​ไม่มี​อยู่​ใน​โปร​เจ​กต์

sealed ที่​บท​ที่ 2 เขียน​ไว้ ต้อง​เติม public ตรง​นี้ เพราะ file อื่น​ใน package เดียวกัน ยัง​เห็น​ได้​ก็​จริง แต่ Rules ของ​บท​ที่ 5 คืน​ค่าที่​มี​ชนิด​นี้​ออก​ไป​เป็น​ส่วน​หนึ่ง​ของ API

standard library ของ Java แบ่ง​เป็น​ก้อน และ​ไม่มี​ก้อน​ไหน​เกี่ยว​กับ JSON เลย

บท​ที่ 1 ชวน​ให้​ลอง​สั่ง java --list-modules · คราว​นี้​กรอง​ด้วย​คำ​ว่า json

$ java --list-modules | wc -l
69
$ java --list-modules | grep -i json
$

หก​สิบ​เก้า​ก้อน ศูนย์​ผลลัพธ์ · นี่​คือ​ความ​ต่าง​ข้อ​ใหญ่​ที่สุด​ระหว่าง​คอร์ส​นี้​กับ​คอร์ส Go ซึ่ง​ใช้ encoding/json ที่มา​ใน​กล่อง​ตั้งแต่​วัน​แรก

ของ​ที่​กำลัง​มา และ​เหตุผล​ที่​มัน​ไม่​ช่วย​เรื่อง​นี้

JEP 540 เสนอ JSON API ตัว​แรก​ของ JDK เข้า​มา​เป็น incubator · สถานะ​ปัจจุบัน​คือ Targeted ที่ Release 28 (อ่าน​หน้า JEP เมื่อ 2026-08-27)

สอง​ข้อ​ที่​ทำให้​มัน​ไม่​แทนที่​บท​นี้ · ข้อ​แรก มัน​เป็น incubator จึง​ต้อง​สั่ง --add-modules jdk.incubator.json แล้ว​ตัว​รัน​จะ​พิมพ์​บรรทัด WARNING: Using incubator modules: jdk.incubator.json ให้​ทุก​ครั้ง

ข้อ​ที่​สอง เอกสาร​ของ JEP เขียน​ไว้​เอง​ใน​หัวข้อ Goals ว่า​จะ​เลี่ยง​ความ​สามารถ​อย่าง data bindingdata bindingการ​แปลง​ข้อความ JSON เข้า​เป็น​ชนิด​ของ​โปร​แก​รม​ตรงๆ ด้วย `readValue(doc, Order.class)` · ต่าง​จาก tree API ที่​ให้​เดิน​อ่าน​ที​ละ​คีย์​เอง · มัน​เป็น tree API ที่​ให้​เดิน​อ่าน​ที​ละ​คีย์​เอง ไม่ใช่​ของ​ที่​แปลง JSON เข้า Order ให้​ใน​บรรทัด​เดียว

ย่อหน้า​นี้​มี​วัน​หมด​อายุ​ที่​รู้​แล้ว · ราว​มีนาคม 2027 เมื่อ JDK 28 ออก ประโยค “JDK ไม่มี JSON” ต้อง​กลับ​มา​แก้​ให้​ตรง แต่​ข้อ​สรุป​ว่า​มัน​ไม่​แทน readValue จะ​ยัง​เหมือน​เดิม

ของ​ที่​เรา​ประกาศ​เอง​มี​ตัว​เดียว แต่​ของ​ที่​ลง​มา​บน disk จริง​มี​สาม​ตัว

pom.xml ข้าง​บน​ประกาศ jackson-databind ไว้ตัว​เดียว · สั่ง mvn dependency:tree แล้ว​ดู ว่า​มี​อะไร​ลง​มา​บ้าง

$ mvn dependency:tree
[INFO] dev.kaen:orderctl:jar:1.0.0
[INFO] \- tools.jackson.core:jackson-databind:jar:3.2.2:compile
[INFO] +- com.fasterxml.jackson.core:jackson-annotations:jar:2.22:compile
[INFO] \- tools.jackson.core:jackson-core:jar:3.2.2:compile

สอง​ตัว​ที่​ย่อหน้า​เข้าไป​คือ transitive dependencytransitive dependencyสิ่ง​ที่​พึ่งพา​โดย​อ้อม มา​พร้อม​กับ​ตัว​ที่​เรา​ประกาศ​เอง · ประกาศ jackson-databind หนึ่ง​บรรทัด​แล้ว​ได้ jar สาม​ตัว​จริง · เรา​ไม่​ได้​ขอ​มัน​ตรงๆ มัน​มา​เพราะ jackson-databind ขอ​ไว้​เอง แล้ว Maven หา​มา​ให้​ครบ​ทั้ง​สาย

แถว​กลาง​มี​ของ​แปลก​ซ่อน​อยู่ · jackson-annotations ยัง​เป็น com.fasterxml.jackson.core รุ่น 2.22 ขณะ​ที่​อีก​สอง​ตัว​เป็น tools.jackson.core รุ่น 3.2.2

นั่น​ไม่ใช่​ความ​ผิด​พลาด​ของ​การ​แก้ pom · Jackson ไม่​ได้​ทำ annotations สาย 3.x ขึ้น​มา​ใหม่ โปร​เจ​กต์​เดียว​จึง​มี​สอง prefix ปน​กัน​อยู่​จริง และ​ทั้ง​คู่​ถูก​ต้อง

ขาด jar ที่​ไม่​ได้​ขอ แล้ว​แดง​ตอน​รัน ไม่ใช่​ตอน​แปล

สมมติ​คุณ​เลิก​ใช้ Maven แล้ว​โหลด jar เอง​สอง​ใบ คือ jackson-databind กับ jackson-core โดย​ลืมตัว annotations ไป

การ​แปล​ผ่าน​ฉลุ​ย ไม่มี​คำ​เตือน​สัก​บรรทัด เพราะ Main.java ไม่​ได้​เอ่ย​ชื่อ annotation ตัว​ไหน​เลย

พอ​สั่ง​รัน​จริง​จึง​ได้​แบบ​นี้

Exception in thread "main" java.lang.NoClassDefFoundError: com/fasterxml/jackson/annotation/JsonSerializeAs
at tools.jackson.databind.introspect.JacksonAnnotationIntrospector.<clinit>(JacksonAnnotationIntrospector.java:39)
at tools.jackson.databind.cfg.MapperBuilder.<clinit>(MapperBuilder.java:50)
at tools.jackson.databind.json.JsonMapper.builder(JsonMapper.java:152)
at orderctl.Main.<clinit>(Main.java:8)

NoClassDefFoundError เป็น​ความ​ผิด​พลาด​ตอน​รัน​เสมอ · การ​แก้​ปัญหา​สาย​พึ่งพา​ให้​ครบ​โดย อัตโนมัติ คือ​สิ่ง​ที่​คุณ​จ่าย XML ยี่สิบ​บรรทัด​ไป​เพื่อ​ซื้อ

ไม่มี​ที่ไหน​ให้​เขียน​ชื่อ​คีย์​กำกับ ชื่อ component จึง​ต้อง​ตรง​กับ​คีย์​ใน JSON ตัว​ต่อ​ตัว

วา​งอ​อเด​อร์​ตัวอย่าง​ไว้​ที่ order.json ตรง​ราก​ของ​โปร​เจ​กต์ · คีย์​ทุก​ตัว​ใน​นี้​สะกด​ตรง​กับ​ชื่อ component ของ Order และ Line

{
"id": "ORD-1042",
"lines": [
{ "sku": "CFE-01", "qty": 2, "unitSatang": 6500 },
{ "sku": "TEA-03", "qty": 1, "unitSatang": 5500 }
]
}

unitSatang เก็บ​เงิน​เป็น​สตางค์​เหมือน​คอร์ส Go · กาแฟ​แก้ว​ละ 65 บาท​จึง​เขียน​เป็น 6500 ไม่ใช่ 65

รัน​ด้วย​คำ​สั่ง​นี้ · คำ​ว่า compile ตัด​ออก​ไม่​ได้ เพราะ​เป้า​หมาย exec:java ไม่​ผูก​กับ phase ไหน เลย มัน​จึง​ไม่​แปล​อะไร​ให้​ก่อน

$ mvn -q compile exec:java -Dexec.mainClass=orderctl.Main -Dexec.args=order.json
ORD-1042 รวม 18500 สตางค์

ครั้ง​แรก​จะ​ช้า​และ​มี​บรรทัด​ดาวน์โหลด​ไหล​ยาว เพราะ Maven ดึง plugin กับ jar ทั้งหมด​ลง ~/.m2 · ครั้ง​ต่อ​ไป​มัน​อ่าน​จาก​ที่​นั่น​แล้ว​เงียบ

readValue(path, Order.class) คือ data binding · มัน​ไม่​ได้​คืน tree ให้​เรา​เดิน​เอง มัน​สร้าง Order ตัว​จริง​พร้อม List<Line> ข้าง​ใน​ให้​เสร็จ

ลอง​แก้ qty ใน​บรรทัด​แรก​ให้​เป็น 0 แล้ว​รัน​ใหม่

$ mvn -q compile exec:java -Dexec.mainClass=orderctl.Main -Dexec.args=zero-qty.json
[ERROR] ... Cannot construct instance of `orderctl.Line`, problem: qty ต้องมากกว่า 0 แต่ได้ 0
[ERROR] at [Source: REDACTED ...] (through reference chain: orderctl.Order["lines"]->java.util.ArrayList[0])

ข้อความ​หลัง​คำ​ว่า problem: คือ​ข้อความ​ที่​เรา​เขียน​เอง​ใน​ตัว​ตรวจ​ของ Line · Jackson ไม่​ได้​ยัด ค่า​ลง component ตรงๆ มัน​เรียก constructor ตัว​จริง​ของ record

ผล​คือ Line ที่ qty เป็น 0 สร้าง​ไม่​ขึ้น ไม่​ว่า​ค่า​จะ​มา​จาก JSON จาก​เทสต์ หรือ​จาก new ใน​โปรแกรม · กฎ​อยู่​ที่​ตัว​ชนิด​เอง ไม่​ได้​อยู่​ที่​ผู้​เรียก

นี่​คือ​ข้อ​ที่ `encoding/json` ของ Go ทำ​ไม่​ได้

คอร์ส Go ก็​มี​ตัว​ตรวจ​ของ​โดเมน​เหมือน​กัน แต่​มัน​เป็น​ฟังก์ชัน​แยก​ที่​คน​อ่าน JSON ต้อง​เรียก​เอง

json.Unmarshal ยัด​ค่า​ลง struct ผ่าน reflection · struct ของ Go ไม่มี constructor จึง​ไม่มี​ที่ไหน ให้​แขวน​ตัว​ตรวจ​ไว้​ให้​มัน​ทำงาน​อัตโนมัติ

ลืม​เรียก​ตัว​ตรวจ​หนึ่ง​จุด ออ​เด​อร์​ที่​ผิด​กฎ​ก็​ไหล​เข้า​ระบบ​ไป​เรียบร้อย · ฝั่ง Java ลืม​ไม่​ได้ เพราะ​ไม่มี​ทาง​สร้าง Line ที่​ผิด​กฎ​ขึ้น​มา​ตั้งแต่​แรก

ข้อดี​ข้าง​บน​มา​พร้อม​ข้อ​เสีย​ที่​ต้อง​รู้ · ไม่มี annotation แปล​ว่า​ไม่มี​ที่​ให้​เขียน​ชื่อ​อื่น​กำกับ และ​ค่า​ปริยาย​ของ Jackson 3 เรื่อง​คีย์​แปลก​ปลอม​ก็​ไม่ใช่​อย่าง​ที่ tutorial เก่า​บอก

src/main/java/orderctl/KeyProbe.java
package orderctl;
import tools.jackson.databind.DeserializationFeature;
import tools.jackson.databind.json.JsonMapper;
public final class KeyProbe {
record Row(String sku, int qty) { }
static final String EXTRA = "{\"sku\":\"CFE-01\",\"qty\":2,\"note\":\"หวานน้อย\"}";
static final String MISSING = "{\"qty\":2}";
public static void main(String[] args) {
JsonMapper lenient = JsonMapper.builder().build();
JsonMapper strict = JsonMapper.builder()
.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.build();
System.out.println("คีย์เกิน ปริยาย : " + lenient.readValue(EXTRA, Row.class));
System.out.println("คีย์ขาด ปริยาย : " + lenient.readValue(MISSING, Row.class));
try {
strict.readValue(EXTRA, Row.class);
} catch (RuntimeException e) {
System.out.println("คีย์เกิน เข้มงวด : " + e.getClass().getSimpleName());
}
try {
System.out.println(lenient.readValue(MISSING, Row.class).sku().length());
} catch (NullPointerException e) {
System.out.println("ใช้ค่าที่ขาด : " + e.getMessage());
}
}
}
$ mvn -q compile exec:java -Dexec.mainClass=orderctl.KeyProbe
คีย์เกิน ปริยาย : Row[sku=CFE-01, qty=2]
คีย์ขาด ปริยาย : Row[sku=null, qty=2]
คีย์เกิน เข้มงวด : UnrecognizedPropertyException
ใช้ค่าที่ขาด : Cannot invoke "String.length()" because the return value of "orderctl.KeyProbe$Row.sku()" is null
  • คีย์ note ที่​ไม่มี​ใน Row ถูก​เมิน​เงียบ​ตาม​ค่า​ปริยาย​ของ Jackson 3
  • คีย์ sku ที่​ขาด​ไป ทำให้ component นั้น​เป็น null โดย​ไม่มี​ใคร​บ่น​อะไร
  • null ตัว​นั้น ระเบิด​ตอน​ถูก​ใช้ ห่าง​จาก​จุด​ที่​อ่าน JSON ไป​แล้ว​หลาย​บรรทัด

บรรทัด​สุดท้าย​บอก​ชื่อ type กับ​ชื่อ accessor ครบ · สอง​ชื่อ​นี้​อยู่​ใน bytecode อยู่​แล้ว วิธี​รัน​จึง​ไม่​เปลี่ยน​มัน ต่าง​จาก​ชื่อ​ตัวแปร​เฉพาะ​ที่​ของ​บท​ที่ 2 ที่​กลาย​เป็น <local1> เมื่อ​ไม่มี -g · ชื่อ KeyProbe$Row คือ​รูป​แบบ​เดียว​กับ Domain$Line ของ​บท​ที่​แล้ว

ค่า​ปริยาย​เรื่อง​คีย์​เกิน กลับ​ด้าน​ระหว่าง Jackson 2 กับ 3

tutorial แทบ​ทุก​อัน​บน​เน็ต​เขียน​บน Jackson 2 ซึ่ง​โยน UnrecognizedPropertyException ให้​คีย์​ที่​ไม่รู้​จัก ตั้งแต่​ค่า​ปริยาย

Jackson 3 พลิก​ค่า​ปริยาย​ของ FAIL_ON_UNKNOWN_PROPERTIES เป็น false · คีย์​ที่​พิมพ์​ผิด จึง​หาย​ไป​เงียบ แล้ว component นั้น​ได้​ค่า​เริ่ม​ต้น​ของ​ชนิด​แทน

ข้อ​นี้​ไม่ใช่​เรื่อง​เล็ก เพราะ​ฝั่ง Java ไม่มี struct tag ให้​เขียน​ชื่อ​คีย์​กำกับ · ชื่อ​คีย์​ใน JSON กับ​ชื่อ component จึง​ผูก​กัน​แน่น และ​การ​พิมพ์​ผิด​หนึ่ง​ตัว​อักษร​มี​ราคา​สูง​กว่า​ที่​ควร

สมมติ order.json เขียน​คีย์​เป็น unit แทน unitSatang ซึ่ง​เป็น​ชื่อ​ที่​คอร์ส Go ใช้ · Main.java ตัว​ปัจจุบัน​ตอบ​แบบ​นี้

$ mvn -q compile exec:java -Dexec.mainClass=orderctl.Main -Dexec.args=wrong-key.json
ORD-1042 รวม 0 สตางค์

ไม่มี​ความ​ผิด​พลาด มี​แต่​คำ​ตอบ​ที่​ผิด · unitSatang ไม่​เคย​ได้​ค่า มัน​จึง​เป็น 0 แล้ว​ยอด​รวม​ก็​เป็น 0

คอร์ส​นี้​จึง​เปิด​ความ​เข้ม​งวด​กลับ​มา​เอง​ที่ Main.java · แก้​บรรทัด MAPPER เป็น​แบบ​นี้ แล้ว​เติม import tools.jackson.databind.DeserializationFeature; ข้าง​บน

// ชิ้นส่วน ไม่ใช่โปรแกรมสมบูรณ์ · แทนที่บรรทัด MAPPER เดิมใน Main.java
static final JsonMapper MAPPER = JsonMapper.builder()
.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.build();

รัน​ซ้ำ​ด้วย file เดิม คราว​นี้​ได้​ข้อความ​ที่​ชี้​ตรง​จุด

$ mvn -q compile exec:java -Dexec.mainClass=orderctl.Main -Dexec.args=wrong-key.json
[ERROR] ... Unrecognized property "unit" (class orderctl.Line), not marked as ignorable
(3 known properties: "sku", "qty", "unitSatang")
[ERROR] at ... (through reference chain: orderctl.Order["lines"]->java.util.ArrayList[0]->orderctl.Line["unit"])

มัน​บอก​ชื่อ​คีย์​ที่​ไม่รู้​จัก บอก​ชนิด​ที่​กำลัง​สร้าง และ​ไล่​ราย​ชื่อ​คีย์​ที่​รู้จัก​มา​ให้​ครบ · นี่​คือ​ความ​ต่าง​ระหว่าง​ตอบ​ผิด​เงียบ กับ​ตอบ​ว่า​ไม่รู้

ฝั่ง​หนึ่ง​จ่าย​ด้วย​เวลา​ตั้ง​ค่า อีก​ฝั่ง​จ่าย​ด้วย​ขั้น​ตอน​ตรวจ​ที่​คน​เรียก​ลืม​ได้

คอร์ส Go เปิด​โปร​เจ​กต์​ด้วย go mod init example.com/orderctl บรรทัด​เดียว แล้ว​ได้ go.mod สั้นๆ ที่​ราก · ไม่มี XML ไม่​ต้อง​เลือก library และ​ไม่​ต้อง​รู้จัก transitive dependency เลย

ราคา​ที่ Java จ่าย คือ​ครึ่ง​แรก​ของ​บท​นี้ · หัวข้อ​ทั้งหมด​ตั้งแต่ pom ถึง jar สาม​ใบ ไม่มี​อะไร เกี่ยว​กับ​ภาษา​เลย มัน​คือ​โครง​ไดเรกทอรี XML และ​การ​แก้​ปัญหา​สาย​พึ่งพา

ราคา​ที่ Go จ่าย อยู่​ที่​การ​ตรวจ​ความ​ถูก​ต้อง · struct ไม่มี constructor ตัว​ตรวจ​จึง​เป็น​ขั้น​ตอน แยก​ที่​คน​เรียก​ต้อง​จำ และ​คน​ที่​ลืม​จะ​ไม่มี​อะไร​มา​เตือน

คอร์ส Go บท​ที่ 3คอร์ส​นี้
เริ่ม​โปร​เจ​กต์go mod init หนึ่ง​บรรทัดpom ยี่สิบ​บรรทัด​ที่​พิมพ์​เอง
library สำหรับ JSONencoding/json มา​ใน​กล่องเลือก​เอง แล้ว​ประกาศ​ใน pom
จับ​คู่​คีย์​กับ fieldstruct tag เขียน​ชื่อ​คีย์​กำกับ​ได้​อิสระชื่อ component ต้อง​ตรง​กับ​คีย์
คีย์​แปลก​ปลอมค่า​ปริยาย​ข้าม​ให้ · ต้อง​ย้าย​ไป json.NewDecoder จึง​ห้าม​ได้ค่า​ปริยาย​ข้าม​ให้ · เปิด FAIL_ON_UNKNOWN_PROPERTIES ที่ mapper เดิม​จึง​ห้าม​ได้
ตัว​ตรวจ​ตอน​แปลงไม่มี ต้อง​เรียก​เอง​แยก​ขั้น​ตอนconstructor ของ record ถูก​เรียก​ทุก​ครั้ง
ความ​ลึก​ของ​โครงinternal/order/ สอง​ชั้นsrc/main/java/orderctl/ สี่​ชั้น

แถว​สุดท้าย​คือ​เรื่อง​ความ​รำคาญ · แถว​ก่อน​สุดท้าย​คือ​เรื่อง​ที่​ทำให้​ระบบ​พัง​ตอน​ตี​สาม · สอง​แถว​นี้​ไม่​ได้​มี​น้ำ​หนัก​เท่า​กัน และ​คอร์ส​นี้​จะ​ไม่​แกล้ง​ทำ​เป็น​ว่า​มัน​เท่า​กัน

แถว​ที่​สาม​กลับ​ด้าน​กัน · Go ยอม​ให้​ชื่อ field กับ​ชื่อ​คีย์​ต่าง​กัน​ได้ แลก​กับ tag ที่​พิมพ์​ผิด​แล้ว เงียบ ส่วน Java ไม่มี​อะไร​ให้​พิมพ์​ผิด แลก​กับ​การ​ที่​ชื่อ​ใน​โดเมน​ถูก​ล็อก​กับ​ชื่อ​คีย์​ใน JSON

jar ที่​ได้​โดย​ปริยาย​ขาด​สอง​อย่าง คือ​จุด​เริ่ม​ของ​โปรแกรม และ dependency ทั้ง​สาม​ใบ

ข้อ​ร้อง​เรียน​ข้อ​ที่​สอง​ของ​หัวข้อ​แรก​คือ “ไม่มี​อะไร​เหลือ​ไว้​ให้​ส่ง​ต่อ” · Maven แก้​ข้อ​นั้น​ด้วย คำ​สั่ง​เดียว

$ mvn -q package
$ ls -l target/orderctl-1.0.0.jar
-rw-r--r-- 1 nook wheel 5212 Aug 27 15:51 target/orderctl-1.0.0.jar

ห้า​กิโล​ไบต์​เศษ วัด​จาก​โปร​เจ​กต์​ที่​มี Main Order Line และ Discount · ถ้า​คุณ​ยัง​เก็บ KeyProbe.java ไว้​ด้วย ตัวเลข​จะ​โตก​ว่า​นี้

ลอง​รัน​ดู​ตรงๆ

$ java -jar target/orderctl-1.0.0.jar order.json
no main manifest attribute, in target/orderctl-1.0.0.jar

ด่าน​แรก​คือ jar ไม่รู้​ว่า​จะ​เริ่ม​ที่ class ไหน · ข้อมูล​นั้น​อยู่​ใน​รายการ​คุณสมบัติ​ของ jar ซึ่ง​ต้อง​สั่ง​ให้ Maven ใส่​ให้

ด่าน​ที่​สอง​รอ​อยู่​ข้าง​หลัง · เลี่ยง​ด่าน​แรก​ด้วย​การ​ชี้ class path ไป​ที่ target/classes ตรงๆ แล้ว​ยัง​เจอ​แบบ​นี้

$ java -cp target/classes orderctl.Main order.json
Exception in thread "main" java.lang.NoClassDefFoundError: tools/jackson/databind/json/JsonMapper

jar ที่ Maven ห่อ​ให้​โดย​ปริยาย ไม่มี dependency อยู่​ข้าง​ใน · สาม​ใบ​ที่​เห็น​ใน dependency:tree ยัง​นอน​อยู่​ที่ ~/.m2 และ​ไม่​ได้​ตาม​ไป​ด้วย

เติม​ก้อน build เข้าไป​ใน pom.xml · pom โต​จาก​ยี่สิบ​บรรทัด​ต้น​เป็น​สามสิบ​เจ็ด​บรรทัด

<build>
<plugins>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.5.0</version>
<configuration>
<archive>
<manifest>
<mainClass>orderctl.Main</mainClass>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>

mainClass ปิด​ด่าน​แรก · addClasspath กับ classpathPrefix เขียน​ราย​ชื่อ jar ที่​ต้อง​ใช้ ลง​ใน​รายการ​คุณสมบัติ โดย​ชี้​ไป​ที่​ไดเรกทอรี lib/ ข้างๆ

บรรทัด version ไม่ใช่​ของ​แถม · plugin ที่​ประกาศ​ไว้​ใน build โดย​ไม่​บอก​รุ่น ทำให้ Maven เตือน​ว่า​ผล​ของ build ไม่​นิ่ง และ​ปล่อย​ให้​รุ่น​ของ plugin เปลี่ยน​เอง​ใน​วัน​ข้าง​หน้า

เหลือ​อีก​อย่าง​เดียว​คือ​เอา jar สาม​ใบ​มา​วาง​ไว้​ที่ lib/ จริง

$ mvn -q package dependency:copy-dependencies -DoutputDirectory=target/lib
$ ls target/lib
jackson-annotations-2.22.jar jackson-core-3.2.2.jar jackson-databind-3.2.2.jar
$ java -jar target/orderctl-1.0.0.jar order.json
ORD-1042 รวม 18500 สตางค์

รายการ​คุณสมบัติ​ที่​ได้​หน้าตา​แบบ​นี้ · บรรทัด Class-Path คือ​สิ่ง​ที่ addClasspath เขียน​ให้

$ unzip -p target/orderctl-1.0.0.jar META-INF/MANIFEST.MF
Manifest-Version: 1.0
Created-By: Maven JAR Plugin 3.5.0
Java-Version: 25
Build-Jdk-Spec: 25
Class-Path: lib/jackson-databind-3.2.2.jar lib/jackson-annotations-2.22.
jar lib/jackson-core-3.2.2.jar
Main-Class: orderctl.Main

ทาง​นี้​ได้​ของ​ที่​ส่ง​ต่อ​ได้​จริง แต่​ยัง​ไม่ใช่​ของ​ชิ้น​เดียว · ย้าย lib/ ออก​ไป​แล้ว​มัน​กลับ​ไป​พัง ที่​เดิม​ทันที

$ mv target/lib /tmp/libhold
$ java -jar target/orderctl-1.0.0.jar order.json
Exception in thread "main" java.lang.NoClassDefFoundError: tools/jackson/databind/json/JsonMapper
ทางได้​อะไรจ่าย​อะไร
jar กับ lib/ — ทาง​ที่​บท​นี้​ใช้jar เล็ก แก้ dependency ที​ละ​ใบ​ได้ต้อง​ขน​ทั้ง​ไดเรกทอรี​ไป​ด้วย และ​ปลาย​ทาง​ต้อง​มี JVM
uber jar ด้วย maven-shade-pluginfile เดียว​จบต้อง​เพิ่ม plugin อีก​ตัว · ปลาย​ทาง​ยัง​ต้อง​มี JVM · สอง jar ที่​มี resource ชื่อ​ซ้ำ​จะ​ทับ​กัน ถ้า​ไม่​ตั้ง​ตัว​รวม​ให้
jlinkruntime ที่​รัน​ได้​โดย​ปลาย​ทาง​ไม่​ต้อง​มี JVMบังคับ​ให้​ทุก​ชิ้น​ใน​สาย​เป็น module จริง ซึ่ง​คอร์ส​นี้​เลี่ยง​ไว้​ตั้งแต่​ต้น​ด้วย​การ​อยู่​บน class path
นี่​คือ​แถว​สุดท้าย​ของ​ตาราง​ใน​บท​ที่ 1

บท​ที่ 1 มี​ตาราง​ห้า​แถว​ที่​บอก​ว่า Go กับ Java เลือก​ทาง​ตรง​ข้าม​กัน​ตรง​ไหน · แถว​สุดท้าย​เขียน​ว่า Go ได้​โปรแกรม​เดียว​ที่​รัน​บน​เครื่อง​เปล่า​ได้ ส่วน Java ต้อง​มี JVM รอ​อยู่​ปลาย​ทาง

หัวข้อ​นี้​คือ​หน้าตา​จริง​ของ​แถว​นั้น · go build คาย​ของ​ชิ้น​เดียว​ที่ copy ไป​แล้ว​รัน​ได้​ทันที ส่วน​ฝั่ง​นี้​ต้อง​เติม pom อีก​สิบ​เจ็ด​บรรทัด บวก​อีก​หนึ่ง​คำ​สั่ง แล้ว​ยัง​ได้​ของ​สอง​ก้อน​อยู่ดี

ทาง​ที่​สาม​ใน​ตาราง​ข้าง​บน​ปิด​ช่อง​ว่าง​นั้น​ได้​จริง แต่​มัน​เปลี่ยน​กติกา​ของ​ทั้ง​โปร​เจ​กต์ · คอร์ส​นี้​เลือก​อยู่​บน class path ตลอด จึง​จ่าย​ราคา​ข้อ​นี้​ไป​แลก​กับ​ความ​เรียบ​ง่าย​ของ​ทุก​บท​ที่​เหลือ

orderctl มี​โครง​จริง มี dependency จริง และ​อ่า​นอ​อเด​อร์​จาก JSON ได้​แล้ว

คุณ​เขียน pom.xml เอง​ทุก​บรรทัด ย้าย type ของ​บท​ที่​แล้ว​เข้า​โครง​ของ Maven และ​รู้​แล้ว​ว่า maven.compiler.release เป็น​บรรทัด​ที่​ตัด​ออก​ไม่​ได้

คุณ​เห็น​แล้ว​ว่า​หนึ่ง​บรรทัด​ใน pom ลาก​อะไร​ลง​มา​บ้าง และ​เห็น​ว่า record รับ JSON ได้​โดย​ไม่​ต้อง ติด annotation แต่​แลก​มา​ด้วย​ข้อ​บังคับ​ว่า​ชื่อ component ต้อง​ตรง​กับ​คีย์

และ​คุณ​ห่อ orderctl เป็น jar ที่ java -jar รัน​ได้​จริง​แล้ว พร้อม​รู้​ว่า​มัน​ยัง​ไม่ใช่​ของ​ชิ้น​เดียว · อีก​สอง​ทาง​ที่​ปิด​ช่อง​ว่าง​นั้น​ได้ อยู่​ใน​ตาราง​ของ​หัวข้อ​ที่​แล้ว

บท​หน้า​เรา​เลิก​เขียน​ลู​ปด้ว​ยมือ · grossSatang() ที่​วน​บวก​ที​ละ​แถว​จะ​กลาย​เป็น​บรรทัด​เดียว แล้ว​เรา​จะ​ดู​ว่า​บรรทัด​เดียว​นั้น​ซ่อน​อะไร​ไว้ และ​เมื่อ​ไร​ที่​ลูป​ธรรมดา​ยัง​ชนะ​อยู่


🔗 อ้างอิง​ต้นทาง​ของ​บท​นี้

บท​นี้​อิง​ต้นทาง​ที่​ลง​วัน​ที่​กำกับ อ่าน​ต่อ​ได้​โดยตรง:

  • JEP 458: Launch Multi-File Source-Code Programs (เข้า​ถึง 2026-08-27) — class ที่มา​จาก class path อ้าง​ถึง class ที่​แปล​ไว้​ใน​หน่วย​ความ​จำ​ไม่​ได้ และ​ตัว​รัน​เลือก​เอง​ว่า​จะ​แปล​ตอน​ไหน
  • JEP 540: Simple JSON API (Incubator) (เข้า​ถึง 2026-08-27) — สถานะ Targeted ที่ Release 28 · หัวข้อ Goals เขียน​เอง​ว่า​เลี่ยง data binding
  • Apache Maven downloads (เข้า​ถึง 2026-08-27) — 3.9.16 คือ​รุ่น​ที่​แนะนำ​สำหรับ​ผู้​ใช้​ทุก​คน
  • Introduction to the Standard Directory Layout (Last Published 2026-08-24) — ที่มา​ของ src/main/java และ src/main/resources
  • maven-compiler-plugin compile mojo (Version 3.15.0, Last Published 2026-01-27) — source กับ target มี​ค่า​เริ่ม​ต้น 1.8 ส่วน release ไม่มี​ค่า​เริ่ม​ต้น
  • exec-maven-plugin java mojo (เข้า​ถึง 2026-08-27) — เป้า​หมาย​นี้​ไม่​ผูก​กับ phase ไหน​ของ​วงจร build
  • Jackson Release 3.0 notes (เข้า​ถึง 2026-08-27) — การ​ย้าย groupId ไป tools.jackson.core และ​ค่า​ปริยาย​ที่​เปลี่ยน
  • jackson-databind บน Maven Central (เข้า​ถึง 2026-08-27) — รุ่น​ที่​ปล่อย​ล่าสุด​คือ 3.2.2

เช็กความเข้าใจ — บทที่ 3

ข้อ 1 / 3

ถ้าลบบรรทัด `maven.compiler.release` ออกจาก pom แล้วสั่ง `mvn -q compile` จะเกิดอะไรขึ้น