JEP 413: Code Snippets in Java API Documentation | Java API 文档中的代码片段
摘要
为 JavaDoc 的标准 Doclet 引入 @snippet
标签,以简化在 API 文档中包含示例源代码的过程。
目标
通过为源代码片段提供 API 访问,来简化对源代码片段的验证。尽管正确性最终是作者的责任,但
javadoc
和相关工具中的增强支持可以更容易地实现这一点。启用现代样式,如语法高亮显示,以及名称到声明的自动链接。
为创建和编辑代码片段提供更好的 IDE 支持。
非目标
javadoc
工具本身并不负责验证、编译或运行任何源代码片段。这项任务留给外部工具处理。虽然我们期望并行努力会验证现有 JDK API 文档中的代码片段,但提供测试以验证这些代码片段并不是
javadoc
工具的目标。目前不支持交互式代码示例。虽然我们不排除未来提供此类支持,但任何此类支持都需要超出本提案范围的外部基础架构。
成功指标
- 展示在 JDK 关键模块中,使用新标签的基本实例替换大多数(如果不是全部)
<pre>{@code ...}</pre>
块的能力,可能通过自动化转换工具实现。(审查和提交这些更改,以及手动编辑选定示例以使用标签的更高级功能,不在此范围内。)
动机
API 文档的编写者经常在文档注释中包含源代码片段。虽然 {@code ...}
可以单独用于小段代码,但非平凡的片段通常使用以下复合模式包含在文档注释中:
<pre>{@code
源代码行
}</pre>
当 javadoc
工具处理这个文档注释时,标准 doclet 将生成 HTML,该 HTML 精确地反映了 {@code ...}
标签的主体内容,包括缩进,但不会验证代码。例如,java.util.Stream
的源代码 包含一个文档注释,展示了 流的使用。
这种方法存在多种不足。
工具无法可靠地检测代码片段以检查其有效性。此外,这些片段通常是不完整的,包含占位符注释和省略号,供读者填补空白。由于无法检查每个片段,因此很容易出错,并且在实践中经常看到这种情况。
使用此模式的片段无法合理地呈现语法高亮,而语法高亮现在是文档中代码片段的常见期望。如果没有正式指示片段中的内容类型,则无法验证或显示语法高亮。
除了在注释中以纯文本形式编辑外,无法在 IDE 中编辑使用此模式的片段。此外,并非所有代码构造都可以包含在注释中。例如,传统的
/* ... */
注释不能包含在内,因为整个片段都呈现在 Java 注释中,而序列*/
不能在这样的注释中表示。这也意味着字符序列*/
不能用于片段内部,尽管它可能对于 glob 模式和正则表达式很有用。使用此模式的片段不能包含 HTML 标记,这可能用于高亮文本的部分内容。
使用此模式的片段不能包含文档注释标签,这可能用于将名称链接到 API 中其他地方的定义。
使用此模式的片段在缩进方面受到不灵活规则的限制。这些规则 相对于注释行开头定义,在剥离任何前导空白和星号之后。
解决所有这些问题的更好方法是提供一个带有元数据的新标签,允许作者隐式或显式地指定内容类型,以便可以对其进行验证并以适当的方式呈现。此外,允许将片段放置在可由作者首选编辑器直接操作的单独文件中也将非常有用。
描述
@snippet
标签
我们引入了一个新的内联标签 {@snippet ...}
,用于声明要出现在生成的文档中的代码片段。它可以用来声明 内联片段,其中代码片段包含在标签本身内部,以及 外部片段,其中代码片段从单独的源文件读取。
可以通过 属性 的形式提供有关片段的更多详细信息,这些属性以 名称 =
值 对的形式出现,并放置在初始标签名称之后。属性名称始终是一个简单的标识符。属性值可以用单引号或双引号括起来;不支持转义字符。属性名与标签名之间以及属性之间用空白字符(如空格和换行符)分隔。
片段可以指定一个 id
属性,该属性可用于在 API 和生成的 HTML 中标识片段,并可用于创建指向片段的链接。在生成的 HTML 中,id 将被放置在用于表示片段的最外层元素上。
代码片段通常是 Java 源代码,但也可以是属性文件的片段、其他语言的源代码或纯文本。片段可以指定一个 lang
属性,该属性标识片段中的内容类型。对于内联片段,默认值为 java
。对于外部片段,默认值是从包含片段内容的文件名扩展名派生而来的。
在代码片段中,可以在行注释中放置 标记标签 来标识文本中的区域并指示如何呈现文本。(我们将在下面看到标记标签的示例,如 @highlight
和 @replace
。)
内联片段
内联片段包含标签本身内部的片段内容。
以下是一个内联片段的示例:
/**
* 下面的代码展示了如何使用 {@code Optional.isPresent}:
* {@snippet :
* if (v.isPresent()) {
* System.out.println("v: " + v.get());
* }
* }
*/
在生成的文档中,片段的内容是位于冒号(:
)后的换行符与闭合大括号(}
)之间的文本。(我们并不期望在 API 文档中频繁出现两个闭合大括号造成的视觉歧义;例如,在 JDK 文档注释中的源代码片段中,这种情况只出现在少数情况中。)
无需将 <
、>
和 &
等字符转义为 HTML 实体,也无需转义文档注释标签。。
使用 String::stripIndent 方法从内容中删除前导空白。这解决了 <pre>{@code ...}</pre>
块的一个令人烦恼的缺点,即要显示的文本总是紧跟在任何前导空格和星号字符之后。在片段中,生成输出中的缩进是相对于源文件中闭合大括号位置的缩进。这与 文本块 中的缩进相对于闭合 """
位置的方式类似。
内联片段的内容有两个限制:
内联片段不能使用
/* ... */
注释,因为*/
会终止包含它的文档注释。此限制适用于文档注释中的所有内容;它不是@snippet
标签特有的。内联片段的内容只能包含成对的大括号字符。整个内联标签由与开头大括号匹配的第一个右大括号终止。此限制适用于所有内联标签;它不是
@snippet
标签特有的。
尽管存在这些限制,但内联片段在示例代码较短、不需要在 IDE 中获得语言级别的编辑支持、且无需与文档其他部分的片段共享时,非常方便。
外部片段
外部片段是指包含片段内容的单独文件。
在外部片段中,可以省略冒号、换行符和后续内容。
以下是与之前相同的示例,作为外部片段:
/**
* 下面的代码展示了如何使用 {@code Optional.isPresent}:
* {@snippet file="ShowOptional.java" region="example"}
*/
其中 ShowOptional.java
是一个包含以下内容的文件:
public class ShowOptional {
void show(Optional<String> v) {
// @start region="example"
if (v.isPresent()) {
System.out.println("v: " + v.get());
}
// @end
}
}
{@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 开头,后跟任何必需的参数。它们被放置在 //
注释中(或其他语言或格式的等效注释),以免不必要地干扰源代码的主体,并且因为内联片段中不能使用 /* ... */
注释。此类注释称为 标记注释。
可以在同一个标记注释中放置多个标记标签。标记标签适用于包含注释的源代码行,除非注释以冒号(:
)结尾,在这种情况下,标记标签仅适用于下一行。如果标记注释特别长,或者片段内容的语法格式不允许注释与非注释源代码出现在同一行上,则后者语法可能很有用。标记注释不会出现在生成的输出中。
由于其他系统使用与标记注释类似的元注释,因此以 @
开头后跟未识别名称的注释将被忽略。如果名称被识别,但标记注释中存在后续错误,则会报告错误。在这种情况下,与从片段生成的输出相关的生成输出是未定义的。
区域
区域是可选命名的行范围,用于标识片段要显示的文本。它们还定义了诸如高亮或修改文本等操作的范围。
区域的开始由以下任一方式标记:
@start region=
名称,或- 一个指定了
region
或region=
名称 的@highlight
、@replace
或@link
标签。如果匹配的@end
标签不需要名称,则可以省略名称。
区域的结束由 @end
或 @end region=
名称 标记。如果指定了名称,则该标签将结束以该名称开始的区域。如果没有指定名称,则该标签将结束最近开始且尚未有匹配 @end
标签的区域。
由不同的匹配 @start
和 @end
标签对创建的区域没有约束。区域甚至可以重叠,尽管我们不期望这种用法很常见。
高亮
要在行上或在行范围内高亮内容,请使用 @highlight
后跟指定要考虑的文本范围、该范围内要高亮显示的文本以及高亮类型的参数。
如果指定了 region
或 region=
名称,则范围是该区域,直至相应的 @end
标签。否则,范围仅是当前行。
要在范围内高亮显示字面量字符串的每个实例,请使用 substring=
string 指定字符串,其中 string 可以是标识符或用单引号或双引号括起来的文本。要在范围内高亮显示与正则表达式匹配的文本的每个实例,请使用 regex=
string。如果未指定这些属性中的任何一个,则整个范围都将被高亮显示。
可以使用 type
参数指定高亮类型。有效的类型名称是 bold
、italic
和 highlighted
。类型名称将转换为 CSS 类名,其属性可以在系统样式表中定义或在用户定义的样式表中覆盖。
例如,以下是如何使用 @highlight
标签来强调特定方法名称的使用:
/**
* 一个简单的程序。
* {@snippet :
* class HelloWorld {
* public static void main(String... args) {
* System.out.println("Hello World!"); // @highlight substring="println"
* }
* }
* }
*/
在生成的文档中,这将显示为:
一个简单的程序。 class HelloWorld { public static void main(String... args) { System.out.println("Hello World!"); } }
以下是如何在一系列行中高亮显示所有变量引用的方法。我们使用匿名区域来设置操作的范围,并使用正则表达式边界匹配器(\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
* }
* }
*/
在生成的文档中,这将显示为:
public static void main(String... args) { for (var arg : args) { if (!arg.isBlank()) { System.out.println(arg); } } }
修改显示的文本
通常,将代码片段的内容编写为可由外部工具访问和验证的代码很方便,但以不编译的形式显示它可能更为方便。例如,可能希望在代码旁边 包含用于说明目的的 import
语句,同时显示使用这些导入类型的代码。或者,可能希望在代码中显示省略号或其他标记,以指示应在该点插入其他代码。这可以通过用替换文本替换代码片段内容的一部分来实现。
要用替换文本替换某些文本,请使用 @replace
后跟指定要考虑的文本范围、该范围内要替换的文本以及替换文本的参数。
如果指定了 region
或 region=
名称,则范围是该区域,直至相应的 @end
标签。否则,范围仅是当前行。
要在范围内替换字面量字符串的每个实例,请使用 substring=
string 指定字符串,其中 string 可以是标识符或用单引号或双引号括起来的文本。要在范围内替换与正则表达式匹配的文本的每个实例,请使用 regex=
string。如果未指定这些属性中的任何一个,则整个范围都将被替换。
使用 replacement
参数指定替换文本。如果使用正则表达式来指定要替换的文本,则可以使用 $
number 或 $
name 来替换正则表达式中找到的组,如 String::replaceAll 所定义。
例如,以下是如何将 println
调用的参数替换为省略号(ellipsis)的方法:
/**
* 一个简单的程序。
* {@snippet :
* class HelloWorld {
* public static void main(String... args) {
* System.out.println("Hello World!"); // @replace regex='".*"' replacement="..."
* }
* }
* }
*/
在生成的文档中,这将显示为:
一个简单的程序。 class HelloWorld { public static void main(String... args) { System.out.println(...); } }
要删除文本,请使用 @replace
并指定一个空的替换字符串。要插入文本,请使用 @replace
替换应插入替换文本位置处的一些无操作文本。这些无操作文本可能是一个 //
标记,或者是一个空语句(;
)。
文本链接
要将文本链接到 API 中其他地方的声明,请使用 @link
后跟指定要考虑的文本范围、范围内要链接的文本以及链接目标的参数。
如果指定了 region
或 region=
name,则范围是到相应 @end
标签为止的该区域。否则,范围仅为当前行。
要在范围内链接每个字面量字符串的实例,请使用 substring=
string 指定字符串,其中 string 可以是标识符或单引号或双引号括起来的文本。要在范围内链接与正则表达式匹配的每个文本实例,请使用 regex=
string。如果未指定这些属性中的任何一个,则整个范围都将被链接。
使用 target
参数指定目标。其值的形式与标准内联 {@link ...}
标签中使用的相同。
例如,以下是如何将文本System.out
链接到其声明的方法:
/**
* 一个简单的程序。
* {@snippet :
* class HelloWorld {
* public static void main(String... args) {
* System.out.println("Hello World!"); // @link substring="System.out" target="System#out"
* }
* }
* }
*/
在生成的文档中,这将显示为:
一个简单的程序。 class HelloWorld { public static void main(String... args) { System.out.println("Hello World!"); } }
(链接的完整目标将取决于生成文档时可用的其他信息。)
其他类型的文件
前面部分中的示例展示了 Java 源代码的片段,但也支持其他类型的文件,如属性文件。与 Java 源代码完全相同,属性文件格式的代码片段可以在内联代码段中使用,并且可以通过 file
属性在外部代码段中指定属性文件。
以下是一个包含整个.properties
文件内容的外部代码段:
/**
* 以下是配置属性:
* {@snippet file="config.properties"}
*/
在属性文件中,标记注释使用该类文件的标准注释语法,即以井号(#
)字符开头的行。由于某些标记标签的默认范围是当前行,并且因为属性文件不允许在同一行上放置注释和非注释内容,因此可能需要使用以 :
结尾的标记注释形式,以便将标记注释视为适用于下一行。
以下是一个定义了一些属性并突出显示第二个属性值的片段示例:
/**
* 这里是一些示例属性:
* {@snippet lang=properties :
* local.timezone=PST
* # @highlight regex="[0-9]+" :
* local.zip=94123
* local.area-code=415
* }
*/
其效果就像是在以下行的末尾放置了标记注释(如果行尾注释是合法的):
/**
* 这里是一些示例属性:
* {@snippet lang=properties :
* local.timezone=PST
* local.zip=94123 # @highlight regex="[0-9]+"
* local.area-code=415
* }
*/
片段标签参考
属性是名称 - 值对,为片段标签和标记标签提供参数。值可以是标识符,也可以是单引号或双引号括起来的字符串。字符串中的转义序列不受支持。对于某些属性,值是可选的,可以省略。
{@snippet}
标签的属性:
class
— 包含片段内容的类file
— 包含片段内容的文件id
— 片段的标识符,用于在生成的文档中标识片段lang
— 片段的语言或格式region
— 要显示的内容中的区域名称
标记标签,出现在标记注释中:
start
— 标记一个区域的开始region
— 区域的名称
end
— 标记一个区域的结束region
— 区域的名称;对于匿名区域可以省略
highlight
— 突出显示行或区域内的文本substring
— 要突出显示的文字regex
— 要突出显示文本的正则表达式region
— 定义在其中查找要突出显示文本的区域的名称type
— 突出显示的类型,如bold
(粗体)、italic
(斜体)或highlighted
(高亮)
replace
— 替换行或区域内的文本substring
— 要替换的文字regex
— 要替换文本的正则表达式region
— 定义在其中查找要替换文本的区域的名称replacement
— 替换文本
link
— 在行或区域内链接文本substring
— 要替换的文字regex
— 要替换文本的正则表达式region
— 定义在其中查找要链接文本的区域的名称target
— 链接的目标,以适合{@link ...}
标签的形式之一表示type
— 链接的类型:link
(默认)或linkplain
之一
验证片段
能够以编程方式验证片段的内容是非常重要的,因为否则内容就只是大量文本,因此容易受到拼写错误和其他人为错误的影响。即使片段中的代码最初是有效的,但随着时间的推移,随着片段中使用的编程语言和 API 的发展,它可能会变得无效。
我们将扩展 Compiler Tree API 以支持 @snippet
标签。这将允许外部工具扫描文档注释中的片段标签,以验证其内容。
通过提供这样的 API,我们并不限制 javadoc
工具内部可用的验证概念。相反,我们的目标是支持使用现有的测试基础架构来测试片段的内容。
使用外部片段文件的一个显著优势是,我们期望这些文件在某些合适的编译上下文中是可编译的。对于库来说,将由其测试基础架构来定位这些文件并验证它们是否可以编译,可能会使用 Java Compiler API。同一基础架构还可能运行生成的类文件。
对于内联片段,尤其是那些不是完整编译单元的片段,将由测试基础架构将代码片段包装在完整的编译单元中,以便可以对其进行编译和可能的运行。
对于在 JDK API 文档中验证 @snippet
标签的使用,我们希望在 jtreg
测试框架中提供支持。
生成的 HTML
除了生成的元素始终为块级元素(如 div
元素)之外,用于呈现片段的 HTML 是故意未明确指定的。因此,文档注释中的片段标签应始终在允许 流动内容 的上下文中使用,而不是仅在允许 短语内容 的上下文中使用,如 span
或 a
(即锚点)元素。
每个片段生成的 HTML 将声明一个 id
属性,以便该片段可以成为文档中其他位置链接的目标。HTML 中 id
属性的值将是片段标签中声明的 id
属性的值(如果有的话),否则将使用默认值。
替代方案
各种第三方 JavaScript 解决方案提供了语法高亮功能。然而,JDK API 文档经常包含涉及新语言特性的示例,这些特性可能无法及时得到这些解决方案的支持。此外,这些解决方案通常基于正则表达式,这可能会非常脆弱,并且无法利用生成文档时可用的额外知识。
我们考虑过使用块注释来指定片段内容中的标记。但是,用于标记的块注释在源代码中视觉上具有侵入性,并且只能用于外部片段。
我们考虑过使用文本块来包围内联片段的内容。但是,这与接受文本内容的现有内联标签不一致,并且为了遵循文本块的完整规范,它将引入关于转义序列的额外规则。此外,这还将使得将文本块用作内联片段中的实际内容变得更加困难。
测试
我们将使用 javadoc
功能的标准测试基础架构来测试此功能,包括 jtreg
测试和相关工具,以检查生成的文档的正确性。我们还将把现有文档中的简单 <pre>{@code ...}</pre>
块转换为简单的片段。