Java 库 univer-lib:让 Univer Sheets 与 xlsx 无损双向转换
-
- 一、为什么需要它
- 二、功能矩阵
- 三、集成方式
-
- [3.1 Maven 依赖](#3.1 Maven 依赖)
- [3.2 单一入口](#3.2 单一入口)
- 四、代码样例
-
- [4.1 读:xlsx → IWorkbookData](#4.1 读:xlsx → IWorkbookData)
- [4.2 写:构造 IWorkbookData → xlsx](#4.2 写:构造 IWorkbookData → xlsx)
- [4.3 自定义选项](#4.3 自定义选项)
- [4.4 Spring Boot REST 接口](#4.4 Spring Boot REST 接口)
- [4.5 前端配合(参考)](#4.5 前端配合(参考))
- 五、设计要点(一句话版)
- 六、参考链接
Univer 是越来越火的开源在线表格 SDK,但前端用户常需要把 Excel 文件导进来编辑、再导出回 xlsx。
univer-lib是一个纯 Java 实现的转换库,专门解决 UniverIWorkbookData↔ Excel xlsx 双向高保真 round-trip 的问题。本文介绍它的能力边界、集成方式和典型代码样例。
一、为什么需要它
Univer Sheets 在浏览器里用 IWorkbookData(一份庞大的 JSON
快照)表达整个工作簿:单元格值、样式、富文本、合并区、冻结、批注、条件格式、图表、透视表......应有尽有。
但 Excel xlsx ≠ Univer JSON:
- xlsx 是基于 OOXML 的压缩包,结构规范但缺少 Univer 一些专有字段(比如
resources、appVersion、pdpadding、overline等); - 直接用 Apache POI 写转换器要踩很多坑:样式 64K 上限、共享公式 master 位置、富文本 run 拆分、Drawing 锚点......
- 没有现成的 Java 库能做到 lossless round-trip。
univer-lib 在 POI 之上做了一层封装,引入 OPC sidecar 机制:把 xlsx 无法承载的 Univer 字段写入自定义 OPC 分区
/univer/metadata.json,外部 Excel 打开依然合法,再次读回 Univer 时字段不丢。
二、功能矩阵
下面这些转换器(converter)都已实现并通过单元 + round-trip 测试覆盖:
| 模块 | 对应 Converter | xlsx 侧 | Univer 侧 |
|---|---|---|---|
| 单元格值 / 公式 | CellConverter |
POI Cell | ICellData.v / f / si |
| 样式 + 去重 | StyleConverter |
XSSFCellStyle |
IStyleData(hash 去重,避免 64K 上限) |
| 富文本 | RichTextConverter |
XSSFRichTextString |
IDocumentData(多 run + 段落) |
| 共享公式 | SharedFormulaRegistry |
si 主格右下 |
Univer f + si 协议 |
| 行/列属性 | WorksheetConverter |
行高、列宽、隐藏 | IRowData / IColumnData |
| 合并 / 冻结 / 网格 / RTL / 缩放 | WorksheetConverter |
sheet 属性 | mergeData / freeze / showGridlines ... |
| 批注 | CommentConverter |
XSSFComment | SHEET_NOTE_PLUGIN resource |
| 条件格式 | ConditionalFormattingConverter |
CF rules | SHEET_CONDITIONAL_FORMATTING_PLUGIN |
| 数据验证 | DataValidationConverter |
XSSFDataValidation |
SHEET_DATA_VALIDATION_PLUGIN |
| 定义名称 | DefinedNameConverter |
XSSFName |
IWorkbookData.definedNames |
| 自动筛选 | FilterConverter |
XSSFAutoFilter |
SHEET_FILTER_PLUGIN |
| 表格(Table) | TableConverter |
XSSFTable |
SHEET_TABLE_PLUGIN |
| 图片 | PictureConverter |
DrawingPatriarch + 图片关系 | SHEET_DRAWING_PLUGIN |
| 形状 / 图表(drawing 扩展) | AdvancedDrawingConverter |
XSSFShape / XSSFChart | SHEET_DRAWING_PLUGIN 扩展 |
| 透视表 | PivotTableConverter |
XSSFPivotTable |
SHEET_PIVOT_TABLE_PLUGIN |
这 15 个 converter 覆盖了 Excel 95% 以上的常用业务场景。多 sheet、
sheetOrder排序、隐藏 sheet、tab 颜色都同步保留。
已知限制
- 公式不计算(仅做字符串 round-trip + cached value)
- 长度换算(px ↔ pt ↔ char)为近似值,可能 ±1 px 偏差
- 不支持 xlsm(带宏)和 xlsb(二进制)
strictMode暂为预留开关,未在所有不支持特性处抛出异常
三、集成方式
3.1 Maven 依赖
xml
<dependency>
<groupId>io.github.autoffice</groupId>
<artifactId>univer-lib</artifactId>
<version>1.0.0</version>
</dependency>
JDK 8+ 即可,没有 Spring 依赖,可在任何 Java 项目(普通 main、Servlet、Spring Boot 2.x/3.x、Quarkus、CLI 工具)中使用。
3.2 单一入口
整个库的对外 API 只有 io.github.autoffice.univer.UniverXlsx 这一个门面类,方法都是静态的:
java
public final class UniverXlsx {
public static IWorkbookData read(InputStream in);
public static IWorkbookData read(Path path);
public static IWorkbookData read(InputStream in, UniverXlsxOptions opts);
public static void write(IWorkbookData wb, OutputStream out);
public static void write(IWorkbookData wb, Path path);
public static void write(IWorkbookData wb, OutputStream out, UniverXlsxOptions opts);
}
底层细节(POI、OPC sidecar、Jackson 配置)全部内部封装,调用方只面对 IWorkbookData POJO。
四、代码样例
4.1 读:xlsx → IWorkbookData
java
import io.github.autoffice.univer.UniverXlsx;
import io.github.autoffice.univer.model.IWorkbookData;
import io.github.autoffice.univer.model.ICellData;
import java.nio.file.Paths;
import java.util.Map;
IWorkbookData wb = UniverXlsx.read(Paths.get("input.xlsx"));
// 取第一个 sheet 的 A1 单元格值
String sheetId = wb.getSheetOrder().get(0);
Map<Integer, Map<Integer, ICellData>> cells = wb.getSheets().get(sheetId).getCellData();
Object a1 = cells.get(0).get(0).getV();
System.out.println("A1 = " + a1);
4.2 写:构造 IWorkbookData → xlsx
java
import io.github.autoffice.univer.UniverXlsx;
import io.github.autoffice.univer.model.*;
import java.nio.file.Paths;
import java.util.*;
IWorkbookData wb = new IWorkbookData()
.setId("demo-workbook")
.setAppVersion("0.10.2")
.setLocale("zhCN");
IWorksheetData sheet = new IWorksheetData().setId("s1").setName("Sheet1");
// 写两个单元格:A1 = "Hello",B1 = 42
Map<Integer, ICellData> row0 = new LinkedHashMap<>();
row0.put(0, new ICellData().setV("Hello").setT(CellValueType.STRING));
row0.put(1, new ICellData().setV(42.0).setT(CellValueType.NUMBER));
sheet.getCellData().put(0, row0);
wb.getSheets().put("s1", sheet);
wb.setSheetOrder(Collections.singletonList("s1"));
UniverXlsx.write(wb, Paths.get("output.xlsx"));
4.3 自定义选项
java
UniverXlsxOptions opts = UniverXlsxOptions.builder()
.strictMode(false) // 严格模式(预留)
.writeSidecar(true) // 是否写边车,默认 true。关掉后导出文件给纯 Excel 用更轻
.prettyJson(false) // sidecar JSON 美化
.locale("zhCN")
.build();
UniverXlsx.write(wb, Paths.get("output.xlsx"), opts);
4.4 Spring Boot REST 接口
最常见的场景:浏览器里 Univer 编辑器和后端做 xlsx 互转。下面是一个最简单的 controller:
java
@RestController
@RequestMapping("/api")
public class UniverXlsxController {
private final ObjectMapper mapper = JsonMapper.get(); // 关键:用库提供的 mapper
/** 上传 xlsx → 返回 IWorkbookData JSON */
@PostMapping(value = "/import", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<String> importXlsx(@RequestParam("file") MultipartFile file) throws Exception {
try (InputStream in = file.getInputStream()) {
IWorkbookData wb = UniverXlsx.read(in);
if (wb.getId() == null) {
wb.setId("imported-" + System.currentTimeMillis());
}
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(mapper.writeValueAsString(wb));
}
}
/** 接收 IWorkbookData JSON → 返回 xlsx 字节 */
@PostMapping(value = "/export", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<byte[]> exportXlsx(@RequestBody String body,
@RequestParam(value = "name", defaultValue = "export") String name)
throws Exception {
IWorkbookData wb = mapper.readValue(body, IWorkbookData.class);
ByteArrayOutputStream out = new ByteArrayOutputStream();
UniverXlsx.write(wb, out);
String fileName = URLEncoder.encode(name + ".xlsx", StandardCharsets.UTF_8.name()).replace("+", "%20");
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.parseMediaType(
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"));
headers.set(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + fileName + "\"; filename*=UTF-8''" + fileName);
return ResponseEntity.ok().headers(headers).body(out.toByteArray());
}
}
⚠️ 必须用
JsonMapper.get():库内部有一个IntegerKeyDeserializer,专门处理cellData嵌套 Map的整数字符串键(
"0":{"0":{...}})。Spring Boot 默认的ObjectMapper解析这个结构会失败。
4.5 前端配合(参考)
前端拿到 /api/import 返回的 JSON,直接喂给 Univer:
typescript
const res = await fetch('/api/import', { method: 'POST', body: formData });
const json = await res.json();
univerAPI.createWorkbook(json); // Univer 渲染
// 用户编辑完后导出
const wb = univerAPI.getActiveWorkbook();
const exportJson = wb.save(); // 拿到 IWorkbookData
const xlsxRes = await fetch('/api/export?name=demo', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(exportJson),
});
const blob = await xlsxRes.blob();
// 触发浏览器下载...
五、设计要点(一句话版)
- 分层清晰 :
io → converter → resource → model/util,POI 类型从不泄漏到converter包以上 - Sidecar 模式 :xlsx 无法表达的 Univer 专有字段写入
/univer/metadata.json。读时先加载 sidecar 作为基线,再用 xlsx 内容覆盖;外部
Excel 打开仍合法 - 样式去重 :
StyleConverter用 IStyleData 的 JSON hash 做缓存,避免 POI 64K cell-style 上限 - 共享公式 :
SharedFormulaRegistry保证 master 落在右下角,符合 Univer 协议 - POJO 转发兼容 :所有模型继承
AbstractUniverModel,未知字段进extras,Univer 升级新字段不会让旧库报错
六、参考链接
- GitHub 仓库:https://github.com/autoffice/univer-lib
- 设计文档(权威):https://github.com/autoffice/univer-lib/blob/main/docs/design.md
- 完整 demo(Spring Boot 2.7 + Vue 3 + Univer Sheets):https://github.com/autoffice/univer-lib/tree/main/example
- 后端 demo 源码:https://github.com/autoffice/univer-lib/tree/main/example/backend
- 前端 demo 源码:https://github.com/autoffice/univer-lib/tree/main/example/frontend
- example 启动说明:https://github.com/autoffice/univer-lib/blob/main/example/README.md
- Maven Central:https://central.sonatype.com/artifact/io.github.autoffice/univer-lib
欢迎 issue 反馈和 PR,如果觉得有用别忘了点个 ⭐。