Dokka:生成 Kotlin API 文档

Dokka 是 Kotlin 的 API 文档引擎。它理解 Kotlin 的 KDoc,也能处理 Java 的 Javadoc 注释,适合 Kotlin-only、Java/Kotlin 混合项目和 Kotlin Multiplatform 项目。官方 Dokka 文档说明,它可以生成现代 HTML、Javadoc HTML、GitHub Flavored Markdown、Jekyll Markdown 等格式,并可通过 Gradle、Maven 或命令行运行。

如果你来自 Java,最直接的类比是:

  • Javadoc 面向 Java 语法和 Java API 模型。
  • Dokka 面向 Kotlin 语法和 Kotlin API 模型。
  • Kotlin 的扩展函数、属性、顶层函数、协程挂起函数、泛型型变、多平台 source set 等概念,用 Javadoc 思维很难完整表达。

为什么不能只用 Javadoc

Kotlin 编译到 JVM 后会生成 class 文件,但源码层的 API 形态并不等同于 Java:

  • Kotlin 属性可能对应 getter/setter、字段、构造参数。
  • 顶层函数会编译到文件 facade 类。
  • 扩展函数本质是静态函数,但在 Kotlin 调用侧看起来像成员函数。
  • 默认参数会生成额外字节码结构。
  • suspend fun 在 JVM 层会带 Continuation 参数。
  • 空性是 Kotlin 类型系统的一部分,Java 签名不完整表达。

Dokka 的价值在于按 Kotlin 使用者看到的 API 来生成文档,而不是只展示编译后的 JVM 形态。

KDoc 基础

KDoc 使用 /** ... */ 注释,语法接近 Javadoc,但链接和标签更适合 Kotlin。

/**
 * 根据用户 id 查询用户资料。
 *
 * 如果用户不存在,返回 `null`。调用方应该显式处理空值,
 * 不要使用 `!!` 把缺失用户转换成运行期异常。
 *
 * @param id 数据库中的用户 id,必须为正数。
 * @return 用户资料;如果不存在则为 `null`。
 * @throws IllegalArgumentException 当 [id] 小于等于 0 时抛出。
 */
fun findUser(id: Long): UserProfile? {
    require(id > 0) { "id must be positive" }
    return null
}

常用写法:

  • 用反引号标记代码:`String?`
  • 用方括号链接符号:[UserProfile][findUser]
  • @param 解释参数约束。
  • @return 解释返回值,尤其是可空、空集合、错误状态。
  • @throws 解释异常条件。
  • @sample 关联示例代码。

Java 对比:

/**
 * Finds a user by id.
 *
 * @param id user id
 * @return the user, or null
 */
UserProfile findUser(long id) { ... }

Java 文档经常需要在自然语言里说明 null;Kotlin 文档应该让签名和说明配合:UserProfile? 已经表达可能为空,KDoc 再解释什么时候为空。

Gradle 中使用 Dokka

官方 Dokka Gradle 文档当前以 DGP v2 模式为主,旧的 DGP v1 模式已经不再支持。基本插件配置:

plugins {
    id("org.jetbrains.dokka") version "2.2.0"
}

生成文档:

./gradlew :dokkaGenerate

默认生成 HTML 文档,并放在:

build/dokka/html

如果你只生成 HTML publication,可以使用:

./gradlew :dokkaGeneratePublicationHtml

如果你安装了 Javadoc 输出插件,可以使用:

./gradlew :dokkaGeneratePublicationJavadoc

注意:IDE 中可能还会看到 dokkaGenerateHtml,官方文档说明它是 dokkaGeneratePublicationHtml 的别名。新项目文档和 CI 脚本建议优先使用官方当前推荐的任务名,减少升级歧义。

单项目配置示例

plugins {
    kotlin("jvm") version "2.2.21"
    id("org.jetbrains.dokka") version "2.2.0"
}

dokka {
    dokkaPublications.html {
        moduleName.set("user-service-client")
        outputDirectory.set(layout.buildDirectory.dir("docs/api"))
        includes.from("README.md")
    }

    dokkaSourceSets.main {
        sourceLink {
            localDirectory.set(file("src/main/kotlin"))
            remoteUrl.set(uri("https://github.com/example/user-service-client/tree/main/src/main/kotlin"))
            remoteLineSuffix.set("#L")
        }
    }
}

这类配置解决三个问题:

  • 文档模块名是什么。
  • 输出到哪里。
  • API 页面如何跳转到 GitHub 源码行。

Java 对比:Javadoc 也可以配置 source link,但 Kotlin 项目还要处理 source set、Kotlin DSL、Multiplatform 和非 Java API 形态。

多项目与聚合文档

多模块项目中,Dokka 插件需要应用到要生成文档的子项目。官方文档建议可以通过 convention plugin 共享配置,避免每个模块复制同一段 dokka {}

根项目可以聚合子项目文档:

dependencies {
    dokka(project(":core"))
    dokka(project(":client"))
    dokka(project(":server-api"))
}

适合聚合的项目:

  • 一个库拆成多个模块。
  • Kotlin Multiplatform 项目有 common、jvm、native 等 source set。
  • 公司内部 SDK 想生成统一 API 站点。

不建议聚合的情况:

  • 多个模块属于不同产品,版本节奏不同。
  • 内部模块不应该对外暴露。
  • 文档规模过大,用户反而难以找到入口。

生成 javadoc.jar

发布到 Maven Central 等仓库时,通常需要提供文档 JAR。官方 Dokka Gradle 文档提到,Dokka 插件本身不直接内置一个固定任务,但可以通过自定义 Gradle Jar 任务打包生成结果。

HTML 文档 JAR 示例:

val dokkaHtmlJar by tasks.registering(Jar::class) {
    description = "A documentation JAR containing Dokka HTML"
    from(tasks.dokkaGeneratePublicationHtml.flatMap { it.outputDirectory })
    archiveClassifier.set("html-doc")
}

Javadoc 文档 JAR 示例:

val dokkaJavadocJar by tasks.registering(Jar::class) {
    description = "A documentation JAR containing Dokka Javadoc"
    from(tasks.dokkaGeneratePublicationJavadoc.flatMap { it.outputDirectory })
    archiveClassifier.set("javadoc")
}

库发布时通常会把 sources.jarjavadoc.jar 和主 artifact 一起发布。即使你的主要文档站使用 Dokka HTML,面向 Maven 生态时仍要考虑消费者工具对 Javadoc 格式的期待。

应该写什么文档

API 文档不是把实现解释一遍,而是给调用者稳定的使用契约。建议重点写:

  • 函数做什么,不做什么。
  • 参数约束,例如是否允许空字符串、负数、过期 token。
  • 返回值语义,例如空集合、null、错误码、异常。
  • 线程安全、协程上下文、是否阻塞。
  • 幂等性和副作用,例如是否写数据库、是否发消息。
  • 性能复杂度或资源消耗。
  • 版本兼容性和废弃替代项。
  • 最小可运行示例。

不建议写:

  • “获取用户”这种只重复函数名的注释。
  • 实现细节,例如临时变量如何拼接。
  • 不稳定的内部链接。
  • 与代码不一致的示例。

好的 KDoc 示例

/**
 * 向指定主题发布一条消息。
 *
 * 此函数会挂起直到 broker 确认接收消息。它不会等待消费者处理完成。
 * 如果调用协程被取消,发送操作也会尝试取消,但 broker 可能已经收到消息。
 *
 * @param topic 主题名,不能为空白字符串。
 * @param payload 待发送内容。调用方仍然拥有该对象,函数不会修改它。
 * @return broker 返回的消息 id。
 * @throws IllegalArgumentException 当 [topic] 为空白时抛出。
 * @throws MessagePublishException 当 broker 拒绝消息或网络不可用时抛出。
 */
suspend fun publish(topic: String, payload: ByteArray): MessageId

这段文档比“发布消息”更有价值,因为它说明了:

  • suspend 的完成语义。
  • 取消行为。
  • 参数约束。
  • 返回值来源。
  • 异常条件。

文档与 API 设计一起做

如果一个函数很难写文档,常常说明 API 本身不清楚。

例如:

fun update(id: Long, flag: Boolean)

这很难解释,因为 flag 不表达业务含义。改成:

fun activateUser(id: Long)

fun deactivateUser(id: Long)

文档会自然很多。

或者:

fun search(q: String?, page: Int?, size: Int?): List<User>

可以改成:

data class UserSearchRequest(
    val keyword: String? = null,
    val page: Int = 1,
    val size: Int = 20,
)

fun searchUsers(request: UserSearchRequest): List<User>

KDoc 不应该替糟糕的命名和参数设计背锅。先改 API,再写文档。

对 Java 使用者的文档补充

如果你的 Kotlin 库会被 Java 调用,Dokka 文档里建议明确写出 Java 侧注意事项:

  • 顶层函数在 Java 中如何调用。
  • 默认参数是否提供 @JvmOverloads
  • object 单例在 Java 中是否通过 INSTANCE 访问,或是否提供 @JvmStatic
  • Kotlin UnitNothing、可空类型在 Java 侧如何表现。
  • suspend API 是否提供 Java-friendly 替代入口,例如 CompletableFuture

示例:

/**
 * 创建默认客户端。
 *
 * Kotlin:
 * ```
 * val client = ClientFactory.create()
 * ```
 *
 * Java:
 * ```
 * Client client = ClientFactory.create();
 * ```
 */
object ClientFactory {
    @JvmStatic
    fun create(): Client = Client()
}

CI 中校验文档

库项目建议把 Dokka 放进 CI:

./gradlew check dokkaGenerate

更严格的项目可以:

  • 对公开 API 要求 KDoc。
  • 生成文档后上传为 CI artifact。
  • 发布版本时同时发布 API 文档。
  • 用链接检查工具扫描 README 和 Dokka 输出。
  • 让文档示例尽量来自可编译的 sample,而不是手写在注释里长期失真。

常见误区

误区一:文档生成成功就代表文档有用

Dokka 只负责生成。内容质量仍取决于 KDoc、命名、示例和 API 设计。

误区二:内部 API 和公开 API 使用同一标准

公开 API 要解释兼容性、错误、版本和 Java 互操作;内部 API 可以更轻,但也应该保证关键业务函数有足够上下文。

误区三:KDoc 越长越好

长文档如果重复代码签名,会降低可读性。重点写调用者无法从签名推断的信息。

误区四:Java 用户会自动理解 Kotlin API

Kotlin 的默认参数、顶层函数、扩展函数、object、协程对 Java 用户并不直观。面向 Java 消费者的库应该主动给 Java 示例。

官方参考

  • Dokka Introduction:https://kotlinlang.org/docs/dokka-introduction.html
  • Dokka Gradle:https://kotlinlang.org/docs/dokka-gradle.html