一、功能简述
在线工具站(onltool.site)有一个"MySQL 表结构文档"小工具:输入数据库连接信息,点击导出,下载一个排版整齐的 Word 设计文档。
体验:
输入 IP / 端口 / 用户名 / 密码 / 数据库名 → 点导出 → 浏览器下载 doc 文件
文档包含每张表的表名、注释、字段序号、字段名、类型、是否必填、备注,用表格清晰展示。
二、技术选型:为什么用 FreeMarker 而不是 POI?
| 方案 | 复杂度 | 排版控制 | 文件体积 |
|---|---|---|---|
| Apache POI 编程式生成 | 高 | 精确 | 小 |
| FreeMarker 模板填充 | 低 | 灵活 | 小 |
| HTML 另存为 .doc | 低 | 弱 | 大 |
选择 FreeMarker:用 Word XML 模板(.ftl),表结构数据填充进去,生成的 .doc 文件排版精确、体积小、兼容 WPS 和 Office。
三、直连模式的实现
3.1 后端采集数据
java
// 1. 查 information_schema 获取所有表
SELECT TABLE_NAME, TABLE_COMMENT
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = ?
ORDER BY TABLE_NAME
// 2. 每张表查字段结构
SHOW FULL COLUMNS FROM `table_name`
3.2 构建数据模型
java
List<Map<String, Object>> tableList = new ArrayList<>();
for (表) {
Map<String, Object> tableMap = new HashMap<>();
tableMap.put("name", "users(用户表)"); // 表名+注释
List<Map<String, String>> structures = new ArrayList<>();
for (字段) {
Map<String, String> col = new LinkedHashMap<>();
col.put("code", "1"); // 序号
col.put("field", "id"); // 字段名
col.put("type", "int(11)"); // 类型
col.put("isNull", "是"); // 是否必填
col.put("comment", "主键ID"); // 备注
structures.add(col);
}
tableMap.put("structure", structures);
tableList.add(tableMap);
}
3.3 FreeMarker 模板渲染
使用 6000 行的 Word XML 模板(tables.ftl),包含字体、页边距、标题样式、表格边框等全部排版信息。数据填充只占模板的极小部分:
xml
<#list tables as table>
<w:p><w:r><w:t>${table.name}</w:t></w:r></w:p>
<#list table.structure as col>
<w:tr>
<w:tc><w:p><w:r><w:t>${col.code}</w:t></w:r></w:p></w:tc>
<w:tc><w:p><w:r><w:t>${col.field}</w:t></w:r></w:p></w:tc>
...
</w:tr>
</#list>
</#list>
3.4 返回文件
java
ByteArrayOutputStream baos = new ByteArrayOutputStream();
template.process(dataMap, new BufferedWriter(new OutputStreamWriter(baos)));
return ResponseEntity.ok()
.header("Content-Disposition", "attachment; filename*=UTF-8''数据库_表结构.doc")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(baos.toByteArray());
四、Agent 模式:当数据库在内网
云端服务器无法直接连接用户内网的 MySQL。与 SQL 查询工具一样,这里也用 WebSocket + 本地 Agent 隧道。
4.1 第一版:Agent 自己生成 Word(踩坑)
最初让 Agent 连 MySQL、查表结构、生成 HTML 转 Word、base64 回传。问题:
问题 1:样式不一致。 Agent 用的简易 HTML 样式,后端直连用的是 6000 行 XML 模板,两个 Word 文档排版完全不同。
问题 2:消息体过大。 8 张表的 Word 文档 base64 后超过 Spring Boot 默认的 64KB WebSocket 缓冲区上限,服务器直接关闭连接:
The decoded text message was too big for the output buffer
and the endpoint does not support partial messages
4.2 第二版:Agent 只采集数据,后端统一渲染
推翻重来,改为:
Agent 连 MySQL → 查表结构 → 返回 JSON 数据 → 前端 POST 给后端 → 后端 FreeMarker 渲染 → 下载
Agent 只做数据采集:
python
def handle_table_structure(ws, msg):
conn = pymysql.connect(host=host, port=port, user=user, ...)
# 查所有表
cursor.execute("SELECT TABLE_NAME, TABLE_COMMENT FROM information_schema...")
tables = cursor.fetchall()
table_data = []
for table_name, comment in tables:
cursor.execute(f"SHOW FULL COLUMNS FROM `{table_name}`")
columns = cursor.fetchall()
cols = [{"field": c[0], "type": c[1], "required": ..., "comment": c[8]} for c in columns]
table_data.append({"name": table_name, "comment": comment, "columns": cols})
send(ws, {"type": "tableStructureResult", "database": database, "tables": table_data})
后端新增 Agent 模式入口:
java
@PostMapping("/export")
public ResponseEntity<byte[]> export(@RequestBody Map<String, Object> body) {
List<Map<String, Object>> tableList = new ArrayList<>();
// Agent 模式:数据已由 Agent 采集好
if (body.containsKey("tables")) {
for (Map<String, Object> t : (List) body.get("tables")) {
// 构建与直连模式完全相同的数据结构
tableMap.put("name", displayName);
tableMap.put("structure", structures);
tableList.add(tableMap);
}
return renderWord(database, tableList); // 共用 FreeMarker 渲染
}
// 直连模式:后端自己连 MySQL
...
return renderWord(database, tableList);
}
前端中继:
javascript
case 'tableStructureResult':
// 收到 Agent 的 JSON 数据 → 发给后端生成 Word
request({
url: '/api/pub/table-structure/export',
method: 'post',
data: { database: msg.database, tables: msg.tables },
responseType: 'blob'
}).then(res => {
// 触发浏览器下载
const url = window.URL.createObjectURL(new Blob([res]))
const a = document.createElement('a')
a.href = url; a.download = msg.database + '_table_structure.doc'
a.click()
})
break
五、关键坑点
5.1 WebSocket 消息体超限
Spring Boot + Tomcat 默认 WebSocket 文本消息缓冲区 64KB 。数据库表多时,JSON 数据体轻松超标。配置 ServletServerContainerFactoryBean 扩到 50MB:
java
@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
container.setMaxTextMessageBufferSize(50 * 1024 * 1024); // 50 MB
container.setMaxSessionIdleTimeout(0L);
return container;
}
5.2 竞态条件导致结果丢失
Agent 返回 JSON 时,如果浏览器 WebSocket 恰好断开重连,后端 removeBrowser 可能误清掉新连接的 session:
java
// 修复:只清理自己的 session
if (pair.browserSession == browserSession) {
pair.browserSession = null;
}
5.3 样式一致性
Agent 不生成 Word,只回传结构化数据。所有 Word 文档统一走后端 FreeMarker 模板渲染,保证直连和 Agent 两种模式输出的文档排版完全一致。
六、结果展示
小工具页面

导出设计文档截图

七、架构总结
直连模式:前端 → 后端(连MySQL + FreeMarker) → Word blob → 下载
Agent模式:前端 → WebSocket → Agent(连MySQL, 回传JSON) → 前端 → 后端(FreeMarker) → Word blob → 下载
↑
两种模式在此汇合
核心设计原则:Agent 只做它擅长的事(穿透内网),Word 生成永远交给后端的同一套 FreeMarker 模板。
项目在线地址:https://onltool.site