首页
/ Keycloak 文档生成管线解析:FreeMarker + AsciiDoc 指南构建系统的工作原理

Keycloak 文档生成管线解析:FreeMarker + AsciiDoc 指南构建系统的工作原理

2026-09-05 18:11:46作者:蔡丛锟

Keycloak 的用户指南(Guides)并不是“手写死”的静态文档,而是由一条 Maven 驱动的生成管线自动构建:FreeMarker 模板先把带宏的 AsciiDoc 渲染为纯 AsciiDoc,再由 Asciidoctor 插件转成 HTML。本篇基于 docs/guides/GENERATE-DOCS.md 的核心说明,结合 docs/maven-plugin 中的实现源码,完整讲清这条管线的构建命令、产物结构、模板解析规则与配置项注入机制,读完你可以独立完成指南构建,也能在本地脱离 Maven 调试渲染过程。

整体架构:两级渲染管线

文档生成的核心思路是“FreeMarker 模板 → 纯 AsciiDoc → HTML”的两级渲染:

  1. 模板渲染阶段:Maven 插件 keycloak-guides-maven-plugin 遍历 docs/guides 下各个指南目录中的 .adoc 文件,将其作为 FreeMarker 模板处理,输出“纯 AsciiDoc 生成版本”到 target/generated-guides/
  2. HTML 转换阶段asciidoctor-maven-plugin 将每个指南目录渲染为一个独立的 index.html,输出到 target/generated-docs/<指南名>/

正如 GENERATE-DOCS.md 所述,管线的一个关键能力是“把 Configuration 中的选项链接进来并暴露给 FreeMarker 模板”(linking the options from the Configuration to expose them to FreeMarker templates),使得配置参数表可以直接在文档中引用;同时 FreeMarker 宏被大量使用,以保证各指南之间的一致性,并让指南源码本身尽可能精简。

构建命令与产物

在项目根目录执行以下命令即可构建全部指南:

mvn clean install -am -pl docs/guides -DskipTests

其中 -am 会同时构建 keycloak-guides 依赖的上游模块(包括 keycloak-guides-maven-plugin),-pl docs/guides 将构建范围限定在指南模块。构建完成后会产生两类产物(与 docs/guides/pom.xml 中的插件配置一一对应):

  • docs/guides/target/generated-guides:指南的纯 AsciiDoc 生成版本;
  • docs/guides/target/generated-docs/<operator|server|migration|getting-started|admin-api>/index.html:所有指南各生成一个 HTML 文件(由 asciidoctor maven plugins 生成)。

需要说明的是,HTML 布局目前主要作为示例存在,最终文档的呈现形式可能还会调整(原文档明确标注:The layout primarily serves as an example for now and is not how we will eventually present the documentation)。

模板渲染引擎:GuideMojo 与目录发现

docs/guides/pom.xml 中声明了自定义插件的执行:

<plugin>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-guides-maven-plugin</artifactId>
    <version>${project.version}</version>
    <executions>
        <execution>
            <id>generate-asciidoc</id>
            <goals>
                <goal>keycloak-guide</goal>
            </goals>
            <configuration>
                <sourceDir>${project.basedir}</sourceDir>
            </configuration>
        </execution>
    </executions>
</plugin>

Mojo 入口是 GuideMojo@Mojo(name = "keycloak-guide", defaultPhase = LifecyclePhase.GENERATE_SOURCES)),它定义了三个可覆盖的输入参数(见 GuideMojo 参数定义):

参数 默认值 用途
docFile rest/admin-v2/services/target/admin-v2-doc.json Admin REST API v2 的 API 描述文档
cliExamplesFile integration/client-cli/admin-cli/target/admin-v2-cli-examples.json kc.sh 命令行示例
jsExamplesFile js/libs/keycloak-admin-client/src/generated/doc-examples/admin-v2-js-examples.json JS Admin Client 示例

这三类 JSON 正是文档中“把配置/API 信息暴露给模板”的数据来源——它们由其他模块在各自构建阶段生成,因此生成指南前需要先构建这些上游模块(这也是构建命令带 -am 的原因)。

GuideMojo.execute() 的逻辑(见 execute 方法):

  1. 通过 getSourceDirs() 列出 docs/guides 下的子目录,过滤掉 srctargettemplates 三个目录(见 getSourceDirs);
  2. images 目录走特殊的复制逻辑,把图片原样拷贝到 target/
  3. 对其余每个指南目录(如 serveroperatormigrationgetting-started 等),创建对应的 target/generated-guides/<目录名>,并交给 GuideBuilder 执行 FreeMarker 渲染。

Guide 元信息解析:GuideParser 与模板约定

每个指南 .adoc 文件的第一行都需要导入统一的宏库,GuideParser 正是依据这个约定来识别“这是一个指南”并提取其元属性。GuideParser 定义了两个核心正则:

private final Pattern TEMPLATE_IMPORT_PATTERN = Pattern.compile("<#import \"/templates/guide.adoc\" as (?<importName>[^ ]*)>");
private final Pattern GUIDE_ELEMENT_PATTERN = Pattern.compile("(?<key>priority|title|summary|tileVisible|levelOffset)=(\\\"(?<valueString>[^\\\"]*)\\\"|(?<valueInt>[\\d]*))");

也就是说,指南模板必须形如(参考 guide.adoc 宏定义):

<#import "/templates/guide.adoc" as g>

<@g.guide
        title="Getting Started"
        summary="This guide shows you how to get up and running with the Keycloak server"
        priority=100
>
...正文内容...
</@>

guide.adoc 的宏签名 可以看到 guide 宏支持的完整参数集:titlesummarypriority(默认 999)、deniedCategoriesincludedOptionsexcludedOptionspreviewtileVisible(默认 "true")、levelOffset(默认 1)、previewDiscussionLink。宏内部会把这些值落成 AsciiDoc 属性(如 :guide-id::guide-title:),注入 :version:,并 include 共享的 attributes.adoc(见 guide.adoc 宏体)。

GuideParser.parse() 的判定流程:逐行扫描文件,先匹配 <#import ...> 拿到导入别名,再找到 <@别名.guide 开头的宏调用(支持跨行拼接,直到出现 >),最后用 GUIDE_ELEMENT_PATTERN 提取 priority/title/summary/tileVisible/levelOffset 五个元属性并组装成 Guide 对象(见 parse 方法)。不匹配该结构的文件会被判为“非指南”,返回 null

FreeMarker 渲染上下文:GuideBuilder 与全局数据

GuideBuilder.build() 负责单目录的批量渲染:

  • 递归遍历目录下所有 .adoc 文件,排除 partials/ 子目录(partials 是被其他模板 include 的片段,不作为独立模板渲染);
  • 对每个模板调用 freeMarker.template(relativePath, targetDir.getParent()),输出保持与源相同的相对路径结构。

渲染上下文由 FreeMarker 类 构建,采用 FreeMarker VERSION_2_3_31 配置、UTF-8 编码、RETHROW_HANDLER 异常策略(模板出错时直接抛异常而不是静默降级)。每个模板渲染时可用到的全局数据有三类(见 GuideBuilder 构造函数):

  • ctxContext 对象,封装了上面三个 JSON 输入(API 文档、CLI 示例、JS 示例)的查询能力;
  • version:来自 org.keycloak.common.Version.VERSION,即当前构建版本;
  • properties:Maven 项目属性。

此外,FreeMarker.template() 还会为每个模板注入三个隐式变量:

attrs.put("id", id(template));
attrs.put("attributes", "../".repeat(template.getNameCount() - 1) + "attributes.adoc[]");
attrs.put("parent", template.getNameCount() > 2 ? template.getName(1).toString() : "");
  • id:由模板相对路径经 Guide.toId() 生成的锚点标识,用于文档内的 [[${id}]] 章节锚;
  • attributes:按模板深度动态拼接的 attributes.adoc include 路径(这也是 docs/guides/attributes.adoc 会被拷贝到 target/generated-guides/ 的原因,见 pom.xml 中的 copy-attributes 执行);
  • parent:模板所在指南目录名,供顶层模板识别自己属于哪组指南。

源码注释中还特别提到一个跨平台细节:模板名不能直接用 Path 传递,因为在 Windows 上路径分隔符是 \,所以 FreeMarker.template() 第 34-37 行 手动用 / 拼接了模板名。

配置项注入:让文档“引用”服务端真实配置

GENERATE-DOCS.md 强调的核心能力——把 Configuration 的选项链接到模板——由 Options.java 实现。其构造函数(见 Options 构造逻辑)做了两件大事:

  1. 收集 Quarkus 配置映射器:遍历 PropertyMappers.getMappers()getWildcardMappers(),过滤掉隐藏项(isHidden())和无描述项,把每个映射器映射为一个 Option(键名、分类 OptionCategory、是否 build-time、类型、描述、默认值、期望取值、是否严格校验取值、enabledWhen 条件、废弃元数据 DeprecatedMetadata、通配符键),按分类归入 TreeSet 排序存储;
  2. 收集 Provider 配置:通过 Providers.getProviderManager(...) 加载 Provider 工厂,把 provider 级别的配置属性也归入选项体系。

文档侧则由宏消费这些数据:guide 宏接受 includedOptions / excludedOptions / deniedCategories 参数,并在正文结束后调用 <@opts.printRelevantOptions ...>(见 guide.adoc 第 30-32 行),由 options.adoc 中的宏生成对应配置参数表格。由于选项元数据直接取自 PropertyMappers 的实时定义,当服务端新增或修改配置项时,文档中的参数表随之更新,避免了“文档与配置漂移”。配套的还有 kc.adoc(命令行示例宏)、api.adoc(API 章节宏)、experimental-feature.adoc(预览特性标记)等模板,覆盖了不同文档片段的复用需求。

调试:脱离 Maven 运行渲染步骤

生成逻辑出问题时(典型如 FreeMarker 模板语法错误),反复跑完整 Maven 构建效率很低。为此项目提供了 DocsBuildDebugUtil——一个带有 main 方法的工具类,可以在 IDE 中直接运行,在 Maven 之外执行同一个渲染步骤(GENERATE-DOCS.md 原文明确推荐了这个调试方式)。

这也是为什么 docs/guides/pom.xml 第 32-34 行 特意给该模块设置了 <packaging>jar</packaging>,并附有注释:“although this doesn't provide a JAR, this is necessary to call the DocsBuildDebugUtil class from an IDE for debugging”。

HTML 生成阶段:asciidoctor-maven-plugin 的多执行配置

渲染出纯 AsciiDoc 后,docs/guides/pom.xml 配置了 asciidoctor-maven-plugin 的多组执行,每组对应一个指南目录:serveroperatorobservabilitymigrationgetting-startedhigh-availabilitysecuring-appsadmin-apiui-customization,均在 generate-resources 阶段执行 process-asciidoc 目标,从 target/generated-guides/<目录> 读入、输出到 target/generated-docs/<目录>

全局共享的渲染属性包括:sourceDocumentName=index.adoc(每个指南以 index.adoc 为入口文档)、backend=html5sourceHighlighter=coderaytoc=leftdocinfo1=trueimagesdir=../images,以及一个关键约束:

<attribute-missing>warn</attribute-missing>

配合 logHandlerfailIf severity=ERROR 配置,意味着任何一个缺失属性被升级报错都会使构建失败——这是文档质量的强制关卡。图片则在更早的 validate 阶段由 maven-resources-plugincopy-images 执行拷贝到 target/generated-docs/images(见 pom.xml 第 52-70 行),与 imagesdir=../images 约定配合。

最后,maven-assembly-pluginpackage 阶段按 assembly.xml 打包归档,使指南产物随构建一并分发。

小结

Keycloak 指南的文档生成管线是一个典型的“数据驱动文档”实践:keycloak-guides-maven-pluginGuideMojo / GuideBuilder / FreeMarker / GuideParser / Options)把 API 描述 JSON、CLI/JS 示例 JSON 与服务端真实配置项统一注入到 FreeMarker 模板上下文中,再经 guide.adocoptions.adoc 等宏库渲染为纯 AsciiDoc,最终由 Asciidoctor 转成 HTML。整个过程的构建入口是 mvn clean install -am -pl docs/guides -DskipTests,调试入口是 DocsBuildDebugUtil。理解这条管线后,无论是新增一篇指南、修改宏模板,还是排查渲染失败,都有了明确的代码落点可以对照。

登录后查看全文
热门项目推荐
相关项目推荐