将 Asciidoctor RevealJS 插件迁移到 buildSrc Kotlin : 对 Groovy API 进行逆向工程
发布于 16 April 2026
背景与目标
起点
项目`slider-gradle`生成来自 AsciiDoc 文件的 Reveal.js 演示文稿,通过 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>()
该插件必须完全自治:它自行应用其插件依赖,配置其仓库,并管理其 Ruby gems。
第一个障碍:`ruby { gems() }
Groovy语法糖所隐藏的
这条线`repositories { ruby { gems() } }`这是一个仅在 buildscript 执行上下文中可用的 Groovy DSL 扩展。它不存在为可从 buildSrc 访问的静态 Kotlin API。
在尝试从这里调用它`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() }`实际上正在执行三个不同的操作:
-
保存指向 Ivy 的仓库`https://rubygems.org/gems/`
-
排除该组`rubygems`Maven 仓库以避免冲突
-
在配置中保存 gem`asciidoctorGems`为了让 JRuby 在运行时加载它
这三个责任必须在 Kotlin 中分别复制。
三部分的解决方案
第一部分: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") }
}
| 1 | 扩展名`.gem`必须硬编码。[ext]`默认情况下解决为.jar`, 这会导致错误`Resource missing`. |
注意:Ivy 仓库已直接声明在`project.repositories`并且不在一个块中`repositories { }`因为这个块的接收器不是`RepositoryHandler`Gradle 标准但一个怪异的 API 与扩展不兼容`ivy`Kotlin DSL。
第二部分:asciidoctorGems 依赖
插件`org.asciidoctor.jvm.gems`必须首先应用 — 它创建配置`asciidoctorGems`和任务`asciidoctorGemsPrepare`:
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。 <2> 限定词`@gem`强制在依赖项上使用正确的扩展 |
第3部分 : settings.gradle.kts
`dependencyResolutionManagement`在`settings.gradle.kts`必须允许项目声明他们自己的仓库 :
@Suppress("UnstableApiUsage")
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
}
如果没有这一行,Gradle 会忽略插件中声明的仓库,并且 gem 的解析会失败。
通过字节码对 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;
JavaForkOptions (grolifant) 的 API
`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`不能被重新分配)。 |
RevealJS扩展
`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")
}
}
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 守护进程必须在 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."
)
}
最终结果
消费者的 buildscript
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 |
仅限 Groovy DSL 扩展 |
三个独立的机制: Ivy 仓库 + Maven 排除 + |
Gradle 正在寻找一个`.jar`而不是一个`.gem` |
`[ext]`解决`jar`默认 |
硬编码者`.gem`在 Ivy 模式 + 限定词`@gem` |
|
grolifant 接收器与 Kotlin DSL 不兼容 |
直接调用`project.repositories.ivy { }` |
`javaLauncher`未解决 |
不存在的属性在`AsciidoctorJRevealJSTask` |
|
`executable = …`不编译 |
属性`val`在`JavaForkOptions`大象 |
方法`executable(Object)`代替 |
|
这是一个 Gradle 服务,而不是一个插件 |
|
`revealjs { }`在任务中未解决 |
项目扩展,而非任务方法 |
|
构建在 Java 25 中崩溃 |
Kotlin 2.0.x 不能解析两位数的 Java 版本 |
自动 Docker 检测 + Java 17 回退 |
调查方法:使用 javap 读取未知的 API
原则
当文档缺失或不完整时,字节码是事实的来源。 javap`是 JDK 的标准工具,用于反编译文件.class`在可读的 Java 签名中 无需源代码。
步骤1:在Gradle缓存中定位jar
Gradle 下载所有依赖到`~/.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`为了识别项目扩展 |
此方法适用于任何其 API 未被文档化的 Gradle 插件。 或者其 alpha 版本不再符合现有的文档。
下一步
此 buildSrc 插件将被提取到已发布在 Gradle Plugin Portal 或 Maven Local 的独立项目中。消费者的 buildscript 将变为:
plugins { id("slides") version "1.0.0" }
Et settings.gradle.kts`将被减至其最低限度,且不作任何参考`foojay-resolver-convention, JDK 配置由插件本身管理或记录为前提条件。
相关文章
31 May 2026
14 May 2026
12 May 2026