การย้ายปลั๊กอิน Asciidoctor RevealJS ไปที่ buildSrc ของ Kotlin : วิศวกรรมย้อนกลับของ API Groovy
เผยแพร่เมื่อ 16 April 2026
บริบทและวัตถุประสงค์
จุดเริ่มต้น
โครงการ`slider-gradle`สร้างการนำเสนอ Reveal.js จากไฟล์ AsciiDoc ผ่านปลั๊กอิน Gradle`org.asciidoctor.jvm.revealjs`. การตั้งค่างานหลัก`asciidoctorRevealJs`อาศัยอยู่โดยตรงใน buildscript ราก`build.gradle.kts`:
plugins { id("org.asciidoctor.jvm.revealjs") }
apply<slides.SlidesPlugin>()
project.tasks.getByName<AsciidoctorJRevealJSTask>(TASK_ASCIIDOCTOR_REVEALJS) {
repositories { ruby { gems() } }
revealjs {
version = "3.1.0"
templateGitHub {
setOrganisation("hakimel")
setRepository("reveal.js")
setTag("3.9.1")
}
}
revealjsOptions {
// ... configuration complète
}
}
วัตถุประสงค์
ย้ายการกำหนดค่าทั้งหมดนี้ไปยัง`buildSrc/src/main/kotlin/slides/SlidesPlugin.kt`เพื่อให้ buildscript ฝั่งผู้บริโภคเหลือเพียง:
apply<slides.SlidesPlugin>()
ปลั๊กอินต้องพึ่งพาตนเองได้อย่างสมบูรณ์: มันจะใช้งาน dependency ของปลั๊กอินด้วยตัวเอง กำหนดค่า repos ของตัวเอง และจัดการ gems Ruby ของตัวเอง
อุปสรรคแรก: `ruby { gems() }
สิ่งที่ซ่อนอยู่ในน้ำตาลทางไวยากรณ์ของ Groovy
เส้น`repositories { ruby { gems() } }`เป็นส่วนขยาย Groovy DSL ที่มีให้ใช้เฉพาะในบริบทการทำงานของ buildscript เท่านั้น ไม่มีอยู่ในรูปแบบ API แบบสแตติกของ Kotlin ที่เข้าถึงได้จาก buildSrc
โดยพยายามโทรหาเขาจาก`SlidesPlugin.kt`, การคอมไพล์ล้มเหลวทันที
การแยกส่วนของกลไก
หลังจากวิเคราะห์แคช Gradle (~/.gradle/caches/modules-2/files-2.1/rubygems/), เราพบในไฟล์`ivy-3.1.0.xml`:
<artifact type='gem' url='https://rubygems.org/gems/asciidoctor-revealjs-3.1.0.gem' />
`ruby { gems() }`ในความเป็นจริงแล้วดำเนินการอยู่การดำเนินการที่แตกต่างกันสามประการ
-
บันทึก repo Ivy ที่ชี้ไปยัง`https://rubygems.org/gems/`
-
ยกเว้นกลุ่ม`rubygems`ที่เก็บ Maven เพื่อหลีกเลี่ยงความขัดแย้ง
-
บันทึก gem ลงในการกำหนดค่า`asciidoctorGems`เพื่อให้ JRuby โหลดมันตอนรันไทม์
ความรับผิดชอบทั้งสามนี้ต้องถูกทำซ้ำแยกจากกันใน Kotlin
โซลูชันสามส่วน
ส่วนที่ 1 : repo Ivy สำหรับ rubygems
project.repositories.mavenCentral() {
content { excludeGroup("rubygems") }
}
project.repositories.ivy {
url = project.uri("https://rubygems.org/gems/")
patternLayout { artifact("[module]-[revision].gem") } (1)
metadataSources { artifact() }
content { includeGroup("rubygems") }
}
ส่วนขยาย`.gem`ต้องถูกฮาร์ดโค้ด`[ext]แก้ไขโดยค่าเริ่มต้นเป็น.jar`, ซึ่งทำให้เกิดข้อผิดพลาด`Resource missing`.
หมายเหตุ: repo Ivy ถูกประกาศโดยตรงบน`project.repositories`และไม่อยู่ในบล็อก`repositories { }`เพราะรีซีฟเวอร์ของบล็อกนี้ไม่ใช่`RepositoryHandler`มาตรฐานของ Gradle แต่เป็น API grolifant ที่ไม่เข้ากันกับส่วนขยาย`ivy`Kotlin DSL
ส่วนที่ 2 : การพึ่งพา asciidoctorGems
ปลั๊กอิน`org.asciidoctor.jvm.gems`ควรใช้ก่อน — มันสร้างการกำหนดค่า`asciidoctorGems`และงาน`asciidoctorGemsPrepare`(No visible text to translate, so result is empty.)
project.plugins.apply("org.asciidoctor.jvm.gems")
project.plugins.apply("org.asciidoctor.jvm.revealjs") (1)
project.dependencies {
add("asciidoctorGems", "rubygems:asciidoctor-revealjs:3.1.0@gem") (2)
}
| 1 | ลำดับการนำไปใช้มีความสำคัญ :`gems`ก่อน`revealjs`. ตัวระบุ`@gem`บังคับให้ใช้ส่วนขยายที่ถูกต้องกับ dependency |
ส่วนที่ 3 : settings.gradle.kts
`dependencyResolutionManagement`ใน`settings.gradle.kts`ต้องอนุญาตให้โปรเจกต์ประกาศรีโปของตนเอง:
@Suppress("UnstableApiUsage")
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
}
หากไม่มีบรรทัดนี้ Gradle จะเพิกเฉยต่อรีโพที่ประกาศไว้ในปลั๊กอิน และการ resolve gems จะล้มเหลว
การตรวจสอบภายใน API ด้วยไบต์โค้ด
ทำไมต้อง javap ?
ปลั๊กอิน`asciidoctor-gradle-jvm-slides`อยู่ในเวอร์ชัน`4.0.0-alpha.1`. เอกสารประกอบของมันไม่มีอยู่หรือไม่สมบูรณ์ แหล่งข้อมูลที่เชื่อถือได้เพียงแหล่งเดียวคือการตรวจสอบคลาสที่คอมไพล์แล้วโดยตรง
ลำดับชั้นของงาน
javap -p -classpath asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.slides.AsciidoctorJRevealJSTask
ผลลัพธ์:
public class AsciidoctorJRevealJSTask
extends org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask
implements org.asciidoctor.gradle.base.slides.SlidesToExportAware
การค้นพบ `forkOptions
ในขณะที่ตรวจสอบ`AbstractAsciidoctorTask` :
javap -p -classpath asciidoctor-gradle-jvm-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask | grep -i "fork\|exec\|jvm"
เราพบ:
final org.ysb33r.grolifant.api.v4.JavaForkOptions javaForkOptions;
public void forkOptions(org.gradle.api.Action<org.ysb33r.grolifant.api.v4.JavaForkOptions>);
public static final org.asciidoctor.gradle.base.process.ProcessMode JAVA_EXEC;
API ของ JavaForkOptions (grolifant)
`javaLauncher`ไม่มีอยู่ในงานนี้ API จริงของ`org.ysb33r.grolifant.api.v4.JavaForkOptions`เปิดเผย :
public void executable(java.lang.Object); (1)
public void setExecutable(java.lang.Object);
| 1 | วิธีการ`executable(Object)`แทนที่การกำหนดค่า`executable = …`ที่คอมไพล์ไม่ผ่าน (`val`ไม่สามารถกำหนดค่าใหม่ได้) |
RevealJSExtension
`revealjs { }`ไม่ใช่วิธีการของงาน แต่เป็นการขยายโครงการ:
javap -p -classpath asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.slides.RevealJSExtension
สามารถเข้าถึงได้ผ่าน :
project.extensions.getByType<RevealJSExtension>().apply {
version = "3.1.0"
templateGitHub {
setOrganisation("hakimel")
setRepository("reveal.js")
setTag("3.9.1")
}
}
การกำหนดค่า toolchain Java
ปัญหาของ JavaToolchainService
`JavaToolchainService`ไม่ใช่ส่วนขยายของโปรเจกต์ การเรียกต่อไปนี้ล้มเหลว:
// ERREUR : Extension of type 'JavaToolchainService' does not exist
project.extensions.getByType<JavaToolchainService>()
API ที่ถูกต้องคือ`serviceOf`:
import org.gradle.kotlin.dsl.support.serviceOf
project.tasks.getByName<AsciidoctorJRevealJSTask>(TASK_ASCIIDOCTOR_REVEALJS) {
setInProcess("JAVA_EXEC")
forkOptions {
executable(
project.serviceOf<JavaToolchainService>()
.launcherFor {
languageVersion.set(JavaLanguageVersion.of(17))
vendor.set(JvmVendorSpec.ADOPTIUM)
}
.get()
.executablePath
.asFile
.absolutePath
)
}
}
การตรวจจับ Docker อัตโนมัติ
บริบท
ปลั๊กอิน Asciidoctor/JRuby ต้องใช้ Java 17 Kotlin 2.0.x ใน buildSrc ไม่รองรับ Java 25 (ตัวแยกวิเคราะห์เวอร์ชันล่มเมื่อเจอ`"25.0.2"`). ดังนั้น Gradle daemon ต้องรันบน Java 17 หรือต้องใช้ Docker
กลยุทธ์
-
มี Docker → รันผ่านคอนเทนเนอร์`eclipse-temurin:17`(พฤติกรรมเริ่มต้น)
-
ไม่มี Docker + Java 17 → เรียกใช้ในเครื่อง
-
Docker ไม่มี + Java > 17 → ข้อผิดพลาดที่ชัดเจน
val isDockerAvailable = try {
Runtime.getRuntime().exec(arrayOf("docker", "info")).waitFor() == 0
} catch (e: Exception) {
false
}
val javaVersion = JavaVersion.current().majorVersion.toInt()
when {
isDockerAvailable -> project.tasks.register<Exec>(TASK_ASCIIDOCTOR_REVEALJS) {
group = GROUP_TASK_SLIDER
description = "Slider settings and generation (via Docker)"
dependsOn(TASK_CLEAN_SLIDES_BUILD)
finalizedBy(TASK_DASHBOARD_SLIDES_BUILD)
commandLine(
"docker", "run", "--rm",
"-v", "${project.rootDir.absolutePath}:/workspace",
"-v", "${System.getProperty("user.home")}/.gradle:/root/.gradle",
"-w", "/workspace",
"eclipse-temurin:17",
"./gradlew", TASK_ASCIIDOCTOR_REVEALJS
)
workingDir = project.rootDir
}
javaVersion == 17 -> {
project.repositories.mavenCentral() {
content { excludeGroup("rubygems") }
}
project.repositories.ivy {
url = project.uri("https://rubygems.org/gems/")
patternLayout { artifact("[module]-[revision].gem") }
metadataSources { artifact() }
content { includeGroup("rubygems") }
}
project.extensions.getByType<RevealJSExtension>().apply {
version = "3.1.0"
templateGitHub {
setOrganisation("hakimel")
setRepository("reveal.js")
setTag("3.9.1")
}
}
project.tasks.getByName<AsciidoctorJRevealJSTask>(TASK_ASCIIDOCTOR_REVEALJS) {
setInProcess("JAVA_EXEC")
forkOptions {
executable(
project.serviceOf<JavaToolchainService>()
.launcherFor {
languageVersion.set(JavaLanguageVersion.of(17))
vendor.set(JvmVendorSpec.ADOPTIUM)
}
.get()
.executablePath
.asFile
.absolutePath
)
}
// ... reste de la configuration
}
}
else -> error(
"Docker est requis pour exécuter $TASK_ASCIIDOCTOR_REVEALJS " +
"avec Java $javaVersion. Installez Docker ou utilisez Java 17."
)
}
ผลลัพธ์สุดท้าย
สคริปต์การสร้างฝั่งผู้บริโภค
apply<slides.SlidesPlugin>()
แค่นั้นเอง ปลั๊กอินแบกรับความรับผิดชอบทั้งหมดแต่เพียงผู้เดียว
settings.gradle.kts
pluginManagement {
repositories {
mavenLocal()
gradlePluginPortal()
}
}
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
}
@Suppress("UnstableApiUsage")
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
}
rootProject.name = "slider-gradle"
สรุปข้อควรระวังและแนวทางแก้ไข
| ปัญหา | สาเหตุ | วิธีแก้ปัญหา |
|---|---|---|
`ruby { gems() }`ไม่พร้อมใช้งานใน Kotlin |
ส่วนขยาย DSL Groovy เท่านั้น |
สามกลไกที่แยกจากกัน: repo Ivy + การยกเว้น Maven + |
Gradle กำลังมองหา`.jar`แทนที่จะเป็น`.gem` |
`[ext]`แก้เป็น`jar`โดยค่าเริ่มต้น |
ฮาร์ดโค้ด`.gem`ในแพตเทิร์น Ivy + ตัวระบุ`@gem` |
|
Receiver grolifant ไม่เข้ากันกับ DSL Kotlin |
การโทรโดยตรง`project.repositories.ivy { }` |
`javaLauncher`ยังไม่ได้รับการแก้ไข |
พร็อพเพอร์ตี้ไม่มีอยู่บน`AsciidoctorJRevealJSTask` |
|
`executable = …`ไม่คอมไพล์ |
ทรัพย์สิน`val`ใน`JavaForkOptions`โกรลีฟองต์ |
วิธีการ`executable(Object)`แทนที่ |
|
มันเป็นบริการ Gradle ไม่ใช่ส่วนขยาย |
|
`revealjs { }`ไม่ได้รับการแก้ไขในงาน |
การขยายโครงการ ไม่ใช่วิธีการของงาน |
|
บิลด์แครชเมื่อใช้ Java 25 |
Kotlin 2.0.x ไม่สามารถแยกวิเคราะห์เวอร์ชัน Java ที่มีตัวเลขสองหลักได้ |
การตรวจจับ Docker อัตโนมัติ + กลไกสำรอง Java 17 |
วิธีการตรวจสอบ: อ่าน API ที่ไม่รู้จักด้วย javap
หลักการ
เมื่อไม่มีเอกสารประกอบหรือเอกสารไม่สมบูรณ์ ไบต์โค้ดถือเป็นแหล่งความจริง javap`เป็นเครื่องมือมาตรฐานของ JDK ที่ดีคอมไพล์ไฟล์.class`เป็นลายเซ็น Java ที่อ่านง่าย, โดยไม่ต้องใช้ซอร์สโค้ด
ขั้นตอนที่ 1: ค้นหาตำแหน่งไฟล์ jar ในแคช Gradle
Gradle ดาวน์โหลด dependency ทั้งหมดของมันไว้ใน`~/.gradle/caches/modules-2/files-2.1/`. ขั้นตอนแรกคือการค้นหา jar ที่มีคลาสที่ต้องตรวจสอบ:
find ~/.gradle/caches -name "asciidoctor-gradle-jvm-slides*.jar" 2>/dev/null
/home/user/.gradle/caches/modules-2/files-2.1/org.asciidoctor/
asciidoctor-gradle-jvm-slides/4.0.0-alpha.1/.../
asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar
ขั้นตอนที่ 2: แสดงรายการคลาสของ jar
ก่อนที่จะตรวจสอบคลาส เราจะตรวจสอบก่อนว่าคลาสนั้นมีอยู่ใน jar จริง:
jar tf asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar | grep -i "RevealJS\|revealjs"
ขั้นตอนที่ 3 : ตรวจสอบคลาส
AsciidoctorJRevealJSTask, RevealJSExtension, RevealJSOptions, ฯลฯjavap -p -classpath asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.slides.AsciidoctorJRevealJSTask
ตัวเลือก`-p`แสดงสมาชิกทั้งหมด รวมถึงสมาชิกที่เป็นส่วนตัว ผลลัพธ์แสดงบรรทัดสำคัญทันที:
public class AsciidoctorJRevealJSTask
extends org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask
ขั้นตอนที่ 4: ย้อนกลับขึ้นตามลำดับชั้น
ภารกิจขยาย`AbstractAsciidoctorTask`. จากนั้นจึงตรวจสอบมันด้วยเช่นกัน โดยระบุตำแหน่ง jar ของมันก่อน:
find ~/.gradle/caches -name "asciidoctor-gradle-jvm-[0-9]*.jar" 2>/dev/null
javap -p -classpath asciidoctor-gradle-jvm-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask | grep -i "fork\|exec\|jvm\|java"
นั่นคือจุดที่เราค้นพบ`forkOptions`, JAVA_EXEC, et javaForkOptions ประเภท`org.ysb33r.grolifant.api.v4.JavaForkOptions`.
ขั้นตอนที่ 5: ติดตามประเภทที่ไม่รู้จัก
`JavaForkOptions`เป็นคลาส grolifant ที่ไม่รู้จัก เราระบุตำแหน่ง jar ของมัน:
find ~/.gradle/caches -name "grolifant*.jar" 2>/dev/null
จากนั้นเราจึงตรวจสอบมัน:
javap -p -classpath grolifant40-legacy-api-2.0.0-alpha.6.jar \
org.ysb33r.grolifant.api.v4.JavaForkOptions
ที่นั่นมี`executable(java.lang.Object)— เมธอดที่ถูกต้องในการเรียก, ตรงกันข้ามกับ`executable = …`ซึ่งคอมไพล์ไม่ผ่านเพราะมันเป็นพร็อพเพอร์ตี้`val.
ขั้นตอนที่ 6 : ตรวจสอบส่วนขยายของโปรเจกต์
สำหรับ`revealjs { }`, คำถามคือ นี่เป็นวิธีการของงานหรือไม่ หรือส่วนขยายของโครงการ ? การตรวจสอบของ`AsciidoctorJRevealJSTask` อย่าแสดงวิธีการใด ๆ`revealjs`. จากนั้นจึงตรวจสอบ`RevealJSExtension`:
javap -p -classpath asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.slides.RevealJSExtension | head -5
public class RevealJSExtension implements groovy.lang.GroovyObject {
public static final java.lang.String NAME;
การมีอยู่ของ`NAME`ยืนยันว่านี่คือส่วนขยายที่ลงทะเบียนไว้ในโปรเจกต์, เข้าถึงได้ผ่าน`project.extensions.getByType<RevealJSExtension>()`.
สรุปวิธีการ
| ขั้นตอน | การกระทำ |
|---|---|
1 |
|
2 |
|
3 |
|
4 |
ระบุ`extends`และไล่ขึ้นไปตามลำดับชั้น |
5 |
ติดตามประเภทที่ไม่รู้จักใน jar ของตัวเอง |
6 |
ค้นหา`NAME`เพื่อระบุส่วนขยายของโครงการ |
เมธอดนี้ใช้ได้กับปลั๊กอิน Gradle ใด ๆ ที่ API ไม่มีเอกสารประกอบ หรือซึ่งเวอร์ชันอัลฟาไม่สอดคล้องกับเอกสารที่มีอยู่อีกต่อไป
ขั้นตอนถัดไป
ปลั๊กอิน buildSrc นี้จะถูกแยกออกเป็นโปรเจกต์อิสระที่เผยแพร่บน Gradle Plugin Portal หรือ Maven Local จากนั้น buildscript ของผู้บริโภคจะกลายเป็น:
plugins { id("slides") version "1.0.0" }
Et settings.gradle.kts`จะถูกลดให้เหลือน้อยที่สุดโดยไม่มีการอ้างอิงถึง`foojay-resolver-convention, โดยการจัดเตรียม JDK นั้นได้รับการจัดการโดยปลั๊กอินเอง หรือมีการจัดทำเอกสารไว้เป็นข้อกำหนดเบื้องต้น