跳到主要内容

JEP 467:Markdown 文档注释

QWen Max 中英对照 JEP 467: Markdown Documentation Comments

总结

启用 JavaDoc 文档注释以 Markdown 格式编写,而不是仅仅使用 HTML 和 JavaDoc @ 标签的混合形式。

目标

  • 通过在文档注释中引入使用 Markdown 语法的能力,使 API 文档注释更易于编写且在源代码形式下更易读,同时支持 HTML 元素和 JavaDoc 标签。

  • 不对现有文档注释的解释产生负面影响。

  • 扩展 编译器树 API,以帮助其他分析文档注释的工具处理这些注释中的 Markdown 内容。

非目标

  • 目标并不是实现将现有文档注释自动转换为 Markdown 语法。

动机

文档注释是出现在源代码中的格式化注释,靠近它们所要记录的声明。Java 源代码中的文档注释使用 HTML 和自定义 JavaDoc 标签 的组合来标记文本。

在 1995 年,选择 HTML 作为标记语言是合理的。HTML 功能强大、标准化,并且在当时非常流行。但是,虽然如今作为 Web 浏览器使用的标记语言,它依然同样流行,但自 1995 年以来,HTML 作为一种由人工手动编写的标记语言,其受欢迎程度大大降低,因为编写起来很乏味,阅读也很困难。如今,它更多地是从其他更适合人类的标记语言生成的。由于 HTML 编写起来很繁琐,格式优美的文档注释也同样繁琐,而且更为麻烦的是,由于 HTML 作为一种人工生成的格式逐渐衰落,许多新开发人员对 HTML 并不熟悉。

内联 JavaDoc 标签,例如 {@link}{@code},也显得繁琐,开发者对其熟悉程度更低,常常需要作者查阅文档才能正确使用。最近对 JDK 源代码中的文档注释进行的一项分析表明,超过 95% 的内联标签使用场景是用于代码片段以及指向文档其他部分的链接。这表明,更简单的构造形式将会受到欢迎。

Markdown 是一种流行的标记语言,适用于简单的文档,易于阅读、易于编写,并且可以轻松转换为 HTML。文档注释通常不是复杂的结构化文档,对于文档注释中常见的构造(如段落、列表、样式文本和链接),Markdown 提供了比 HTML 更简单的形式。对于那些 Markdown 不直接支持的构造,也可以使用 HTML。

引入在文档注释中使用 Markdown 的能力将会融合两者的优势。它将为最常见的结构提供简洁的语法,减少对 HTML 标记和 JavaDoc 标签的需求,同时保留使用专门标签来实现 Markdown 中不具备功能的能力。这将使编写和阅读源代码中的文档注释变得更加容易,同时保留生成与以前相同类型的生成 API 文档的能力。

描述

作为在文档注释中使用 Markdown 的示例,可以参考 java.lang.Object.hashCode 的注释:

/**
* Returns a hash code value for the object. This method is
* supported for the benefit of hash tables such as those provided by
* {@link java.util.HashMap}.
* <p>
* The general contract of {@code hashCode} is:
* <ul>
* <li>Whenever it is invoked on the same object more than once during
* an execution of a Java application, the {@code hashCode} method
* must consistently return the same integer, provided no information
* used in {@code equals} comparisons on the object is modified.
* This integer need not remain consistent from one execution of an
* application to another execution of the same application.
* <li>If two objects are equal according to the {@link
* #equals(Object) equals} method, then calling the {@code
* hashCode} method on each of the two objects must produce the
* same integer result.
* <li>It is <em>not</em> required that if two objects are unequal
* according to the {@link #equals(Object) equals} method, then
* calling the {@code hashCode} method on each of the two objects
* must produce distinct integer results. However, the programmer
* should be aware that producing distinct integer results for
* unequal objects may improve the performance of hash tables.
* </ul>
*
* @implSpec
* As far as is reasonably practical, the {@code hashCode} method defined
* by class {@code Object} returns distinct integers for distinct objects.
*
* @return a hash code value for this object.
* @see java.lang.Object#equals(java.lang.Object)
* @see java.lang.System#identityHashCode
*/

同样的注释也可以通过用 Markdown 表达其结构和样式来编写,不使用 HTML,只需几个 JavaDoc 内联标签:

/// Returns a hash code value for the object. This method is
/// supported for the benefit of hash tables such as those provided by
/// [java.util.HashMap].
///
/// The general contract of `hashCode` is:
///
/// - Whenever it is invoked on the same object more than once during
/// an execution of a Java application, the `hashCode` method
/// must consistently return the same integer, provided no information
/// used in `equals` comparisons on the object is modified.
/// This integer need not remain consistent from one execution of an
/// application to another execution of the same application.
/// - If two objects are equal according to the
/// [equals][#equals(Object)] method, then calling the
/// `hashCode` method on each of the two objects must produce the
/// same integer result.
/// - It is _not_ required that if two objects are unequal
/// according to the [equals][#equals(Object)] method, then
/// calling the `hashCode` method on each of the two objects
/// must produce distinct integer results. However, the programmer
/// should be aware that producing distinct integer results for
/// unequal objects may improve the performance of hash tables.
///
/// @implSpec
/// As far as is reasonably practical, the `hashCode` method defined
/// by class `Object` returns distinct integers for distinct objects.
///
/// @return a hash code value for this object.
/// @see java.lang.Object#equals(java.lang.Object)
/// @see java.lang.System#identityHashCode

(出于示例目的,为方便前后对比,刻意避免了重新排版文本等美化性更改。)

需要观察的关键差异:

  • 使用 Markdown 的标志是一种新型的文档注释形式,其中每一行都以 /// 开头,而不是传统的 /** ... */ 语法。

  • 不需要 HTML 的 <p> 元素;空行表示段落的分隔。

  • HTML 的 <ul><li> 元素被 Markdown 的项目符号列表标记所取代,使用 - 来表示列表中每个条目的开始。

  • HTML 的 <em> 元素被下划线 (_) 所取代,用于表示字体的变化。

  • {@code ...} 标签的实例被反引号(`...`)所取代,以表示等宽字体。

  • 用于链接到其他程序元素的 {@link ...} 实例被扩展形式的 Markdown 参考链接 所取代。

  • 块标签的实例,例如 @implSpec@return@see,通常不受影响,只是这些标签的内容现在也使用 Markdown,例如这里 @implSpec 标签内容中的反引号。

这是一张屏幕截图,突出显示了两个版本之间的差异,并排显示:

差异的截图

使用 /// 来编写 Markdown 文档注释

我们使用 /// 来编写 Markdown 注释,以克服传统 /** 注释的两个问题。

  • /* 开头的块注释不能包含字符序列 */JLS §3.7)。在文档注释中放入代码示例变得越来越普遍。这种限制排除了包含嵌入式 /*...*/ 注释或包含字符 */ 的表达式的示例,除非使用破坏性解决方法。

    // 注释中,对行中其余部分可能出现的字符没有任何限制。

  • 在传统的以 /** 开头的文档注释中,每行开头使用空格后跟一个或多个星号是可选的。当注释行省略这些星号时,会与 Markdown 构造产生歧义,例如强调、列表项和主题分隔符。

    /// 注释中,永远不会出现这种歧义。

改变 Java 语言的语法以允许新的注释形式并不是一个选项。因此,任何新的文档注释样式必须采用传统的 /* ... */ 块注释形式,或者一系列 // 行尾注释形式。

上述几点证明了使用行尾注释代替传统注释的合理性,但如何区分文档注释和其他行尾注释的问题仍然存在。我们使用了一个额外的 /,这与在传统文档注释开头使用额外的 * 相呼应。此外,虽然这不是主要考虑因素,但其他支持行尾文档注释的语言,例如 C#DartRust,都成功地使用 /// 作为文档注释已有相当长的一段时间。

语法

Markdown 文档注释是用 CommonMark 变体的 Markdown 编写的。链接的增强功能允许方便地链接到其他程序元素。支持简单的 GFM 管道表,并且支持所有 JavaDoc 标签。

你可以通过使用 Markdown 参考链接 的扩展形式,创建一个指向 API 中其他地方声明的元素的链接,其中参考的标签来源于标准 JavaDoc 引用 到该元素本身。

要创建一个简单链接,其文本来源于元素的标识符,只需用方括号将对该元素的引用括起来即可。例如,要链接到 java.util.List,可以写成 [java.util.List],或者如果代码中存在针对 java.util.Listimport 语句,则只需写成 [List]。链接的文本将以等宽字体显示。该链接等同于使用标准的 JavaDoc {@link ...} 标签。

你可以链接到任何类型的程序元素:

/// - a module [java.base/]
/// - a package [java.util]
/// - a class [String]
/// - a field [String#CASE_INSENSITIVE_ORDER]
/// - a method [String#chars()]

要使用替代文本创建链接,请使用格式 [text][element]。例如,要创建一个指向 java.util.List 且文本为 a list 的链接,可以写成 [a list][List]。该链接将以当前字体显示,尽管您可以在文本中使用格式化标记。此链接等同于使用标准的 JavaDoc {@linkplain ...} 标签。

例如:

/// - [the `java.base` module][java.base/]
/// - [the `java.util` package][java.util]
/// - [a class][String]
/// - [a field][String#CASE_INSENSITIVE_ORDER]
/// - [a method][String#chars()]

在参考链接中,必须转义方括号的任何使用。这可能出现在对带有数组参数的方法的引用中;例如,你会将指向 String.copyValueOf(char[]) 的链接写作 [String#copyValueOf(char\[\])]

你可以使用所有其他形式的 Markdown 链接,包括链接到 URL 的链接,但链接到其他程序元素的链接可能最为常见。

表格

支持简单表格,使用 GitHub Flavored Markdown 的语法。例如:

/// | Latin | Greek |
/// |-------|-------|
/// | a | alpha |
/// | b | beta |
/// | c | gamma |

可能需要出于可访问性的考虑而添加的标题和其他功能均不受支持。在此类情况下,仍然建议使用 HTML 表格。

JavaDoc 标签

JavaDoc 标签,包括行内标签(如 {@inheritDoc})和块标签(如 @param@return),可以在 Markdown 文档注释中使用:

/// {@inheritDoc}
/// In addition, this methods calls [#wait()].
///
/// @param i the index
public void m(int i) ...
QWen Max 中英对照 JEP 467: Markdown Documentation Comments

JavaDoc 标签不能在文字文本中使用,例如 代码跨度`...`)或代码块,即 缩进 的文本块或包含在 围栏 内的文本块,如 ```~~~。换句话说,字符序列 @...{@...} 在代码跨度和代码块中没有任何特殊含义:

/// The following code span contains literal text, and not a JavaDoc tag:
/// `{@inheritDoc}`
///
/// In the following indented code block, `@Override` is an annotation,
/// and not a JavaDoc tag:
///
/// @Override
/// public void m() ...
///
/// Likewise, in the following fenced code block, `@Override` is an annotation,
/// and not a JavaDoc tag:
///
/// ```
/// @Override
/// public void m() ...
/// ```

对于那些可能包含带标记文本的标签,在 Markdown 文档注释中,该标记也使用 Markdown:

/// @param l   the list, or `null` if no list is available

{@inheritDoc} 标签从一个或多个超类型中引入方法的文档。包含该标签的注释格式不需要与包含要继承文档的注释格式相同:

interface Base {
/** A method. */
void m()
}

class Derived implements Base {
/// {@inheritDoc}
public void m() { }
}

用户自定义的 JavaDoc 标签可以在 Markdown 文档注释中使用。例如,在 JDK 文档中,我们定义并使用了 {@jls ...} 作为链接到 Java 语言规范的简写形式,以及像 @implSpec@implNote 这样的块标签来引入特定信息的部分:

/// For more information on comments, see {@jls 3.7 Comments}.
///
/// @implSpec
/// This implementation does nothing.
public void doSomething() { }

独立的 Markdown 文件

doc-files 子目录中的 Markdown 文件会以与这些目录中的 HTML 文件类似的方式进行适当处理。此类文件中的 JavaDoc 标签会被处理。页面标题是从第一个标题推断出来的。不支持像 Pandoc Markdown 处理器所支持的 YAML 元数据。

包含生成的顶级 概览页面 内容的文件也可以是 Markdown 文件。

语法高亮和嵌入式语言

fenced code block 的开头栏后可以跟随一个 info string。信息字符串的第一个单词用于生成相应 HTML 中的 CSS 类名,也可能被 JavaScript 库用来启用语法高亮(例如 Prism)和渲染图表(例如 Mermaid)。

例如,结合适当的库,这将显示带有语法高亮的 CSS 代码片段:

/// ```css
/// p { color: red }
/// ```

你可以通过使用 javadoc--add-script 选项,将 JavaScript 库添加到你的文档中。

语法细节

由于 Markdown 文本每一行开头和结尾的水平空白可能具有重要意义,因此 Markdown 文档注释的内容确定方式如下:

  • 每一行的前导空白字符和三个初始的 / 字符将被移除。

  • 通过移除前导空白字符,行会被向左移动,直到具有最少前导空白的非空行不再有剩余的前导空白。

  • 每一行中额外的前导空白和任何尾随空白都会被保留,因为它们可能具有重要意义。例如,行首的空白可能表示一个缩进代码块或列表项的延续,而行尾的空白可能表示一个硬换行

(移除前导附带空白的策略与 String.stripIndent() 类似,只是无需处理尾部的空行。)

对于每行注释中 /// 后可能出现的字符没有任何限制。特别是,注释可能包含代码示例,而这些代码示例可能包含其自身的注释:

/// Here is an example:
///
/// ```
/// /** Hello World! */
/// public class HelloWorld {
/// public static void main(String... args) {
/// System.out.println("Hello World!"); // the traditional example
/// }
/// }
/// ```

除了有助于从视觉上区分新型的文档注释外,使用行尾(//)注释还消除了传统(/* ... */)注释对注释内容的固有限制。特别是,在传统注释中无法使用字符序列 */JLS §3.7),但在编写包含传统注释、包含全局表达式的字符串以及包含正则表达式的字符串的示例代码时,可能需要这样做。

要包含空行的注释,该行必须以任意可选的空白字符开头,然后是 ///

/// This is an example ...
///
/// ... of a 3-line comment containing a blank line.

完全空白的行会导致其前后的注释被视为独立的注释。在这种情况下,除最后一个注释外的所有注释都会被丢弃,只有最后一个注释会被视为任何后续声明的文档注释:

/// This comment will be treated as a "dangling comment" and will be ignored.

/// This is the comment for the following declaration.
public void m() { }

任何其他未以 /// 开头的注释如果出现在两个 /// 注释之间,情况也是如此。

API 与实现

解析后的文档注释由 Compiler Tree APIcom.sun.source.doctree 包的元素表示。

我们引入了一种新的树节点类型 RawTextTree,它包含未经解释的文本,同时引入了一种新的树节点种类 DocTree.Kind.MARKDOWN,用于指示 RawTextTree 中的 Markdown 内容。我们还向 DocTreeVisitor 及其子类型 DocTreeScannerDocTreePathScanner 添加了相应的新方法 visitRawText

RawTextTree 节点的种类为 MARKDOWN 时,表示 Markdown 内容,包括 HTML 构造,但不包括任何 JavaDoc 标签,例如 {@inheritDoc}@param

Markdown 文本分两个阶段进行处理:

  1. 解析 — Markdown 注释被解析为一系列 RawTextTree 节点,每个节点的类型为 DocTree.Kind.MARKDOWN,并包含 Markdown 内容,其间还穿插了用于内联和块标签的标准 DocTree 节点。内联和块标签的解析方式与传统文档注释相同,只是标签内容也会被解析为 Markdown。节点序列以常规方式存储在 DocCommentTree 节点中。

    与传统文档注释不同,HTML 构造不会被解析为对应的 DocTree 节点,因为需要考虑过多的上下文环境。

    然后,对初始解析后得到的 DocCommentTree 中的 Markdown 内容进行检查,查找任何没有关联 链接引用定义引用链接,并且其 链接标签 在语法上匹配对程序元素的引用。此类链接将被替换为等效节点,表示为 {@link ...}{@linkplain ...}

  2. 渲染javadoc 工具将 DocCommentTree 渲染为适合包含在生成页面中的 HTML。

    任何由 RawTextTree 节点和其他节点组成的序列都会被转换为一个字符串,其中包含 RawTextTree 节点的文本,并用 Unicode 对象替换字符(U+FFFC)代替非 Markdown 内容。生成的字符串由 Markdown 处理器渲染,然后在输出中将 U+FFFC 字符替换为非 Markdown 内容节点的渲染形式。

    尽管大部分渲染过程是直接的,但特别关注的是 Markdown 标题:

    • 标题级别会根据其所在上下文进行调整。这适用于最初在文档注释中以 ATX 样式标题(使用 # 字符前缀表示级别)或 Setext 样式标题(使用 =- 下划线表示级别)编写的情况。

      例如,模块、包或类的文档注释中的 1 级标题在生成的页面中会被渲染为 2 级标题,而字段、构造函数或方法的文档注释中的 1 级标题则会被渲染为 4 级标题。

      此调整仅适用于 Markdown 标题,而不适用于任何直接使用的 HTML 标题。

    • 渲染后的 HTML 中包含一个 id 标识符属性,以便可以从其他地方轻松引用该标题。标识符根据标题的内容生成,方式与其他由 javadoc 生成的标识符相同。(您可以通过点击浏览器中查看标题时弹出的链接图标轻松获取指向标题的链接。)

    • 标题的文本会被添加到生成文档的主搜索索引中。

该实现利用了著名的 commonmark-java 库的内部副本。根据设计,该库的使用不会在任何公开支持的 JDK API 中暴露。

这里描述的大部分功能都是 JDK 的 javadoc 工具和 jdk.javadoc 模块中编译器树 API 的一部分。然而,在标准 Java API 中有一个地方可以观察到文档注释新风格的使用:java.compiler 模块中的方法 javax.lang.model.util.Elements.getDocComment,该方法返回声明的规范化文档注释文本(如果有的话)。我们将更新此方法以涵盖 /// 注释。此外,由于注释的类型会影响其解释,我们将提供一个新方法来确定声明的文档注释是使用传统的 /** ...*/ 块注释形式还是新的 /// 行尾注释形式。

未来工作

可以检测到一些标题的风格化用法,后面跟着适当的内容,并将它们转换为等效的 JavaDoc 标签。

例如,Parameters 标题后跟参数名称及其描述的列表可以转换为等效的 @param 标签:

  • 注释

    # 参数

    * x x 坐标
    * y y 坐标
  • 翻译

    @param x   the x coordinate
    @param y the y coordinate

可以为方法可能抛出的异常列表采用类似的策略:

  • 注释

    # Throws

    * NullPointerException 如果第一个参数是 `null`
    * NullPointerException 如果第二个参数是 `null`
    * IllegalArgumentException 如果参数不被接受
  • 翻译

    @throws NullPointerException      如果第一个参数是 `null`
    @throws NullPointerException 如果第二个参数是 `null`
    @throws IllegalArgumentException 如果参数不被接受

对于一个方法的返回值,应该始终只有一个描述,所以在此情况下无需使用列表:

  • 注释

    # 返回值

    参数的平方根
  • 翻译

    @return 参数的平方根

提议的格式看起来确实像普通的 Markdown,但它们也占据了更多的垂直空间。开发者可能更倾向于使用较为简洁的格式,使用旧式的 JavaDoc 标签。

可能很难将此策略扩展到所有块标签,包括用户指定的标签,但在 JDK 代码库中,仅五个标签(@param@return@see@throws@since)就占了块标签总使用量的 90% 以上。

替代方案

可插拔实现

我们其实可以支持使用其他用户指定的 Markdown 处理器,从而提供不同风格的 Markdown,而不是依赖于某个特定的 Markdown 解析器实现。然而,这种方法在生成跨越不同库的文档时可能会导致不一致性,而带来的好处却微乎其微。

转换更多 Markdown 为 HTML

我们可以将额外的 Markdown 构造转换为等效的 DocTree 节点,以表示纯文本、HTML 和 JavaDoc 标签。尽管这种方法的优点是 API 客户端可能不需要知道注释的原始来源是 Markdown,但它也有一些缺点:

  • 表示形式与原始语法树的距离越远,一旦有必要,就越难提供准确且相关的诊断信息。例如,如果原始注释中没有明确存在这样的项,关于合成的 <table> 元素的消息可能会令人困惑。

  • 当为从 Markdown 结构派生的 HTML 元素合成 DocTree 节点时,很难给出准确的位置信息,将该节点与其在原始注释中的位置相关联,因为该节点在原始注释中没有表示形式。充其量,你可以给出一个附近的位置。这个问题在 Java 编译器 javac 中有一个类似的情况,即在为合成元素(例如默认的无参构造函数或桥接方法)分配位置时。

  • 一个通用的解决方案是困难的,因为它需要了解可能涉及的所有 JavaDoc 标签的知识,因为许多标签允许富内容(如 Markdown 或 HTML)作为其部分内容而非全部内容。

    例如,@param 标签后跟一个参数名称,然后才是描述,如果该名称是类型参数的名称,则该名称可能被包含在 <...> 中。将该名称解释为 HTML 的片段是错误的。同样,@serialField 标签后面跟着一个名称和类型,然后才是描述。虽然这些是标准 doclet 已知的标准标签,但 doclet 也允许使用用户定义的标签。

行内标签

虽然大多数块标签的用途可以通过样式化的标题和后续内容来替代,但对于大多数不太常见的行内标签,却没有这样的等价物。在这些标签中,{@inheritDoc} 是最常用的,而且在 Markdown 中没有明显的类似标签。与其为了替代而发明一种新的语法,似乎最好还是继续沿用现有的行内标签语法。

/**...*/ 注释中的 Markdown

上文所述,使用 /// 来编写文档注释有许多优势。抛开这些原因不谈,如果我们想要解析嵌入在传统的 /**...*/ 注释中的 Markdown,而不是或者额外引入 /// 注释,那么有两种可能性:要么将所有现有的 /** 注释都视为 Markdown 注释,要么在每个 /** 注释中编码一种方式来区分 Markdown 注释和传统注释。

将现有的注释视为 Markdown 是不可行的,因为 Markdown 和 HTML 是具有不同语法规则的不同语言。在 HTML 中,空白仅在 <pre> 元素中的文本字面量时才有意义。相比之下,在 Markdown 中,垂直空白可能表示段落分隔,前导水平空白可能表示缩进的代码块或嵌套列表,尾随空白可能表示硬换行,等同于 HTML 中的 <br>。此外,规则 在 Markdown 文档中使用 HTML 时有些复杂且不直观。最后,JDK 代码中有许多在叙述性文本中使用方括号的例子,这些方括号可能会被误解为指向程序元素的链接;例如,信息以二维数组的形式返回 (array[x][y])

在每个 /** 注释中对文档注释的类型进行编码是可行的,但并不理想。例如,我们可以在初始的 /** 后放置一个短字符串,以指示后续文本何时应被视为 Markdown:

/**md
* Hello _World!_
*/

当我们为这种方法制作原型时,它普遍不受欢迎,在小评论中被认为过于侵扰,而在大评论中又显得无足轻重。

可配置的注释样式

我们可以构建一个可配置的系统,接受一些 /** ... */ 文档注释使用 Markdown 格式,而另一些则使用 HTML 格式。然而,目前尚不清楚这样的机制相较于更明确地使用 /// 注释来表示 Markdown 格式的注释,以及继续使用 /** ... */ 来表示 HTML 格式的注释,是否具备任何显著的优势。

风险与假设

  • 该实现采用了一个第三方库 commonmark-java 来将 Markdown 转换为 HTML。如果该库停止维护,那么我们将不得不为其在 JDK 中的使用维护一个分支,或者寻找一个等效的替代方案。

  • 由于检查错误代码的能力降低,并且作者有时会忘记检查其文档的生成形式,因此生成的 API 文档中可能会出现更多错误。

    例如,在传统的文档注释中,如果某段落包含未终止的 code 标签(如 {@code abc),当调用 JavaDoc 时会发出诊断消息,并在生成的文档中显示为 ▶ 无效的 @code。而在 Markdown 中,等效的未闭合代码片段 `abc 被指定为按字面文本处理,并会以此形式显示,且不会产生相应的诊断消息。