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

สลับ​สอง​บรรทัด​ใน pom.xml แล้ว build ยัง​เขียว แต่​โปรแกรม​พิมพ์​ได้​บรรทัด​เดียว

คุณ​เปิด pom.xml แล้ว​สลับ​ที่ <dependency> สอง​ก้อน เพราะ​อยาก​ให้​มัน​เรียง​ตาม​ตัว​อักษร คุณ​ไม่​ได้​แตะ​รุ่น​สัก​ตัว ไม่​ได้​ลบ​อะไร ไม่​ได้​เพิ่ม​อะไร

mvn compile ยัง​เขียว รหัส​ออก 0 เหมือน​เดิม​ทั้ง​ก่อน​และ​หลัง

แล้ว​โปรแกรม​ที่​เคย​พิมพ์ 2 บรรทัด พิมพ์​ได้ 1 บรรทัด แล้ว​ตาย​ด้วย NoSuchMethodError

หน้า​นี้​เอา​กราฟ dependency ใบ​เดียว ที่​มี library ตัว​หนึ่ง​ถูก​ขอ​สอง​รุ่น​จาก​สอง​เส้น​ทาง ไป​ให้ resolver ตัว​จริง​สอง​ตัว​ตัดสิน แล้ว​วัด​ว่า​ใคร​ได้​อะไร​มา​บน classpath

ธี​สิ​ส​ของ​หน้า​นี้​อยู่​ตรง​นี้ — ทั้ง​สอง​ตัว​เลือก​ให้​คุณ​เงียบ ๆ อยู่​แล้ว และ​กฎ​ที่​มัน​ใช้​เลือก ไม่ใช่​กฎ​เดียวกัน สิ่ง​ที่ Maven เรียก​ว่า​คำ​ตอบ​ที่​ถูก คือ​สิ่ง​ที่ Gradle ทิ้ง และ​บรรทัด​ที่​คุณ​เขียน​เอง​เพื่อ​สั่ง​มัน ฝั่ง​หนึ่ง​เชื่อ​ฟัง อีก​ฝั่ง​เขียน​ทับ

กราฟ​ที่​ใช้​วัด มี​สี่ artifact และ​เส้น​ทาง​สอง​เส้น

หัวข้อ​ที่​มีชื่อ​ว่า “กราฟ​ที่​ใช้​วัด มี​สี่ artifact และ​เส้น​ทาง​สอง​เส้น”

ทำไม​ถึง​ต้อง​สร้าง library ปลอม​ขึ้น​มา​เอง แทนที่​จะ​หยิบ​ของ​จริง​จาก Maven Central มา​วัด

graph TD
  app["app<br/>โปรแกรมของคุณ"] --> liba["liba 1.0.0"]
  app --> libb["libb 1.0.0"]
  liba --> core1["core 1.0.0<br/>มี name()"]
  libb --> core2["core 2.0.0<br/>มี name() และ extra()"]

liba ขอ core รุ่น 1.0.0 ส่วน libb ขอ​รุ่น 2.0.0 ทั้ง​คู่​อยู่​ลึก​เท่า​กัน​คือ​สอง​ชั้น และ app ไม่​ได้​เอ่ย​ถึง core เลย​สัก​บรรทัด

core รุ่น 2.0.0 เพิ่ม method ชื่อ extra() ที่​รุ่น 1.0.0 ไม่มี และ libb เรียก method นั้น​อยู่​ข้าง​ใน มัน​จึง​เป็นการ​เปลี่ยน​ที่​เข้า​กัน​ไม่​ได้ที่​ระดับ binary

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

วัด​จาก​อะไร

ทุก​ค่า​ใน​หน้า​นี้​มา​จาก scripts/dep-conflict/spike.py ที่​รัน resolver ตัว​จริง​ทั้ง​สอง​ฝั่ง บน Apache Maven 3.9.16 (2bdd9fddda4b155ebf8000e807eb73fd829a51d5) · Gradle 9.7.1 · openjdk version "25.0.4.1" 2026-08-18

หน่วย​ของ​หลัก​ฐาน​ทั้ง​หน้า​คือ​สตริง​รุ่น​ที่ resolver เลือก รหัส​ออก​ของ build รหัส​ออก​ของ​โปรแกรม ชื่อ class ของ​ข้อ​ยกเว้น จำนวน jar และ​จำนวน​บรรทัด ไม่มี​หน่วย​เวลา​สัก​ช่อง เครื่อง​ช้า​จึง​เปลี่ยน​คำ​ตอบ​ไม่​ได้ · probe รัน​ชุด​เดิม 3 รอบ​ต่อ​การ​เรียก​หนึ่ง​ครั้ง และ​แดง​ถ้า​มี​ค่า​ไหน​ขยับ

Maven เลือก​ตัว​ที่​ใกล้​ที่สุด และ​เมื่อ​เสมอ​กัน มัน​เลือก​ตัว​ที่​ประกาศ​ก่อน

หัวข้อ​ที่​มีชื่อ​ว่า “Maven เลือก​ตัว​ที่​ใกล้​ที่สุด และ​เมื่อ​เสมอ​กัน มัน​เลือก​ตัว​ที่​ประกาศ​ก่อน”

ลำดับ​ของ <dependency> สอง​ก้อน​ตัดสิน​ว่า jar ใบ​ไหน​ขึ้น classpath

ทั้ง​สอง​เส้น​ทาง​อยู่​ลึก​สอง​ชั้น​เท่า​กัน กฎ nearest-wins จึง​เสมอ และ​ตัว​ที่​ตัดสิน​การ​เสมอ​คือ​ลำดับ​ที่​คุณ​พิมพ์

ประกาศ​ตัว​ไหน​ก่อนcore ที่​ได้รหัส​ออก​ของ buildรหัส​ออก​ของ​โปรแกรม
liba1.0.001
libb2.0.000

pom.xml สอง​ใบ​นี้​ต่าง​กัน​ที่​การ​เรียง​ของ​สอง​ก้อน​เท่านั้น รุ่น​ที่​ประกาศ​เหมือน​กัน​ทุก​ตัว

จำนวน jar บน classpath เท่า​กัน​ทั้ง​สอง​ทรง คือ 3 ใบ ปัญหา​จึง​ไม่​ได้​อยู่​ที่​จำนวน มัน​อยู่​ที่​ตัว​ตน

liba ถูก​คอม​ไพล์​ครั้ง​เดียว แล้ว​ไม่มี​ใคร​แก้​มัน​อีก แต่​บรรทัด​ที่​มัน​พิมพ์​ออก​มา​คนละ​บรรทัด

ประกาศ​ตัว​ไหน​ก่อนบรรทัด​แรก​ที่​ได้
libaA sees core-1.0.0
libbA sees core-2.0.0

liba ถูก​สร้าง​ขึ้น​โดย​เห็น core รุ่น 1.0.0 แต่​ตอน​รัน​มัน​ได้​รุ่น 2.0.0 มา​แทน แล้ว​มัน​ก็​ทำงาน​ต่อ​ไป​เงียบ ๆ ด้วย​พฤติกรรม​ของ​รุ่น​ที่​มัน​ไม่​เคย​เห็น

ส่วน libb ไม่​ได้​โชค​ดี​แบบ​นั้น มัน​เรียก extra() ซึ่ง​รุ่น 1.0.0 ไม่มี ทรง​แรก​จึง​พิมพ์​ได้​บรรทัด​เดียว​แล้ว​ตาย​ด้วย NoSuchMethodError

ความ​เงียบ​คือ​ส่วน​ที่​แพง​ที่สุด

mvn compile ใน​ทรง​ที่​ชน​กัน คาย​คำ​เตือน​ที่​เอ่ย​ถึง core ออก​มา 0 บรรทัด

การ​เลือก​เกิด​ขึ้น​แล้ว รุ่น​หนึ่ง​ถูก​ทิ้ง​แล้ว และ​ไม่มี​อะไร​บน​หน้า​จอบ​อก​ว่า​มัน​เกิด​ขึ้น

กราฟ​ใบ​เดียวกัน สลับ​ลำดับ​เหมือน​กัน แต่​คำ​ตอบ​ไม่​ขยับ

ประกาศ​ตัว​ไหน​ก่อนcore ที่ Maven เลือกcore ที่ Gradle เลือก
liba1.0.02.0.0
libb2.0.02.0.0

Gradle ไม่​สนใจ​ว่า​ใคร​อยู่​ใกล้​กว่า​หรือ​ใคร​ถูก​พิมพ์​ก่อน มัน​เอา​รุ่น​ที่​สูง​ที่สุด​ที่​มี​คน​ขอ

กฎ​นี้​ทำให้​ผล​นิ่ง​กว่า แต่​มัน​ไม่​ได้​แปล​ว่า​ปลอดภัย​กว่า มัน​แปล​ว่า​คุณ​ถูก​อัป​เกรด​โดย​ไม่​ได้​ขอ liba ที่​เขียน​ไว้​สำหรับ​รุ่น 1.0.0 ได้​รุ่น 2.0.0 ไป​ทุก​ครั้ง

บรรทัด​ที่​คุณ​เขียน​เอง ฝั่ง​หนึ่ง​เชื่อ​ฟัง อีก​ฝั่ง​เขียน​ทับ

หัวข้อ​ที่​มีชื่อ​ว่า “บรรทัด​ที่​คุณ​เขียน​เอง ฝั่ง​หนึ่ง​เชื่อ​ฟัง อีก​ฝั่ง​เขียน​ทับ”

เติม core รุ่น 1.0.0 เข้าไป​ตรง ๆ ที่​ชั้น​บน​สุด แล้ว​ดู​ว่า​ใคร​ฟัง​คุณ

นี่​คือ​สิ่ง​ที่​คน​ส่วน​ใหญ่​ทำ​เมื่อ​รู้​ว่า​มี​การ​ชน​กัน คือ​ประกาศ​รุ่น​ที่​ต้องการ​เอง​ที่​ระดับ​บน​สุด เพราะ​มัน​ดู​เหมือน​คำ​สั่ง​ที่​ชัด​ที่สุด​เท่า​ที่​จะ​เขียน​ได้

เครื่อง​มือบรรทัด​ที่​เขียนcore ที่​ได้รหัส​ออก​ของ​โปรแกรม
Maven<dependency> ของ core รุ่น 1.0.01.0.01
Gradleimplementation("...core:1.0.0")2.0.00

Maven เชื่อ​ฟัง เพราะ​บรรทัด​นั้น​อยู่​ลึก​ชั้น​เดียว ซึ่ง​ใกล้​กว่า​ทุก​เส้น​ทาง มัน​จึง​ชนะ​แม้แต่ libb ที่​ประกาศ​ไว้​ก่อน​หน้า​มัน

Gradle เขียน​ทับ เพราะ​กฎ​ของ​มัน​ไม่​ได้​ดู​ว่า​ใคร​อยู่​ใกล้ มัน​ดู​ว่า​ใคร​สูง และ 2.0.0 ที่ libb ขอ ยัง​สูง​กว่า​บรรทัด​ที่​คุณ​เพิ่ง​พิมพ์

ผล​ที่​ตาม​มาก​ลับ​ด้าน​กับ​สัญชาตญาณ ฝั่ง​ที่​เชื่อ​ฟัง​คุณ​คือ​ฝั่ง​ที่​ตาย​ตอน​รัน ส่วน​ฝั่ง​ที่​เมิน​บรรทัด​ของ​คุณ​คือ​ฝั่ง​ที่​รัน​จบ 2 บรรทัด​ตาม​ปกติ

Gradle มี​รูป​เขียน​ที่​แปล​ว่า “รุ่น​นี้​เท่านั้น” คือ​เติม !! ท้าย​รุ่น

ผู้​เขียน probe ทำนาย​ไว้​ก่อน​วัด​ว่า​มัน​จะ​ทำให้ build แดง เพราะ​มัน​ขัด​กับ​รุ่น​ที่ libb ขอ คำ​ทำนาย​นั้น​ผิด

ทรงรหัส​ออก​ของ buildcore ที่​ได้รหัส​ออก​ของ​โปรแกรม
core:1.0.002.0.00
core:1.0.0!!01.0.01

build ยัง​เขียว รหัส​ออก 0 เท่า​เดิม resolver ลด​รุ่น​ที่ libb ขอ​ไว้​ลง​มา​ให้​เงียบ ๆ แล้ว​โปรแกรม​ตาย​ด้วย NoSuchMethodError

สอง​อักขระ​นี้​พลิก​ผล​จาก​รัน​จบ​เป็น​ตาย​ตอน​รัน โดย​ไม่มี​อย่าง​อื่น​ใน​ทั้ง​โปร​เจ​กต์​เปลี่ยน​เลย

สอง​ฝั่ง​มี​ทาง​บังคับ​ที่​ใช้ได้​จริง และ​ฝั่ง​หนึ่ง​มี​ทาง​ทำให้​มัน​แดง​ก่อน​ถึง​ตอน​รัน

Maven มี dependencyManagement ซึ่ง​อยู่​คนละ​ชั้น​กับ​การ​ประกาศ​ธรรมดา มัน​ไม่​ได้​แข่ง​เรื่อง​ความ​ใกล้ มัน​ตั้ง​รุ่น​ให้​ทุก​เส้น​ทาง​ที่​เอ่ย​ถึง core

ทางcore ที่​ได้รหัส​ออก​ของ​โปรแกรม
Maven · dependencyManagement ตรึง 2.0.02.0.00
Gradle · resolutionStrategy.force ตรึง 1.0.01.0.01

ทั้ง​สอง​ทาง​บังคับ​ได้​จริง แต่​สังเกต​ว่า​ทั้ง​สอง​ทาง​ก็​ยัง​เงียบ​เหมือน​เดิม มัน​เปลี่ยน​คำ​ตอบ ไม่​ได้​เปลี่ยน​ว่า​คุณ​จะ​รู้​หรือ​ไม่รู้

Gradle มี​อีก​สวิตช์​ที่​ต่าง​ออก​ไป คือ failOnVersionConflict() ซึ่ง​ไม่​เลือก​ให้​เลย

สิ่ง​ที่​วัดค่าที่​ได้
รหัส​ออก​ของ build1
jar ที่ resolve ได้0
ข้อความ​เอ่ย​ถึง​ทั้ง​สอง​รุ่นใช่

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

เครื่อง​มือ​ที่​คุณ​ใช้​ตอบ​คำถาม​นี้ ไม่​ตอบ​โดย​ไม่​ถูก​ถาม

หัวข้อ​ที่​มีชื่อ​ว่า “เครื่อง​มือ​ที่​คุณ​ใช้​ตอบ​คำถาม​นี้ ไม่​ตอบ​โดย​ไม่​ถูก​ถาม”

mvn dependency:tree พิมพ์​ต้นไม้​ที่​ดู​ครบ และ​ตัว​ที่​แพ้​ไม่​ได้​อยู่​ใน​นั้น

คำ​สั่ง​แรก​ที่​ทุก​คน​พิมพ์​เมื่อ​สงสัย​เรื่อง​รุ่น​ที่​ชน​กัน คือ mvn dependency:tree

สิ่ง​ที่​วัดค่า​ปริยายเติม -Dverbose
ปม​ที่​พิมพ์33
บรรทัด​ที่​บอก​ว่า​มี​การ​ชน​กัน01

ค่า​ปริยาย​พิมพ์ 3 ปม และ​ไม่มี​ปม​ไหน​บอก​ว่า​มี​รุ่น​ที่​สอง​อยู่ ต้นไม้​ที่​ได้​จึง​อ่าน​เหมือน​กราฟ​ที่​ไม่มี​การ​ชน​กัน​เลย

เติม​ธง​เดียว​แล้ว​บรรทัด​นั้น​โผล่​มา​ทันที พร้อม​คำ​ว่า omitted for conflict with 1.0.0

ฝั่ง Gradle ไม่มี​ธง​ให้​เติม gradle dependencies พิมพ์ 1.0.0 -> 2.0.0 ให้​เห็น​ใน​คำ​สั่ง​เดียวกัน

สิ่ง​ที่​ควร​จำ​จาก​หน้า​นี้

หนึ่ง ทั้ง​สอง resolver เลือก​ให้​คุณ​เสมอ คำถาม​ไม่ใช่​ว่า​มัน​เลือก​ไหม แต่​เลือก​ด้วย​กฎ​อะไร

สอง Maven ดู​ความ​ใกล้ แล้ว​ตัดสิน​การ​เสมอ​ด้วย​ลำดับ​ที่​พิมพ์ · Gradle ดู​รุ่น​ที่​สูง​ที่สุด

สาม การ​ประกาศ​รุ่น​ตรง ๆ ที่​ชั้น​บน​สุด เป็น​คำ​สั่ง​สำหรับ Maven แต่​เป็น​แค่​ข้อ​เสนอ​สำหรับ Gradle

สี่ ทาง​ที่​บังคับ​ได้​จริง​คือ dependencyManagement กับ resolutionStrategy.force ส่วน​ทาง​ที่​ทำให้​คุณ รู้ มี​ทาง​เดียว​คือ failOnVersionConflict()

ทุก​ช่อง​ใน​ตาราง​นี้​มา​จาก probe ตัว​เดียวกัน และ​ซ้ำ​ได้ที​ละ​อักขระ

ทรงเครื่อง​มือcore ที่​ได้buildรันข้อ​ยกเว้น
liba ก่อนMaven1.0.001NoSuchMethodError
libb ก่อนMaven2.0.000—
ประกาศ core ตรง ๆMaven1.0.001NoSuchMethodError
dependencyManagementMaven2.0.000—
liba ก่อนGradle2.0.000—
libb ก่อนGradle2.0.000—
ประกาศ core ตรง ๆGradle2.0.000—
forceGradle1.0.001NoSuchMethodError
core:1.0.0!!Gradle1.0.001NoSuchMethodError
failOnVersionConflictGradle—11—

เช็กความเข้าใจ

ข้อ 1 / 3

pom.xml ประกาศ liba แล้วตามด้วย libb ทั้งคู่อยู่ลึกสองชั้นและขอ core คนละรุ่น สลับสองก้อนนี้แล้วเกิดอะไรขึ้น

ทุก​ค่า​ใน​หน้า​นี้​สร้าง​ใหม่​ได้​จาก​คำ​สั่ง​สอง​บรรทัด

Terminal window
scripts/dep-conflict/up.sh # อุ่น repo ที่แยกไว้ · ต้องใช้เน็ตรอบแรก
npm run verify:dep-conflict # วัด 10 ทรง 3 รอบ แล้วเทียบกับข้ออ้างทุกข้อ

probe ไม่​แตะ ~/.m2 และ ~/.gradle ของ​เครื่อง​คุณ มัน​วาง repo ของ​ตัว​เอง​ไว้​ที่ scripts/dep-conflict/.tools/ ทั้งหมด

รหัส​ออก 1 แปล​ว่า​ข้อ​อ้าง​ใน​หน้า​นี้​ผิด ส่วน​รหัส​ออก 2 แปล​ว่า​วัด​ไม่​ได้ เช่น​หา JDK ไม่​เจอ หรือ​ดึง plugin ของ Maven ไม่​ได้ สอง​อย่าง​นี้​ไม่​เหมือน​กัน

scripts/dep-conflict/README-predictions.md เก็บ​คำ​ทำนาย​ที่​เขียน​ไว้​ก่อน​วัด​รอบ​แรก พร้อม​ข้อ​ที่​ถูก​หัก​ล้าง