PlantUML 预处理进阶:Jinja/JSON/YAML 注入与动态生成

puml.online

PlantUML 的 !include 是基础预处理。但企业级架构图常常需要从外部数据源读 service 列表、读端口号、读 AWS 资源 ID 生成图。这篇是 Jinja/JSON/YAML 动态生成实战。

为什么需要动态生成

硬编码 .puml 的痛:

1
2
3
4
5
6
7
8
9
10
11
12
@startuml
component "user-service" as us
component "order-service" as os
component "payment-service" as ps
component "inventory-service" as is
component "notification-service" as ns

us --> os : HTTP 8080
os --> ps : HTTP 8081
os --> is : HTTP 8082
us --> ns : HTTP 8083
@enduml

问题是:新增一个 service 要手动改两处——component 列表 + 连线。新增 service 多到 30+ 个时,改图是噩梦

修法 1:用 !define 抽常量

1
2
3
4
5
6
7
8
9
10
11
!define USER_SVC user-service
!define ORDER_SVC order-service
!define PAYMENT_SVC payment-service
!define INVENTORY_SVC inventory-service
!define NOTIFICATION_SVC notification-service

component USER_SVC as us
component ORDER_SVC as os
component PAYMENT_SVC as ps
component INVENTORY_SVC as is
component NOTIFICATION_SVC as ns

好了一点点——service 名只在一处。但加 service 仍要改多行。

修法 2:用 !procedure 写循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
!procedure $component($name, $alias)
component "$name" as $alias
!endprocedure

!procedure $connect($a, $b, $port)
$a --> $b : HTTP $port
!endprocedure

$component("user-service", us)
$component("order-service", os)
$component("payment-service", ps)
$connect(us, os, 8080)
$connect(os, ps, 8081)
$connect(os, is, 8082)
$connect(us, ns, 8083)
@enduml

!procedure 是 PlantUML 的子程序定义——能抽函数。但 service 列表仍要手写。

修法 3:从 JSON 读(用 !include + !function)

1
2
3
4
5
6
7
8
9
10
// services.json
{
"services": [
{"name": "user-service", "alias": "us", "port": 8080, "deps": ["order", "notification"]},
{"name": "order-service", "alias": "os", "port": 8081, "deps": ["payment", "inventory"]},
{"name": "payment-service", "alias": "ps", "port": 8082, "deps": []},
{"name": "inventory-service", "alias": "is", "port": 8083, "deps": []},
{"name": "notification-service", "alias": "ns", "port": 8084, "deps": []}
]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@startuml
!include services.json

!procedure $render_component($svc)
component $svc.name as $svc.alias
!endprocedure

!procedure $render_connections($svc)
!foreach $dep in $svc.deps
!$depAlias = $dep + "-svc"
$svc.alias --> $depAlias : HTTP
!endforeach
!endprocedure

!foreach $svc in %json()
$render_component($svc)
!endforeach

!foreach $svc in %json()
$render_connections($svc)
!endforeach
@enduml

%json() 魔术变量——把 include 的 JSON 文件解析成字典列表,!foreach 遍历。

注意:JSON 文件名要写完整路径。!include 不会递归处理——只 include 一次。

修法 4:从 YAML 读(用 !include + %yaml())

1
2
3
4
5
6
7
8
9
10
# services.yaml
services:
- name: user-service
alias: us
port: 8080
deps: [order, notification]
- name: order-service
alias: os
port: 8081
deps: [payment, inventory]
1
2
3
4
5
6
7
8
9
10
11
12
13
@startuml
!include services.yaml

!foreach $svc in %yaml()
component $svc.name as $svc.alias
!endforeach

!foreach $svc in %yaml()
!foreach $dep in $svc.deps
$svc.alias --> $dep : HTTP $svc.port
!endforeach
!endforeach
@enduml

%yaml()%json() 同源——底层都走 PlantUML 的 !foreach 上下文。

修法 5:用 Python 预生成 .puml

如果 PlantUML 预处理满足不了(比如需要根据数据库 schema 生成 ER 图),先用脚本生成 .puml,再用 plantuml 编译:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# gen_diagram.py
import yaml, pathlib

config = yaml.safe_load(pathlib.Path("services.yaml").read_text())

with open("architecture.puml", "w") as f:
f.write("@startuml\n\n")
for svc in config["services"]:
f.write(f'component "{svc["name"]}" as {svc["alias"]}\n')
f.write("\n")
for svc in config["services"]:
for dep in svc["deps"]:
f.write(f'{svc["alias"]} --> {dep} : HTTP\n')
f.write("\n@enduml\n")
1
2
python gen_diagram.py
plantuml architecture.puml

优点:

  • 完全编程——能用任何 Python 库(SQLAlchemy 读 schema, boto3 读 AWS 资源, requests 读 HTTP API)
  • 测试简单——脚本是普通 Python,pytest 就能覆盖
  • PlantUML 编译产物保持简洁

缺点:

  • 加一个步骤——CI 要跑 Python 再跑 plantuml
  • 改架构要先改数据源(YAML)再 gen_diagram.py,两步而非 PlantUML !include 的一步

实战:从 K8s 集群自动生成拓扑图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# gen_k8s_diagram.py
import yaml, subprocess
from kubernetes import client, config

config.load_kube_config()
v1 = client.CoreV1Api()

puml_lines = ["@startuml", "left to right direction", ""]

services = {}
for svc in v1.list_service_for_all_namespaces().items:
name = svc.metadata.name
namespace = svc.metadata.namespace
label = f"{namespace}/{name}".replace("-", "_")
services[name] = {"label": label, "namespace": namespace}

puml_lines.extend([
f'component "{svc.metadata.name}" as {services[svc.metadata.name]["label"]}'
for svc in v1.list_service_for_all_namespaces().items
])

puml_lines.append("")
endpoints = v1.list_endpoints_for_all_namespaces()
for ep in endpoints.items:
for subset in (ep.subsets or []):
if not subset.ports:
continue
for port in subset.ports:
target_name = ep.metadata.name
target_label = services[target_name]["label"]
for ref in (subset.addresses or []):
if ref.target_ref and ref.target_ref.kind == "Pod":
pod_name = ref.target_ref.name
puml_lines.append(f'{target_label} --> [{pod_name}] : {port.port}')

puml_lines.append("@enduml")

with open("k8s-topology.puml", "w") as f:
f.write("\n".join(puml_lines))

subprocess.run(["plantuml", "k8s-topology.puml"], check=True)

生成的图反映当前集群状态——加 service 后跑一次 CI,图自动更新。

修法 6:从 OpenAPI/Swagger 生成时序图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# gen_sequence_from_openapi.py
import yaml, pathlib, re

spec = yaml.safe_load(pathlib.Path("openapi.yaml").read_text())
base_url = spec.get("servers", [{}])[0].get("url", "https://api.example.com")

puml = ["@startuml", "participant Client", "participant API", ""]
for path, methods in spec["paths"].items():
for method, op in methods.items():
if method not in ("get", "post", "put", "delete"):
continue
endpoint = f"{method.upper()} {path}"
puml.append(f"Client -> API: {endpoint}")
for code, resp in op.get("responses", {}).items():
puml.append(f"API --> Client: {code}")
puml.append("")

puml.append("@enduml")
pathlib.Path("api-sequence.puml").write_text("\n".join(puml))

生成的时序图覆盖每个 API endpoint——文档跟代码永远同步。

模板继承(!include + !define)

1
2
3
4
5
# _base.puml
!define $service_color #LightBlue
!procedure $svc($name, $alias)
component "$name" <<$service_color>> as $alias
!endprocedure
1
2
3
4
5
6
7
# diagram.puml
!include _base.puml

$svc("user-service", us)
$svc("order-service", os)
us --> os
@enduml

_base.puml模板基础,多个图共用同一个组件样式定义。改色板只改 _base.puml

字符串/数值/日期函数

1
2
3
4
5
6
7
8
9
10
11
12
!function $today()
%date()
!endfunction

!function $build_id()
%substr($GITHUB_SHA, 0, 7)
!endfunction

note as N1
Generated at $today()
Build: $build_id()
end note

%date() 当前日期,%substr(s, start, end) 字符串切片,%str() 转字符串,所有内置函数在 PlantUML Preprocessing 文档

实战踩坑

  • !include 不支持 HTTP——只能 include 本地文件。要 include 远程文件,先用 curl 拉下来或者用 Python 预生成。
  • %json() 在 include 失败时静默——文件路径错也不会报错,只是 !foreach 什么都不执行。CI 加 lint 阶段校验所有 !include 文件存在
  • PlantUML 字符串拼接不自动转数字:"service-" + 1 不会变成 "service-1"——需要 %str($i)
  • !foreach 嵌套超过 3 层会出错——PlantUML 内部栈深度限制。复杂嵌套用 Python 预生成
  • 中文 service 名带连字符:component "user-service" as us 没问题,但 component "用户服务" as user_svc alias 用拼音/英文,PlantUML 别名不支持中文+连字符混合

决策树

1
2
3
4
5
6
需要动态生成吗?
├─ 不需要 → 硬编码 .puml,完事
├─ 需要 5-10 个 service → !procedure 抽函数
├─ 需要 10-50 个 service → JSON/YAML + !foreach
├─ 需要从外部 API/K8s/DB 读 → Python 预生成 .puml
└─ 需要每个 API endpoint 生成时序 → OpenAPI parser + Python

越复杂的场景,越应该把 .puml 当成”模板的产物”——生成它的代码单独维护,而不是在 .puml 里堆 !function

  • 标题: PlantUML 预处理进阶:Jinja/JSON/YAML 注入与动态生成
  • 作者: puml.online
  • 创建于 : 2026-07-30 17:00:00
  • 更新于 : 2026-08-14 21:34:29
  • 链接: https://puml.online/blog/plantuml-preprocessing-jinja/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。