Keycloak 文档生成管线解析:FreeMarker + AsciiDoc 指南构建系统的工作原理
Keycloak 的用户指南(Guides)并不是“手写死”的静态文档,而是由一条 Maven 驱动的生成管线自动构建:FreeMarker 模板先把带宏的 AsciiDoc 渲染为纯 AsciiDoc,再由 Asciidoctor 插件转成 HTML。本篇基于 docs/guides/GENERATE-DOCS.md 的核心说明,结合 docs/maven-plugin 中的实现源码,完整讲清这条管线的构建命令、产物结构、模板解析规则与配置项注入机制,读完你可以独立完成指南构建,也能在本地脱离 Maven 调试渲染过程。
整体架构:两级渲染管线
文档生成的核心思路是“FreeMarker 模板 → 纯 AsciiDoc → HTML”的两级渲染:
- 模板渲染阶段:Maven 插件
keycloak-guides-maven-plugin遍历docs/guides下各个指南目录中的.adoc文件,将其作为 FreeMarker 模板处理,输出“纯 AsciiDoc 生成版本”到target/generated-guides/; - 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 方法):
- 通过
getSourceDirs()列出docs/guides下的子目录,过滤掉src、target、templates三个目录(见 getSourceDirs); - 对
images目录走特殊的复制逻辑,把图片原样拷贝到target/; - 对其余每个指南目录(如
server、operator、migration、getting-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 宏支持的完整参数集:title、summary、priority(默认 999)、deniedCategories、includedOptions、excludedOptions、preview、tileVisible(默认 "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 构造函数):
ctx:Context对象,封装了上面三个 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.adocinclude 路径(这也是 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 构造逻辑)做了两件大事:
- 收集 Quarkus 配置映射器:遍历
PropertyMappers.getMappers()与getWildcardMappers(),过滤掉隐藏项(isHidden())和无描述项,把每个映射器映射为一个Option(键名、分类OptionCategory、是否 build-time、类型、描述、默认值、期望取值、是否严格校验取值、enabledWhen条件、废弃元数据DeprecatedMetadata、通配符键),按分类归入TreeSet排序存储; - 收集 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 的多组执行,每组对应一个指南目录:server、operator、observability、migration、getting-started、high-availability、securing-apps、admin-api、ui-customization,均在 generate-resources 阶段执行 process-asciidoc 目标,从 target/generated-guides/<目录> 读入、输出到 target/generated-docs/<目录>。
全局共享的渲染属性包括:sourceDocumentName=index.adoc(每个指南以 index.adoc 为入口文档)、backend=html5、sourceHighlighter=coderay、toc=left、docinfo1=true、imagesdir=../images,以及一个关键约束:
<attribute-missing>warn</attribute-missing>
配合 logHandler 的 failIf severity=ERROR 配置,意味着任何一个缺失属性被升级报错都会使构建失败——这是文档质量的强制关卡。图片则在更早的 validate 阶段由 maven-resources-plugin 的 copy-images 执行拷贝到 target/generated-docs/images(见 pom.xml 第 52-70 行),与 imagesdir=../images 约定配合。
最后,maven-assembly-plugin 在 package 阶段按 assembly.xml 打包归档,使指南产物随构建一并分发。
小结
Keycloak 指南的文档生成管线是一个典型的“数据驱动文档”实践:keycloak-guides-maven-plugin(GuideMojo / GuideBuilder / FreeMarker / GuideParser / Options)把 API 描述 JSON、CLI/JS 示例 JSON 与服务端真实配置项统一注入到 FreeMarker 模板上下文中,再经 guide.adoc、options.adoc 等宏库渲染为纯 AsciiDoc,最终由 Asciidoctor 转成 HTML。整个过程的构建入口是 mvn clean install -am -pl docs/guides -DskipTests,调试入口是 DocsBuildDebugUtil。理解这条管线后,无论是新增一篇指南、修改宏模板,还是排查渲染失败,都有了明确的代码落点可以对照。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00