บทความที่ 4 : กับดักของแคชการกำหนดค่า Gradle
เผยแพร่เมื่อ 26 September 2025
บทนำ
ในการผจญภัยสร้างปลั๊กอินของเรา`site-baker`, เราได้ปฏิบัติตามแนวทาง TDD ที่เข้มงวด ทุกฟีเจอร์ได้รับการทดสอบและตรวจสอบความถูกต้อง และเราเดินหน้าต่อด้วยความมั่นใจ และแล้ววันหนึ่ง สิ่งที่ไม่คาดคิดก็เกิดขึ้น การบิลด์เริ่มมีพฤติกรรมเอาแน่เอานอนไม่ได้ การแก้ไขในลอจิกของปลั๊กอินหรือในไฟล์กำหนดค่าดูเหมือนจะถูกละเลย และการทดสอบเชิงฟังก์ชันของเรา ซึ่งเคยเชื่อถือได้ กลับล้มเหลวโดยไม่มีสาเหตุที่ชัดเจน
ปัญหาลักษณะนี้อาจทำให้หงุดหงิดได้อย่างเหลือเชื่อ มันทำให้เกิดคำถามถึงความน่าเชื่อถือของเครื่องมือและความถูกต้องของโค้ดของเรา หลังจากช่วงการดีบักอย่างหนักหน่วง ผู้รับผิดชอบถูกระบุตัวแล้ว:แคชการกำหนดค่าของ Gradle.
อาการ : บิลด์ผี
ปัญหานี้แสดงออกในหลายรูปแบบ:
-
ฉันกำลังแก้ไขสตริงอักขระในงานหนึ่ง`println`, แต่สตริงเก่ายังคงแสดงผลเมื่อรัน
-
ฉันกำลังเปลี่ยนค่าในไฟล์ของฉัน`managed-jbake-context.yml`, แต่ปลั๊กอินทำงานราวกับว่าไฟล์ไม่ได้ถูกแก้ไข
-
การทดสอบเชิงฟังก์ชัน ซึ่งสร้างโปรเจกต์ทดสอบขึ้นมาแบบทันที กำลังล้มเหลว เพราะปลั๊กอินดูเหมือนจะตรวจไม่พบไฟล์การกำหนดค่าที่เพิ่งสร้างขึ้นใหม่
ทุกอย่างเป็นไปราวกับว่า Gradle กำลังเรียกใช้ "เวอร์ชันผี" ของบิลด์ของเรา โดยไม่สนใจการเปลี่ยนแปลงล่าสุดของเรา
การสำรวจ: แคชการกำหนดค่าคืออะไร?
แคชการกำหนดค่าเป็นฟีเจอร์ที่ค่อนข้างใหม่และทรงพลังอย่างยิ่งของ Gradle ซึ่งเปิดใช้งานโดยค่าเริ่มต้นในเวอร์ชันใหม่ ๆ จุดประสงค์ของมันคือทำให้การบิลด์เร็วขึ้น
หลักการนั้นง่าย:
-
ในการรันครั้งแรก Gradle จะดำเนินการในระยะของการกำหนดค่า(การอ่าน`build.gradle.kts`, การสร้างงาน การจัดการการพึ่งพา) และสร้างกราฟของงาน
-
เมื่อสิ้นสุดระยะนี้ Gradleทำให้กราฟของงานนี้เป็นอนุกรมและเก็บไว้ในแคช
-
ในการรันครั้งถัดไป หากไม่มีอะไรเปลี่ยนแปลง (สคริปต์ build,
gradle.properties, ฯลฯ), Gradleข้ามขั้นตอนการกำหนดค่าไปเลยและนำกราฟงานที่แคชไว้กลับมาใช้ใหม่
การประหยัดเวลานั้นชัดเจนมากในโปรเจกต์ขนาดใหญ่ อย่างไรก็ตาม ประสิทธิภาพนี้มีราคาที่ต้องจ่าย: มันกำหนดกฎที่เข้มงวดเกี่ยวกับวิธีที่ปลั๊กอินต้องถูกเขียน
@startuml start :การดำเนินการครั้งแรก; :ระยะการกำหนดค่า; :การสร้างกราฟของงาน; :การแคชกราฟ; :ระยะการดำเนินการ; end start :การดำเนินการครั้งถัดไป; if (Cache valide ?) then (oui) :การกู้คืนกราฟจากแคช; note right ขั้นตอนการกำหนดค่าถูกข้ามไป! end note else (non) :ระยะการกำหนดค่า; :การอัปเดตแคช; endif :ระยะการดำเนินการ; stop @enduml
สาเหตุของปัญหา: ปลั๊กอินที่ไม่เป็นไปตามข้อกำหนด
ปลั๊กอินของเรา`site-baker`โดยไม่รู้ตัวว่ากำลังละเมิดกฎหลายข้อของแคชการกำหนดค่า เพื่อให้กราฟงานสามารถซีเรียลไลซ์ได้ งานต้องไม่มีการอ้างอิงถึงออบเจ็กต์ที่ซับซ้อน เช่น ออบเจ็กต์`Project`หรืออ่านไฟล์ตามอำเภอใจในระหว่างขั้นตอนการรัน
ข้อผิดพลาดหลักของเราคือการอ่านเนื้อหาของไฟล์ YAML โดยตรงภายในลอจิกการทำงานของ task โดยใช้การอ้างอิงไปยังพาธที่จัดเก็บไว้ใน extension ของเรา แนวทางนี้เข้ากันไม่ได้กับแคช เนื่องจาก Gradle ไม่สามารถทราบได้ว่าเนื้อหาของไฟล์เปลี่ยนแปลงหรือไม่ หากการอ่านนี้ไม่ได้ถูกจำลองให้เป็นอินพุตงาน(อินพุตงาน)
วิธีแก้ไขชั่วคราว: ปิดใช้งานแคช
เพื่อให้เราหลุดจากสถานะที่ติดขัดและกลับมามีพฤติกรรมการบิลด์ที่คาดเดาได้ วิธีที่เร็วที่สุดคือการปิดใช้งานแคชการกำหนดค่า เพียงเพิ่มบรรทัดต่อไปนี้ในไฟล์`gradle.properties`ของโปรเจกต์ที่ใช้ปลั๊กอิน (หรือในกรณีของเรา โปรเจกต์ทดสอบ`site-baker`).
# site-baker/gradle.properties
org.gradle.configuration-cache=false
ทันทีนั้น บิลด์ก็กลับสู่พฤติกรรมปกติ การรันแต่ละครั้งเริ่มเฟสการกำหนดค่าใหม่ และการเปลี่ยนแปลงของเราถูกนำไปใช้
อย่างไรก็ตาม นี่เป็นเพียงวิธีเลี่ยงปัญหา ไม่ใช่แนวทางแก้ไขที่ยั่งยืน วิธีนี้แลกมาด้วยประสิทธิภาพและไม่ได้แก้ปัญหาพื้นฐานของปลั๊กอินของเรา
วิธีแก้ปัญหาที่แท้จริง: ทำให้ปลั๊กอินเข้ากันได้
เพื่อให้ปลั๊กอินเป็นพลเมืองที่ดีของระบบนิเวศ Gradle สมัยใหม่ ปลั๊กอินนั้นต้องเข้ากันได้กับแคชการกำหนดค่า ซึ่งหมายความว่าต้องคิดทบทวนวิธีที่ข้อมูลไหลเวียนไปยังทาสก์ของเรา
กุญแจสำคัญคือการใช้API ของผู้ให้บริการของ Gradle แทนที่จะส่งค่าที่ระบุโดยตรง (เช่น`String` ou un File) สำหรับงานของเรา เราต้องใช้`Property<T>`หรือบาง`Provider<T>`.
-
ประกาศรายการงาน:งานที่พาร์สไฟล์ YAML ต้องประกาศไฟล์นี้เป็นอินพุต สำหรับสิ่งนี้จะใช้แอนโนเทชัน`@InputFile`.
[source,kotlin] ---- @get:InputFile abstract val configFile: RegularFileProperty ----
-
ใช้
PropertyและProvider:ค่าของ`configFile`จะถูกเชื่อมต่อกับทรัพย์สิน`configPath`ของส่วนขยาย DSL ของเรา Gradle จึงสามารถติดตามแหล่งที่มาของข้อมูลได้
[source,kotlin] ---- // ในปลั๊กอิน tasks.register<MyTask>("myTask") { configFile.set(extension.configPath.flatMap { project.layout.projectDirectory.file(it) }) } ----
-
อ่านเนื้อหาในเวลาที่เหมาะสม:การอ่านไฟล์ต้องทำภายในแอ็กชันของทาสก์ (
@TaskAction), โดยใช้`Provider`ของทางเข้า
[source,kotlin] ---- @TaskAction fun execute() { val content = configFile.get().asFile.readText() // … แยกวิเคราะห์เนื้อหา } ----
เมื่อปฏิบัติตามรูปแบบนี้ Gradle จะเข้าใจว่าหากเนื้อหาของ`configFile`เปลี่ยนแปลง, แคชการกำหนดค่าไม่ถูกต้อง และต้องดำเนินการขั้นตอนการกำหนดค่าใหม่ นอกจากนี้ ระบบรู้ว่าผลลัพธ์ของงานขึ้นอยู่กับเนื้อหาของไฟล์นี้ ซึ่งยังช่วยให้สามารถปรับแคชการดำเนินการให้เหมาะสมได้ด้วย (UP-TO-DATE).
บทสรุป
การผจญภัยในการดีบักครั้งนี้เป็นบทเรียนอันมีค่า พฤติกรรม "มหัศจรรย์" ของแคชการกำหนดค่าได้บังคับให้เราเข้าใจวงจรชีวิตของ Gradle และหลักการของการเขียนโปรแกรมเชิงประกาศและแบบเลื่อนเวลาได้ดียิ่งขึ้น (lazy).
การปิดใช้งานแคชการกำหนดค่าเป็นเครื่องมือวินิจฉัยที่มีประโยชน์ แต่ทางแก้ที่แท้จริงคือการออกแบบปลั๊กอินที่แข็งแกร่งและทันสมัย โดยการจำลองอินพุตและเอาต์พุตของงานของเราอย่างถูกต้องด้วย API`Provider`, เราไม่ได้เพียงแค่แก้บั๊ก แต่เรายังปรับปรุงประสิทธิภาพ ความน่าเชื่อถือ และความสามารถในการบำรุงรักษาของปลั๊กอินของเรา
ในบทความถัดไป เราจะนำการรีแฟกเตอร์นี้ไปปฏิบัติจริง เพื่อทำให้งานแยกวิเคราะห์ YAML ของเราเข้ากันได้อย่างสมบูรณ์กับแคชการกำหนดค่า