跳到主要内容

JEP 413:Java API 文档中的代码片段

QWen Max 中英对照 JEP 413: Code Snippets in Java API Documentation

总结

为 JavaDoc 的标准文档工具引入 @snippet 标签,以简化在 API 文档中包含示例源代码的操作。

目标

  • 通过提供对这些片段的 API 访问权限,来促进源代码片段的验证。尽管正确性最终由作者负责,但 javadoc 和相关工具中的增强支持可以让验证变得更加容易。

  • 支持现代样式,例如语法高亮显示,以及名称与声明之间的自动链接。

  • 提供更好的 IDE 支持,以便创建和编辑代码片段。

非目标

  • javadoc 工具本身的目标并不是能够验证、编译或运行任何源代码片段。这项任务留给外部工具完成。

  • 虽然我们预计会有平行的工作来完成,但目前的目标并不是提供测试来验证现有 JDK API 文档中的代码片段。

  • 当前的目标并不包括支持交互式代码示例。虽然我们不排除未来会提供此类支持,但任何此类支持都需要外部基础设施,这超出了本提案的范围。

成功指标

  • 展示替换关键 JDK 模块中大部分(如果不是全部)<pre>{@code ...}</pre> 代码块的能力,可以使用新的标签的基本实例来实现,或许可以通过自动化转换工具完成。(审查和提交这些更改,以及手动编辑选定的示例以使用该标签的更高级功能,不在范围之内。)

动机

API 文档的作者经常在文档注释中包含源代码片段。虽然 {@code ...} 本身可以用于小段代码,但非简单的代码片段通常会通过以下复合模式包含在文档注释中:

<pre>{@code
lines of source code
}</pre>
html

javadoc 工具运行此文档注释时,标准 doclet 将生成精确反映 {@code ...} 标签主体的 HTML,包括缩进,并且不会验证代码。例如,java.util.Stream 的源代码 包含一个文档注释,展示了 流的使用

这种方法有各种缺点。

  • 工具无法可靠地检测代码片段,以检查其有效性。此外,这些片段往往不完整,包含占位符注释和省略号,需要读者自行填补空白。由于无法检查每个片段,容易出现错误,并且在实践中经常看到。

  • 使用这种模式的片段无法合理地呈现语法高亮,而如今语法高亮是文档中代码片段的常见期望。片段中没有正式的内容类型指示,而这对于验证片段或使用语法高亮显示是必需的。

  • 使用这种模式的片段无法在 IDE 中作为代码编辑,只能作为注释中的纯文本。此外,并非所有代码结构都可以包含在注释中。例如,传统的 /* ... */ 注释无法包含,因为整个片段是以 Java 注释形式呈现的,而序列 */ 无法在这样的注释中表示。这也意味着字符序列 */ 不能出现在片段中,尽管它可能在 glob 模式和正则表达式中有用。

  • 使用这种模式的片段不能包含 HTML 标记,而这些标记可能用于突出显示文本的某些部分。

  • 使用这种模式的片段不能包含文档注释标签,而这些标签可能用于将名称链接到 API 其他地方的定义。

  • 使用这种模式的片段受制于关于缩进的不灵活规则。这些规则是相对于去掉任何前导空格和星号后的注释行开头定义的。

解决所有这些问题的一个更好的方法是提供一个带有元数据的新标签,该标签允许作者隐式或显式地指定内容的类型,以便对其进行验证并以适当的方式呈现。允许将片段放置在单独的文件中也是很有用的,这些文件可以由作者首选的编辑器直接操作。

描述

@snippet 标签

我们引入了一个新的行内标签 {@snippet ...},用于声明出现在生成文档中的代码片段。它既可以用来声明内联代码片段(代码片段包含在标签本身内),也可以用来声明外部代码片段(代码片段从单独的源文件中读取)。

关于片段的更多细节可以作为属性给出,形式为名称=对,放在初始标签名之后。属性名称始终是一个简单的标识符。属性值可以用单引号或双引号字符括起来;不支持转义字符。属性与标签名之间以及彼此之间通过空白字符(如空格和换行符)分隔。

片段可以指定一个 id 属性,该属性可用于在 API 和生成的 HTML 中识别片段,并且可用于创建指向片段的链接。在生成的 HTML 中,id 将被放置在生成用来表示片段的最外层元素上。

代码片段通常是 Java 源代码,但也可能是属性文件的片段、其他语言的源代码或纯文本。片段可以指定一个 lang 属性,用于标识片段中的内容类型。对于内联片段,默认值为 java。对于外部片段,默认值则根据包含片段内容的文件扩展名推导而来。

在代码片段中,标记标签 可以放置在行注释中,以标识文本中的区域并指示如何呈现文本。(我们将在下面看到标记标签的示例,例如 @highlight@replace。)

行内代码片段

内联代码段包含代码段本身标记中的内容。

下面是一个行内代码片段的示例:

/**
* The following code shows how to use {@code Optional.isPresent}:
* {@snippet :
* if (v.isPresent()) {
* System.out.println("v: " + v.get());
* }
* }
*/
java

代码片段的内容会包含在生成的文档中,它是冒号(:)后换行符与闭合大括号(})之间的文本。(我们预计两个闭合大括号的视觉歧义不会在 API 文档中频繁出现;例如,在 JDK 文档注释中,它仅出现在极少数的源代码片段中。)

不需要使用 HTML 实体对 <>& 等字符进行转义,也不需要对文档注释标记进行转义。

使用 String::stripIndent 方法去除内容的前导空格。这解决了 <pre>{@code ...}</pre> 块的一个恼人缺陷,即要显示的文本总是紧跟在任何前导空格和星号字符之后开始。在代码片段中,生成的输出中的缩进是相对于源文件中右花括号位置的缩进。这与 文本块 中缩进的方式类似,其缩进是相对于闭合 """ 的位置。

内联代码段的内容有两个限制:

  1. 内联代码片段不能使用 /* ... */ 注释,因为 */ 会终止包含它的文档注释。此限制适用于文档注释中的所有内容;它并非 @snippet 标签所特有。

  2. 内联代码片段的内容只能包含成对平衡的大括号字符。整个内联标签会在遇到与起始左大括号匹配的第一个右大括号时终止。此限制适用于所有内联标签;它并非 @snippet 标签所特有。

尽管存在这些限制,当示例代码较短时,使用内联代码片段非常方便,它不需要在 IDE 中提供语言级别的编辑支持,也不需要与文档其他地方的代码片段共享。

外部代码片段

外部代码段是指包含该代码段内容的单独文件。

在外部代码段中,可以省略冒号、换行符和后续内容。

以下是与之前相同的示例,作为外部代码片段:

外部代码片段内容保持不变

/**
* The following code shows how to use {@code Optional.isPresent}:
* {@snippet file="ShowOptional.java" region="example"}
*/
java

其中 ShowOptional.java 是一个包含以下内容的文件:

public class ShowOptional {
void show(Optional<String> v) {
// @start region="example"
if (v.isPresent()) {
System.out.println("v: " + v.get());
}
// @end
}
}
java

{@snippet ...} 标签中的属性标识了要显示的文件以及文件中区域的名称。ShowOptional.java 中的 @start@end 标签定义了该区域的界限。在此示例中,该区域的内容与上一个示例中的内容相同。(有关 @start@end 标签的更多信息,请参见下方。)

与行内片段不同,外部片段对其内容没有限制。特别是,它们可能包含 /* ... */ 注释。

外部代码的位置可以通过类名来指定,使用 class 属性,或者通过简短的相对文件路径来指定,使用 file 属性。在这两种情况下,文件都可以放置在包层次结构中,该层次结构位于包含带有 {@snippet ...} 标签的源代码目录下的 snippet-files 子目录中。另一种方法是,可以将文件放置在由 javadoc 工具的 --snippet-path 选项指定的辅助搜索路径中。使用 snippet-files 子目录的方式类似于当前使用 doc-files 子目录来存放辅助文档文件的方式。

外部代码片段的文件可能包含多个区域,这些区域会被不同的代码片段标签引用,并出现在文档的不同部分。

外部代码片段非常有用,因为它们允许将示例代码写入单独的文件中,这些文件可以直接在 IDE 中编辑,并且可以在多个相关的代码片段之间共享。snippet-files 目录中的文件可以在同一包中的代码片段之间共享,并且与其它包中 snippet-files 目录中的代码片段相互隔离。辅助搜索路径中的文件存在于一个单一的共享命名空间中,并且可以从文档中的任何地方引用。

混合片段

混合代码段既是内部代码段,也是外部代码段。它在标签本身内包含代码段的内容,以便阅读被记录类的源代码的任何人方便,同时也引用包含该代码段内容的单独文件。

如果将混合代码片段作为行内代码片段处理的结果与将其作为外部代码片段处理的结果不匹配,则这是一个错误。

标记标签

标记标签定义了代码片段内容中的区域。它们还控制内容的呈现方式,例如突出显示文本的一部分、修改文本或链接到文档其他地方。它们可以用在内部、外部和混合代码片段中。

标记标签以 @name 开头,后跟任何所需的参数。它们被放置在 // 注释中(或其他语言或格式中的等效注释中),这样可以避免对源代码主体造成不必要的干扰,同时也因为 /* ... */ 注释不能用于内联代码片段。这样的注释被称为 标记注释

多个标记标签可以放在同一个标记注释中。标记标签应用于包含该注释的源代码行,除非注释以冒号(:)结尾,在这种情况下,标记标签仅应用于下一行。如果标记注释特别长,或者代码片段的内容的语法格式不允许注释与非注释源代码出现在同一行时,后一种语法可能很有用。标记注释不会出现在生成的输出中。

由于某些其他系统使用类似于标记注释的元注释,因此以 @ 开头但后面跟着无法识别名称的注释将被忽略。如果名称被识别,但在标记注释中随后出现错误,则会报告错误。在这种情况下,生成的输出相对于从代码片段生成的输出是未定义的。

区域

区域(Regions)是可选命名的行范围,用于标识代码片段要显示的文本。它们还定义了诸如高亮或修改文本等操作的作用范围。

一个区域的起点由以下两者之一标记:

  • @start region=name,或者
  • 一个指定了 regionregion=name@highlight@replace@link 标签。如果匹配的 @end 标签不需要名称,则可以省略该名称。

区域的结束由 @end@end region=name 标记。如果给出了名称,则该标记结束以该名称开始的区域。如果没有给出名称,则该标记结束最近开始的尚未有匹配 @end 标记的区域。

不同的匹配 @start@end 标签对所创建的区域没有任何限制。区域甚至可以重叠,尽管我们预计这种情况并不常见。

高亮

要突出显示一行或一个行范围内的内容,请使用 @highlight,后跟指定要考虑的文本范围、该范围内要突出显示的文本以及突出显示类型的参数。

如果指定了 regionregion=name,那么作用域就是该区域,直到相应的 @end 标签。否则,作用域仅为当前行。

要突出显示范围内每个文字字符串的实例,请使用 substring=string 指定该字符串,其中 string 可以是标识符或用单引号或双引号括起来的文本。要突出显示范围内与正则表达式匹配的每个文本实例,请使用 regex=string。如果未指定这些属性,则会高亮显示整个范围。

可以通过 type 参数指定高亮的类型。有效的类型名称为 bolditalichighlighted。类型名称会被转换为一个 CSS 类名,其属性可以在系统样式表中定义,也可以在用户自定义样式表中被覆盖。

例如,以下是如何使用 @highlight 标签来强调某个特定方法名称的使用:

/**
* A simple program.
* {@snippet :
* class HelloWorld {
* public static void main(String... args) {
* System.out.println("Hello World!"); // @highlight substring="println"
* }
* }
* }
*/
java

这将出现在生成的文档中为:

一个简单的程序。

class HelloWorld \{
public static void main(String... args) \{
System.out.println(_..._);
}
}
java

以下是突出显示某范围行内变量所有引用的方法。我们使用匿名区域来设置操作的范围,并使用正则表达式边界匹配器(\b)仅突出显示所需的变量。

/**
* {@snippet :
* public static void main(String... args) {
* for (var arg : args) { // @highlight region regex = "\barg\b"
* if (!arg.isBlank()) {
* System.out.println(arg);
* }
* } // @end
* }
* }
*/
java

这将在生成的文档中显示为:

public static void main(String... args) {
for (var arg : args) {
if (!arg.isBlank()) {
System.out.println(arg);
}
}
}
java

修改显示的文本

通常,将代码片段的内容编写为可被外部工具访问和验证的代码是很方便的,但要以一种不编译的形式显示它。例如,可能需要包含 import 语句用于说明目的,以及使用导入类型的代码。或者,可能需要用省略号或其他标记来显示代码,以表明在该位置应插入更多代码。这可以通过用一些替换文本替换代码片段的部分内容来实现。

要使用替换文本替换某些文本,请使用 @replace,后跟指定要考虑的文本范围、该范围内要替换的文本和替换文本的参数。

如果指定了 regionregion=name,那么作用域就是该区域,直到相应的 @end 标签。否则,作用域仅为当前行。

要替换范围内每个字面字符串的实例,请使用 substring=string 指定字符串,其中 string 可以是标识符或用单引号或双引号括起来的文本。要替换范围内正则表达式匹配的每个文本实例,请使用 regex=string。如果未指定这些属性,则替换整个范围。

使用 replacement 参数指定替换文本。如果使用正则表达式指定要替换的文本,则可以使用 $number$name 来替换正则表达式中找到的组,具体定义可参考 String::replaceAll

例如,以下是将 println 调用的参数替换为省略号的方法:

/**
* A simple program.
* {@snippet :
* class HelloWorld {
* public static void main(String... args) {
* System.out.println("Hello World!"); // @replace regex='".*"' replacement="..."
* }
* }
* }
*/
java

这将出现在生成的文档中为:

一个简单的程序。

class HelloWorld \{
public static void main(String... args) \{
System.out.println(_..._);
}
}
java

要删除文本,请使用 @replace 并提供一个空的替换字符串。要插入文本,请使用 @replace 替换一些放置在应插入替换文本位置的无操作文本。无操作文本可能是一个 '//' 标记,或是一个空语句(;)。

链接文本

要将文本链接到 API 中其他地方的声明,请使用 @link,后跟指定要考虑的文本范围、该范围内要链接的文本以及链接目标的参数。

如果指定了 regionregion=name,那么作用域就是该区域,直到相应的 @end 标签。否则,作用域仅为当前行。

要链接范围内每个文字字符串的实例,请使用 substring=string 指定该字符串,其中 string 可以是标识符或用单引号或双引号括起来的文本。要链接范围内正则表达式匹配的每个文本实例,请使用 regex=string。如果未指定这些属性,则链接整个范围。

使用 target 参数指定目标。其值的形式与标准内联 {@link ...} 标签所使用的相同。

例如,以下是将文本 System.out 链接到其声明的方法:

/**
* A simple program.
* {@snippet :
* class HelloWorld {
* public static void main(String... args) {
* System.out.println("Hello World!"); // @link substring="System.out" target="System#out"
* }
* }
* }
*/
java

这将出现在生成的文档中为:

A simple program.

class HelloWorld {
public static void main(String... args) {
System.out.println(...);
}
}

(链接的完整目标将取决于生成文档时可用的其他信息。)

其他类型的文件

前面章节中的示例展示了 Java 源代码的片段,但也支持其他类型的文件,例如属性文件。与 Java 源代码的方式完全相同,属性文件格式的代码片段可以用于内联代码段,而使用 file 属性可以在外部代码段中指定属性文件。

下面是一个外部代码段,其中包含 .properties 文件的全部内容:

/**
* Here are the configuration properties:
* {@snippet file="config.properties"}
*/
java

在属性文件中,标记注释使用此类文件的标准注释语法,即以井号(#)字符开头的行。由于某些标记标签的默认作用范围是当前行,并且由于属性文件不允许将注释放在与非注释内容相同的行上,因此可能需要使用以 : 结尾的标记注释形式,以便标记注释被视为应用于下一行。

下面的代码片段定义了一些属性,并突出了第二个属性的值:

/**
* Here are some example properties:
* {@snippet lang=properties :
* local.timezone=PST
* # @highlight regex="[0-9]+" :
* local.zip=94123
* local.area-code=415
* }
*/
java
QWen Max 中英对照 JEP 413: Code Snippets in Java API Documentation

效果就好像标记注释被放置在下一行的末尾,如果行尾注释是合法的话:

/**
* Here are some example properties:
* {@snippet lang=properties :
* local.timezone=PST
* local.zip=94123 # @highlight regex="[0-9]+"
* local.area-code=415
* }
*/
java
QWen Max 中英对照 JEP 413: Code Snippets in Java API Documentation

片段标签参考

属性是提供代码片段标签和标记标签参数的名称-值对。值可以是标识符,也可以是用单引号或双引号括起来的字符串。字符串中的转义序列不受支持。对于某些属性,值是可选的,可以省略。

{@snippet} 标签的属性:

  • class — 包含代码片段内容的类
  • file — 包含代码片段内容的文件
  • id — 代码片段的标识符,用于在生成的文档中识别该片段
  • lang — 代码片段的语言或格式
  • region — 要显示的内容区域的名称

标记标签,出现在标记注释中:

  • start — 标记区域的起点

    • region — 区域的名称
  • end — 标记区域的终点

    • region — 区域的名称;对于匿名区域可以省略
  • highlight — 高亮显示行内或区域内的文本

    • substring — 要高亮显示的字面文本
    • regex — 用于匹配要高亮显示文本的正则表达式
    • region — 定义查找要高亮显示文本范围的区域
    • type — 高亮类型,例如 bolditalichighlighted
  • replace — 替换行内或区域内的文本

    • substring — 要替换的字面文本
    • regex — 用于匹配要替换文本的正则表达式
    • region — 定义查找要高亮显示文本范围的区域
    • replacement — 替换文本
  • link — 链接行内或区域内的文本

    • substring — 要替换的字面文本
    • regex — 用于匹配要替换文本的正则表达式
    • region — 定义查找要高亮显示文本范围的区域
    • target — 链接的目标,以适用于 {@link ...} 标签的形式之一表示
    • type — 链接的类型:link(默认)或 linkplain 中的一种

验证代码片段

能够以编程方式验证片段的内容是很重要的,因为否则内容只是大量的文本,因此容易受到拼写错误和其他人为错误的影响。即使片段中的代码最初是有效的,但随着片段中使用的编程语言和 API 随时间演变,它可能会变得无效。

我们将扩展 Compiler Tree API 以支持 @snippet 标签。这将允许外部工具扫描文档注释中的片段标签,以验证其内容。

通过提供这样的 API,我们并没有将验证的概念限制在 javadoc 工具中可用的支持范围内。相反,我们的目标是支持使用现有的测试基础设施来测试代码片段的内容。

使用外部代码片段文件的一个显著优势是,我们期望这些文件在某种合适的编译上下文中是可编译的。库的测试基础设施将负责定位这些文件,并验证它们是否可以被编译,或许可以使用 Java 编译器 API。同样的基础设施也可能运行生成的类文件。

对于内联代码片段,尤其是那些不是完整编译单元的代码片段,测试基础设施需要负责将代码片段包装成完整的编译单元,以便可以编译并可能运行。

为了验证 JDK API 文档中对片段标签的使用,我们希望在 jtreg 测试框架中提供支持。

生成的 HTML

用于呈现代码片段的 HTML 生成格式并未明确指定,但生成的元素始终会是一个块级元素,例如 div 元素。因此,文档注释中的代码片段标签应始终在允许使用流内容的上下文中使用,而不是在仅允许短语内容的地方使用,例如 spana(即锚点)元素。

为每个代码段生成的 HTML 将声明一个 id 属性,以便该代码段可以作为文档其他位置链接的目标。HTML 中 id 属性的值将是代码段标签中声明的 id 属性的值(如果有的话),否则将使用默认值。

替代方案

  • 各种第三方 JavaScript 解决方案提供了语法高亮功能。然而,JDK API 文档经常包含涉及新语言特性的示例,而这些特性可能无法被这类解决方案及时支持。此外,这类解决方案通常基于正则表达式,这可能会非常脆弱,并且无法在生成文档时利用可用的额外知识。

  • 我们考虑过使用块注释来指定代码片段内容中的标记。然而,用于标记的块注释在源代码中视觉上会显得突兀,并且只能用于外部代码片段。

  • 我们考虑过使用文本块来包裹内联代码片段的内容。然而,这与现有的接受文本内容的内联标签不一致,并且为了遵循文本块的完整规范,这将引入关于转义序列的额外规则。这也会使得在内联代码片段中将文本块用作实际内容变得更加困难。

测试

我们将使用 javadoc 功能的标准测试基础设施来测试此功能,包括 jtreg 测试和相关工具,以检查生成文档的正确性。我们还将在现有文档中将现有的简单 <pre>{@code ...}</pre> 块转换为简单的代码片段。