Java 库 univer-lib:让 Univer Sheets 与 xlsx 无损双向转换

Java 库 univer-lib:让 Univer Sheets 与 xlsx 无损双向转换

Univer 是越来越火的开源在线表格 SDK,但前端用户常需要把 Excel 文件导进来编辑、再导出回 xlsx。
univer-lib 是一个纯 Java 实现的转换库,专门解决 Univer IWorkbookData ↔ Excel xlsx 双向高保真 round-trip 的问题。

本文介绍它的能力边界、集成方式和典型代码样例。


一、为什么需要它

Univer Sheets 在浏览器里用 IWorkbookData(一份庞大的 JSON

快照)表达整个工作簿:单元格值、样式、富文本、合并区、冻结、批注、条件格式、图表、透视表......应有尽有。

Excel xlsx ≠ Univer JSON

  • xlsx 是基于 OOXML 的压缩包,结构规范但缺少 Univer 一些专有字段(比如 resourcesappVersionpd padding、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 升级新字段不会让旧库报错

六、参考链接

欢迎 issue 反馈和 PR,如果觉得有用别忘了点个 ⭐。

相关推荐
拂拉氏1 天前
【项目分享-知识讲解】C++标准库string类的模拟实现+KMP算法讲解+哈希思想了解
开发语言·c++·算法·kmp算法·哈希·string类
枫叶丹41 天前
【HarmonyOS 6.0】Graphics Accelerate Kit:AI超帧能力技术解析与实践
开发语言·人工智能·华为·harmonyos
枕星而眠1 天前
C++ 类与对象核心知识点及面试高频题详解
开发语言·c++·面试
2501_930707781 天前
使用C#代码在 PowerPoint 中组合或取消组合形状
开发语言·c#
晚烛1 天前
CANN 调试工具与性能剖析:从日志分析到 NPU 行为追踪的完整调试体系
开发语言·windows·python·深度学习·缓存
惊鸿一博1 天前
图标加载方式_zeroIcon_是否加前缀mdi
开发语言·前端·javascript
王八八。1 天前
linux后台java、postSQL部署命令
java·linux·运维
森G1 天前
TypeScript 基础类型
开发语言·typescript
月落归舟1 天前
MyBatis缓存机制
java·缓存·mybatis