Di chuyển plugin Asciidoctor RevealJS sang buildSrc Kotlin: kỹ thuật đảo ngược (reverse engineering) một API Groovy
Đăng ngày 16 April 2026
Bối cảnh và mục tiêu
Điểm khởi đầu
Dự án`slider-gradle`tạo các bản thuyết trình Reveal.js từ các tệp AsciiDoc thông qua plugin Gradle`org.asciidoctor.jvm.revealjs`. Cấu hình của tác vụ chính`asciidoctorRevealJs`nằm trực tiếp trong buildscript gốc`build.gradle.kts`Vui lòng cung cấp văn bản cần dịch.
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
}
}
Mục tiêu
Di chuyển toàn bộ cấu hình này vào`buildSrc/src/main/kotlin/slides/SlidesPlugin.kt`để buildscript tiêu thụ chỉ còn lại:
apply<slides.SlidesPlugin>()
Plugin phải hoàn toàn độc lập: nó tự áp dụng các plugin phụ thuộc, cấu hình các repo và quản lý các Ruby gem của chính mình.
Trở ngại đầu tiên: `ruby { gems() }
Những gì ẩn sau cú pháp đường (syntactic sugar) của Groovy
Dòng`repositories { ruby { gems() } }`là một phần mở rộng DSL Groovy chỉ khả dụng trong ngữ cảnh thực thi của buildscript. Nó không tồn tại dưới dạng API Kotlin tĩnh có thể truy cập được từ buildSrc.
Khi cố gắng gọi nó từ`SlidesPlugin.kt`, quá trình biên dịch sẽ thất bại ngay lập tức.
Phân tích cơ chế
Sau khi phân tích bộ nhớ đệm Gradle (~/.gradle/caches/modules-2/files-2.1/rubygems/), chúng ta tìm thấy trong tệp`ivy-3.1.0.xml`Vui lòng cung cấp văn bản cần dịch.
<artifact type='gem' url='https://rubygems.org/gems/asciidoctor-revealjs-3.1.0.gem' />
ruby { gems() }`thực chất đã thực hiệnba thao tác riêng biệtHãy nhập `name của bạn:
-
Lưu một repo Ivy trỏ đến`https://rubygems.org/gems/`
-
Loại bỏ nhóm`rubygems`các repo Maven để tránh xung đột
-
Lưu gem vào cấu hình`asciidoctorGems`để JRuby tải nó vào runtime
Ba trách nhiệm này phải được tái hiện riêng biệt trong Kotlin.
Giải pháp gồm ba phần
Phần 1: repo Ivy cho 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") }
}
| 1 | Tiện ích mở rộng`.gem`phải được hardcoded.[ext]`được giải quyết theo mặc định bằng.jar`, điều này gây ra lỗi`Resource missing`Vui lòng cung cấp văn bản cần dịch. |
LƯU Ý: Repo Ivy được khai báo trực tiếp trên`project.repositories`và không nằm trong một khối`repositories { }`vì receiver của khối này không phải là`RepositoryHandler`tiêu chuẩn của Gradle nhưng là một API grolifant không tương thích với extension`ivy`Kotlin DSL.
Phần 2: sự phụ thuộc asciidoctorGems
Plugin`org.asciidoctor.jvm.gems`phải được áp dụng trước tiên — nó tạo ra cấu hình`asciidoctorGems`và tác vụ`asciidoctorGemsPrepare`Vui lòng cung cấp văn bản cần dịch.
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 | Thứ tự áp dụng là rất quan trọng:`gems`trước`revealjs`Vui lòng cung cấp nội dung cần dịch. <2> Bộ định tính`@gem`buộc extension chính xác trên dependency. |
Phần 3: `settings.gradle.kts
`dependencyResolutionManagement`trong`settings.gradle.kts`phải cho phép các dự án khai báo các repo của riêng chúng:
@Suppress("UnstableApiUsage")
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
}
Nếu không có dòng này, Gradle sẽ bỏ qua các repo được khai báo trong plugin và việc giải quyết các gems sẽ thất bại.
Truy xuất nội soi API bằng bytecodes
Tại sao lại là javap?
Plugin`asciidoctor-gradle-jvm-slides`đang ở phiên bản`4.0.0-alpha.1`. Tài liệu của nó không tồn tại hoặc không đầy đủ. Nguồn đáng tin cậy duy nhất là kiểm tra trực tiếp các lớp đã biên dịch.
Hệ thống phân cấp tác vụ
javap -p -classpath asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.slides.AsciidoctorJRevealJSTask
Kết quả:
public class AsciidoctorJRevealJSTask
extends org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask
implements org.asciidoctor.gradle.base.slides.SlidesToExportAware
Khám phá `forkOptions
Khi kiểm tra`AbstractAsciidoctorTask`Vui lòng cung cấp văn bản tiếng Pháp cần dịch.
javap -p -classpath asciidoctor-gradle-jvm-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask | grep -i "fork\|exec\|jvm"
Chúng ta tìm thấy:
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 của JavaForkOptions (grolifant)
`javaLauncher`không tồn tại trong tác vụ này. API thực tế của`org.ysb33r.grolifant.api.v4.JavaForkOptions`phơi bày :
public void executable(java.lang.Object); (1)
public void setExecutable(java.lang.Object);
| 1 | Phương pháp`executable(Object)`thay thế phép gán`executable = …`không biên dịch được (`val`không thể gán lại). |
RevealJSExtension
`revealjs { }`không phải là một phương thức của tác vụ mà là mộtmở rộng dự ánVui lòng cung cấp văn bản cần dịch.
javap -p -classpath asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.slides.RevealJSExtension
Truy cập thông qua:
project.extensions.getByType<RevealJSExtension>().apply {
version = "3.1.0"
templateGitHub {
setOrganisation("hakimel")
setRepository("reveal.js")
setTag("3.9.1")
}
}
Cấu hình toolchain Java
Vấn đề của JavaToolchainService
`JavaToolchainService`không phải là một phần mở rộng của dự án. Lời gọi sau đây bị lỗi:
// ERREUR : Extension of type 'JavaToolchainService' does not exist
project.extensions.getByType<JavaToolchainService>()
API đúng là`serviceOf`Vui lòng cung cấp văn bản cần dịch.
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
)
}
}
Tự động phát hiện Docker
Ngữ cảnh
Plugin Asciidoctor/JRuby yêu cầu Java 17. Kotlin 2.0.x trong buildSrc không hỗ trợ Java 25 (trình phân tích cú pháp phiên bản bị treo tại`"25.0.2"`). Do đó, Gradle daemon phải chạy trên Java 17 hoặc phải sử dụng Docker.
Chiến lược
-
Docker có sẵn → thực thi qua container`eclipse-temurin:17`(hành vi mặc định)
-
Thiếu Docker + Java 17 → thực thi cục bộ
-
Thiếu Docker + Java > 17 → lỗi rõ ràng
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."
)
}
Kết quả cuối cùng
Buildscript tiêu thụ
apply<slides.SlidesPlugin>()
Vậy là xong. Plugin chịu toàn bộ trách nhiệm.
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"
Tóm tắt các cạm bẫy và giải pháp
| Vấn đề | Nguyên nhân | Giải pháp |
|---|---|---|
`ruby { gems() }`không có sẵn trong Kotlin |
Tiện ích mở rộng chỉ dành cho DSL Groovy |
Ba cơ chế riêng biệt: repo Ivy + loại trừ Maven + |
Gradle đang tìm một`.jar`thay vì một`.gem` |
`[ext]`giải quyết bằng`jar`mặc định |
Hardcoder`.gem`trong pattern Ivy + qualifier`@gem` |
|
Bộ thu grolifant không tương thích với DSL Kotlin |
Cuộc gọi trực tiếp`project.repositories.ivy { }` |
`javaLauncher`chưa giải quyết |
Thuộc tính không tồn tại trên`AsciidoctorJRevealJSTask` |
|
`executable = …`không biên dịch được |
Thuộc tính`val`trong`JavaForkOptions`grolifant |
Phương pháp`executable(Object)`thay vào đó |
|
Đây là một Gradle service, không phải là một extension |
|
`revealjs { }`chưa được giải quyết trong tác vụ |
Mở rộng dự án, không phải phương pháp tác vụ |
|
Build bị crash với Java 25 |
Kotlin 2.0.x không phân tích cú pháp các phiên bản Java có hai chữ số |
Tự động phát hiện Docker + fallback Java 17 |
Phương pháp điều tra: đọc một API chưa biết bằng `javap
Nguyên tắc
Khi tài liệu thiếu hoặc không đầy đủ, các bytecode là nguồn sự thật. javap`là công cụ tiêu chuẩn của JDK dùng để dịch ngược các tệp.class`thành các chữ ký Java dễ đọc, mà không cần mã nguồn.
Bước 1: xác định vị trí tệp jar trong cache Gradle
Gradle tải xuống tất cả các phụ thuộc của mình vào`~/.gradle/caches/modules-2/files-2.1/`Vui lòng cung cấp văn bản cần dịch. Bước đầu tiên là tìm file jar chứa class cần kiểm tra:
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
Bước 2: liệt kê các lớp của jar
Trước khi kiểm tra một lớp, chúng ta xác minh rằng nó thực sự tồn tại trong file jar:
jar tf asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar | grep -i "RevealJS\|revealjs"
Điều này hiển thị tất cả các lớp có sẵn: AsciidoctorJRevealJSTask, RevealJSExtension, RevealJSOptions, v.v.
Bước 3: kiểm tra một lớp
javap -p -classpath asciidoctor-gradle-jvm-slides-4.0.0-alpha.1.jar \
org.asciidoctor.gradle.jvm.slides.AsciidoctorJRevealJSTask
Tùy chọn`-p`hiển thị tất cả thành viên bao gồm cả những thành viên riêng tư. Kết quả hiển thị ngay lập tức dòng chính:
public class AsciidoctorJRevealJSTask
extends org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask
Bước 4: đi ngược lên cấp bậc phân cấp
Nhiệm vụ mở rộng`AbstractAsciidoctorTask`. Lần lượt kiểm tra nó bằng cách định vị tệp jar của nó trước:
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"
Đó là nơi chúng ta khám phá ra`forkOptions`, JAVA_EXEC, et javaForkOptions loại`org.ysb33r.grolifant.api.v4.JavaForkOptions`Vui lòng cung cấp nội dung cần dịch.
Bước 5: theo dõi các kiểu không xác định
`JavaForkOptions`là một lớp grolifant không xác định. Chúng ta xác định vị trí file jar của nó:
find ~/.gradle/caches -name "grolifant*.jar" 2>/dev/null
Sau đó chúng ta kiểm tra nó:
javap -p -classpath grolifant40-legacy-api-2.0.0-alpha.6.jar \
org.ysb33r.grolifant.api.v4.JavaForkOptions
Ở đó có`executable(java.lang.Object)`— phương thức chính xác cần gọi, trái ngược với`executable = …`không biên dịch được vì đó là một thuộc tính`val`Vui lòng cung cấp văn bản tiếng Pháp cần dịch.
Bước 6: kiểm tra các phần mở rộng của dự án
Cho`revealjs { }`, câu hỏi là: đây có phải là một phương pháp của tác vụ hay một phần mở rộng của dự án? Việc kiểm tra`AsciidoctorJRevealJSTask` không hiển thị phương thức nào`revealjs`. Sau đó chúng ta kiểm tra`RevealJSExtension`Vui lòng cung cấp văn bản tiếng Pháp cần dịch.
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;
Sự hiện diện của`NAME`xác nhận rằng đây là một phần mở rộng được đăng ký trên dự án, có thể truy cập thông qua`project.extensions.getByType<RevealJSExtension>()`Vui lòng cung cấp văn bản cần dịch.
Tóm tắt phương pháp
| Bước | Hành động |
|---|---|
1 |
|
2 |
|
3 |
|
4 |
Định danh`extends`và đi lên cấp bậc cao hơn |
5 |
Theo dõi các kiểu không xác định trong các file jar của riêng chúng |
6 |
Tìm kiếm`NAME`để xác định một phần mở rộng của dự án |
Phương pháp này áp dụng cho bất kỳ plugin Gradle nào có API không được tài liệu hóa hoặc phiên bản alpha của nó không còn khớp với tài liệu hiện có.
Bước tiếp theo
Plugin buildSrc này sẽ được trích xuất thành một dự án độc lập được xuất bản trên Gradle Plugin Portal hoặc Maven Local. Khi đó, buildscript tiêu thụ sẽ trở thành:
plugins { id("slides") version "1.0.0" }
Et settings.gradle.kts`sẽ được giảm xuống mức tối thiểu tuyệt đối mà không tham chiếu đến`foojay-resolver-convention, việc cấp phát (provisioning) JDK được quản lý bởi chính plugin hoặc được ghi chú là điều kiện tiên quyết.