不升级 POI 版本,如何生成符合新版标准的Excel 2007文件
开心一刻
记得小时候,家里丢了钱,是我拿的,可爸妈却一口咬定是弟弟拿的
爸爸把弟弟打的遍体鳞伤,弟弟气愤的斜视着我
我不敢直视弟弟,目光转向爸爸说到:爸爸,你看他,好像还不服

问题描述
项目基于 POI 4.1.2 生成 Excel 2007 文件,已经对接了很多客户,也稳定运行了好几年了;就在前两天,对接一个新的客户,生成的 Excel 2007 文件导入他们的系统失败,提示:
-700006004当前Excel表单列名中未查找到该列.
实话实说,这个提示对我而言,一毛钱作用没有,那就只能问他们系统的开发人员了;经过半天的排查,他们的开发人员给出的结论是:
你们的Excel 2007文件看着像是旧版的,不符合新版标准
这个回答让我更懵了,触及到我的知识盲区,都不直到如何接话了

Excel 2007 文件还有标准与非标准之分?这个问题我们先不纠结,本着优先解决问题的原则,试着去尝试升级下 POI 的版本
为什么第一时间想到的是升级 POI 版本?因为是用 POI 生成的 Excel 2007 文件嘛(貌似等于没说)
将 POI 版本升级到 5.3.0,代码不做任何调整,重新生成文件发送给客户,客户验证可以正常导入;你们是不是以为事情到此告一段落,升级 POI 版本就好了嘛,我只能说你们是有了新欢忘了旧爱,已经对接的客户怎么办?你敢保证升级 POI 后生成的 Excel 2007(2003 也会跟着受影响)还能正常导入这些客户的系统吗,所以我们的野心能不能更大一些:新欢旧爱都要!

既对已有客户不造成影响,又能满足新客户要求,也就引申出了本文标题
不升级 POI 版本,如何生成符合新版标准的Excel 2007文件
是个压缩包
从 Excel 2007 开始,Microsoft 采用了新的文件格式,称为开放的 XML 文件格式,很好地改进了文件和数据管理、数据恢复和可交互能力;而 Excel 2007 就是是一个包含 XML、图片等文件的压缩包;我们暂且先只关注 XML,先基于 POI 4.1.2
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
String filePath = "D:/POI_4_1_2.xlsx";
public void createExcel(String filePath) throws Exception {
try(SXSSFWorkbook wb = new SXSSFWorkbook();
OutputStream os = Files.newOutputStream(Paths.get(filePath))) {
SXSSFSheet sheetA = wb.createSheet("a");
SXSSFSheet sheetB = wb.createSheet("b");
SXSSFRow sheetA_row1 = sheetA.createRow(0);
sheetA_row1.createCell(0).setCellValue("hello world");
sheetA_row1.createCell(1).setCellValue("666");
SXSSFRow sheetA_row2 = sheetA.createRow(1);
sheetA_row2.createCell(0).setCellValue("888");
sheetA_row2.createCell(1).setCellValue("999");
SXSSFRow sheetB_row1 = sheetB.createRow(0);
sheetB_row1.createCell(0).setCellValue("qsl");
sheetB_row1.createCell(1).setCellValue("青石路");
wb.write(os);
os.flush();
}
}
生成个旧版的 Excel 2007 文件:POI_4_1_2.xlsx,直接用 7z 进行提取(也可以直接将 POI_4_1_2.xlsx 重命名成 POI_4_1_2.zip,然后进行解压)

解压之后目录结构如下

所有的文件都是 XML;将 POI 升级到 5.3.0
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.3.0</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.3.0</version>
</dependency>
String filePath = "D:/POI_5_3_0.xlsx";
public void createExcel(String filePath) throws Exception {
try(SXSSFWorkbook wb = new SXSSFWorkbook();
OutputStream os = Files.newOutputStream(Paths.get(filePath))) {
SXSSFSheet sheetA = wb.createSheet("a");
SXSSFSheet sheetB = wb.createSheet("b");
SXSSFRow sheetA_row1 = sheetA.createRow(0);
sheetA_row1.createCell(0).setCellValue("hello world");
sheetA_row1.createCell(1).setCellValue("666");
SXSSFRow sheetA_row2 = sheetA.createRow(1);
sheetA_row2.createCell(0).setCellValue("888");
sheetA_row2.createCell(1).setCellValue("999");
SXSSFRow sheetB_row1 = sheetB.createRow(0);
sheetB_row1.createCell(0).setCellValue("qsl");
sheetB_row1.createCell(1).setCellValue("青石路");
wb.write(os);
os.flush();
}
}
解压 POI_5_3_0.xlsx,目录结构与 POI_4_1_2.xlsx 的解压目录结构一致,文件名与文件数量也一致

关于
Excel 2007 文件是个压缩包!
相信大家没疑问了吧;我们来对比下两个目录

虽然差异文件挺多,但可以归为两类
standalone 差异
_rels\.rels
docProps\core.xml
xl\_rels\workbook.xml.rels
[Content_Types].xml
这四个文件的差异是一样的(四个文件都是一行,我为了突显差异,将相同的换到了第二行)

POI 4.1.2 生成的 xml 中的 standalone 值是
no,而 POI 5.3.0 生成的 xml 中的 standalone 值是yes,就这么一个区别core.xml 中还有一个差异:

创建时间不同是正常的,这个差异可以忽略
dimension 差异
xl\worksheets目录下存放的是 sheet 相关的 xml,但是名字是 sheet1 ~ sheetn,而不是我们代码中指定的a、b,有多少个 sheet,对应就会有多少个 xml 文件,我们只需要看其中某个 xml 文件的差异即可,其他类似
就一处差异:POI 4.1.2 生成的 sheet 中是
<dimension ref="A1"/>,而 POI 5.3.0 中是<dimension ref="A1:B2"/>
这么看来,Excel 2007 文件确实有标准与非标之分
回到问题
不升级 POI 版本,如何生成符合新版标准的Excel 2007文件
你们会如何处理?
要保证不影响已对接的客户(潜台词就是:既不能更换掉 POI,也不能升级 POI)的同时,还要能生成标准版的 Excel 2007文件来满足新客户,感觉没什么办法了呀,只能增加配置项:是否生成标准Excel 2007,默认值是:否,表示生成非标Excel 2007文件,保证已对接的客户不受影响,配置项值如果是:是,则生成标准Excel 2007文件;那么问题又来了
标准Excel 2007文件如何生成?
通过 POI 生成肯定是不行了,因为不能升级其版本,生成的是非标Excel 2007文件,那怎么办呢,我们可以换个组件嘛,条条大路通罗马,生成Excel 2007的组件肯定不只有 POI,换个组件来生成标准Excel 2007文件就好了嘛
其他组件
阿里的 EasyExcel ,你们肯定都知道吧,那就用它来生成标准Excel 2007文件,引入依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>4.0.2</version>
</dependency>
我们来看下它的依赖树

框住的部分,你们应该能看懂吧;EasyExcel 依赖 POI,但因为 POI 4.1.2 的优先级高于 EasyExcel 依赖的 5.2.5,所以最终依赖的还是 POI 4.1.2
关于 maven 的优先级可查看:结合实例看 maven 传递依赖与优先级,难顶也得上丫
此时你们是不是懵逼了?

显然用 EasyExcel 行不通;我还试了 jxl,发现也不行(解压后目录结构完全不一样),没有去试其他组件,因为我想到了一种感觉可行的方案
重打包
还记得前面的目录对比吗,差异文件分两类,standalone 差异固定是 4 个文件
_rels\.rels
docProps\core.xml
xl\_rels\workbook.xml.rels
[Content_Types].xml
dimension 差异固定为一类文件
xl\worksheets\sheet*.xml
除了这些差异文件,其他文件都是一致的,那么我们是不是可以这样处理
Excel 2007 文件还是基于 POI 4.1.2 生成,若配置项:
是否生成标准Excel 2007未配置或者配置的是否,则文件生成结束(既有逻辑),如果配置项配置的是:是,则对生成好的 Excel 2007 进行以下处理
- 解压生成好的 Excel 2007 文件
- 对差异文件进行修改,将对应的差异项修改成标准值
- 重新打包成 Excel 2007 文件,并替换掉之前的旧 Excel 2007 文件
这样是不是就实现需求了?方案有了那就试呗
解压
就用 POI 依赖的
commons-compress进行解压即可/**
* 对 Excel 2007 文件进行解压
* @param sourceFile 源Excel 2007文件
* @param unzipDir 解压目录
* @throws IOException 解压异常
* @author 青石路
*/
private void unzip(File sourceFile, String unzipDir) throws IOException {
try (ZipFile zipFile = new ZipFile(sourceFile)) {
// 遍历 ZIP 文件中的每个条目
Enumeration<ZipArchiveEntry> entries = zipFile.getEntries();
while(entries.hasMoreElements()) {
ZipArchiveEntry entry = entries.nextElement();
// 创建输出文件的路径
Path outputPath = Paths.get(unzipDir, entry.getName());
if (!Files.exists(outputPath.getParent())) {
// 确保父目录存在
Files.createDirectories(outputPath.getParent());
}
try (InputStream inputStream = zipFile.getInputStream(entry);
FileOutputStream outputStream = new FileOutputStream(outputPath.toFile())) {
IOUtils.copy(inputStream, outputStream);
}
}
}
}
修改
standalone 值修改
/**
* 修改xml 的 standalone 属性值
* @param filePath 包含 standalone 属性的xml文件
* @throws IOException IO异常
* @author 青石路
*/
private void updateXmlStandalone(Path filePath) throws IOException {
Path bakPath = Paths.get(filePath.getParent().toString(), filePath.getFileName() + "_bak");
try (BufferedReader reader = Files.newBufferedReader(filePath)) {
String line = reader.readLine();
String replace = line.replace("standalone=\"no\"", "standalone=\"yes\"");
Files.write(bakPath, replace.getBytes(StandardCharsets.UTF_8));
}
Files.delete(filePath);
Files.move(bakPath, filePath);
}
dimension 修改,首先我们需要弄清楚
ref值的含义// POI 4.1.2
// POI 5.3.0
POI 4.1.2 中,ref 的值仅表示起始坐标,A表示X坐标值,1表示Y坐标值,而在 POI 5.3.0 中,ref 的值不仅有起始坐标,还包括结束坐标,
A1表示起始坐标,B2表示结束坐标,这里的 2 表示数据行数/**
* 修改xml 的 dimension ref 属性值
* @param sheetDir sheet xml所在目录
* @throws IOException IO异常
* @author 青石路
*/
private void updateSheetXmlDimension(Path sheetDir) throws IOException {
// 修改第二行中的 <dimension ref="A1"/>
try (Stream<Path> filePaths = Files.list(sheetDir)) {
filePaths.forEach(filePath -> {
// 先获取列数和行数,rows:数据行数,totalRows:内容总行数
AtomicInteger columns = new AtomicInteger(0);
AtomicInteger rows = new AtomicInteger(0);
try (Stream<String> lines = Files.lines(filePath)) {
lines.forEach(line -> {
if (line.endsWith("</row>")) {
rows.incrementAndGet();
}
if (rows.get() == 1 && line.endsWith("</row>")) {
columns.set(line.split("</c>").length - 1);
}
});
} catch (IOException e) {
throw new RuntimeException(e);
}
// Excel 列坐标 A ~ Z,AA ~ ZZ,...
int circleTimes = columns.get() % 26 == 0 ? (columns.get() / 26 - 1) : (columns.get() / 26);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < circleTimes; i++) {
sb.append("A");
}
sb.append((char) ('A' + (columns.get() % 26 == 0 ? 25 : (columns.get() % 26 - 1))));
// <dimension ref="A1:B2"/>
String objStr = "<dimension ref=\"A1:" + sb + rows.get();
try {
Path bakPath = Paths.get(filePath.getParent().toString(), filePath.getFileName() + "_bak");
Files.createFile(bakPath);
try (Stream<String> lines = Files.lines(filePath)) {
lines.forEach(line -> {
try {
if (line.contains("<dimension ref=\"A1")) {
line = line.replace("<dimension ref=\"A1", objStr);
}
if (!line.endsWith("</worksheet>")) {
line = line + "\n";
}
Files.write(bakPath, line.getBytes(StandardCharsets.UTF_8), StandardOpenOption.APPEND);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
Files.delete(filePath);
Files.move(bakPath, filePath);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
};
}
这个代码稍微复杂一点,但可以归纳为以下几步
遍历 sheet xml文件的内容,得到列数和行数
根据列数去推算出最大列坐标(B),再根据行数(2)得到结束坐标(B2),那么 ref 的值也就是:
A1:B2这里有个小坑,当数据只有一行一列时,新版的 ref 的值与旧版的 ref 值一致,都是
A1,但上述代码得到却是A1:A1,所以还需要兼容调整下,至于如何调整,就交给你们了,我这里只是提示你们要注意这个坑!!!进行 sheet xml 数据拷贝,并用
<dimension ref=\"A1:B2替换掉<dimension ref=\"A1,最后用新的 sheet xml 文件替换旧的
打包
需要修改的 xml 文件都修改完成之后重新进行打包,这里继续用
commons-compress/**
* 重新打包成 xlsx
* @param basePath 解压根目录([Content_Types].xml所在目录)
* @param oriFile 源Excel 2007文件
* @throws IOException
* @author 青石路
*/
private void repackage(String basePath, File oriFile) throws IOException {
File newFile = new File(basePath + ".xlsx");
try (FileOutputStream fos = new FileOutputStream(newFile);
ZipArchiveOutputStream zaos = new ZipArchiveOutputStream(fos)) {
// 获取源文件夹下的所有文件和子文件夹
File srcDir = new File(basePath);
for (File f : Objects.requireNonNull(srcDir.listFiles())) {
addToZip(f, "", zaos);
}
}
// 用新文件覆盖原文件
Path oriPath = oriFile.toPath();
Files.delete(oriPath);
Files.move(newFile.toPath(), oriPath);
} private void addToZip(File file, String parentFolder, ZipArchiveOutputStream zaos) throws IOException {
if (file.isDirectory()) {
// 如果是目录,则遍历其中的文件并递归调用 addToZip
for (File childFile : Objects.requireNonNull(file.listFiles())) {
addToZip(childFile, parentFolder + file.getName() + "/", zaos);
}
} else {
// 如果是文件,则将其添加到 ZIP 文件中
try (FileInputStream fis = new FileInputStream(file)) {
// 创建一个不带第一层目录的 ZipArchiveEntry
String entryName = parentFolder + file.getName();
if (entryName.startsWith("/")) {
entryName = entryName.substring(1);
}
ZipArchiveEntry entry = new ZipArchiveEntry(entryName);
zaos.putArchiveEntry(entry);
IOUtils.copy(fis, zaos);
zaos.closeArchiveEntry();
}
}
}
没什么复杂点,相信你们都能看懂
串联
将上面 3 步串起来
/**
* 重打包Excel2007文件
* @param ifExcel2007New 是否重新打包
* @param xlsxFile xlsx源文件
* @throws IOException
* @author 青石路
*/
private void repackageExcel2007(boolean ifExcel2007New, File xlsxFile) throws IOException {
if (!ifExcel2007New) {
return;
}
Path unzipDir = Files.createTempDirectory("");
try {
String basePath = Paths.get(unzipDir.toString(), xlsxFile.getName().substring(0, xlsxFile.getName().lastIndexOf("."))).toString();
// 解压xlsx
unzip(xlsxFile, basePath);
// 修改xml
updateXmlStandalone(Paths.get(basePath, "_rels", ".rels"));
updateXmlStandalone(Paths.get(basePath, "docProps", "core.xml"));
updateXmlStandalone(Paths.get(basePath, "xl", "_rels", "workbook.xml.rels"));
updateXmlStandalone(Paths.get(basePath, "[Content_Types].xml"));
updateSheetXmlDimension(Paths.get(basePath, "xl", "worksheets"));
// 打包成xlsx
repackage(basePath, xlsxFile);
} finally {
// 删除临时文件夹
try (Stream<Path> walk = Files.walk(unzipDir)) {
walk.sorted(Comparator.reverseOrder())
.map(Path::toFile)
.forEach(File::delete);
}
}
}
至此,大功告成!我已经试过了,重打包之后的 Excel 2007 文件,用 Windows 的 Excel 工具能正常打开,WPS 也能正常打开,给新客户测试,也能正常导入,简直完美!

总结
Excel 2007 文件是集 xml、图片等文件的压缩包
引入新功能时,一定不能影响已有功能
可以通过解压、修改、打包的方式,修改Excel 2007文件的元数据
解压与打包都用
commons-compress,用别的可能会有惊吓!
不升级 POI 版本,如何生成符合新版标准的Excel 2007文件的更多相关文章
- spring boot下使用logback或log4j生成符合Logstash标准的JSON格式
spring boot下使用logback或log4j生成符合Logstash标准的JSON格式 一.依赖 由于配置中使用了json格式的日志输出,所以需要引入如下依赖 "net.logst ...
- 不升级Element-UI 版本为时间选择器增加标记功能
Element-UI里的date-picker是个优秀的时间选择器,支持的选项很多,定制型很强.不过date-picker在2.12版本之前并不支持自定义单元格样式,也就是2.12的cellClass ...
- 非关系型数据库来了,CRL快速开发框架升级到版本4
轮子?,我很任性,我要造不一样的轮子,同时支持关系型和非关系型的框架有没有 新版数据查询作了些调整,抽象了LabmdaQueryy和DBExtend,升级到版本4,非关系数据库MongoDB被支持了! ...
- CentOS6.x升级MySQL版本5.1到5.6
CentOS6.x升级MySQL版本5.1到5.6 分类: Web MySQL 2014-08-04 11:22 2813人阅读 评论(1) 收藏 举报 mysql云服务器升级centos6 有一些虚 ...
- 升级R版本后,更新Package
升级R版本后,若重新安装所有的package将非常麻烦,可以尝试运行一下程序: 1)在旧版本中的R中运行 #--run in the old version of R setwd("C:/T ...
- 10、借助POI实现Java生成并打印excel报表(1)
10.1.了解 Apache POI 实际开发中,用到最多的是把数据库中数据导出生成报表,尤其是在生产管理或者财务系统中用的非常普遍.生成报表格式一般是EXCEL或者PDF .利用Apache PO ...
- jQuery1.9+ 废弃的函数和方法 升级Jquery版本遇到的问题
面临问题 很久没关注JQuery了,今天突然想升级一下系统中使用的jquery版本,突然发现,升级JQuery版本到1.9之后出现了很多问题,比如:$.browser is undefined.突然就 ...
- 06.升级git版本及命令学习
博客为日常工作学习积累总结: 1.升级git版本: 参考博客:https://blog.csdn.net/yuexiahunone/article/details/78647565由于新的版本可以使用 ...
- 在centos6.5上升级php-libxml版本到2.9.0
当前系统,软件版本说明: php libxml glibc 2.12 zlib xz-libs 需求: 应开发的需求,线上环境,php-libxml版本升级到2.8以上. 升级步骤:1.安装工具集 y ...
- CENTOS 7 升级内核版本(附带升级脚本)
写在前面的话 对于系统而言,除非是那种安全性要求非常高的公司或者经常会有第三方安全机构对其漏洞扫描的才容易涉及到系统的内核升级,比如之前呆过一个公司,因为需要做三级等保的原因,就会涉及到系统扫描,这时 ...
随机推荐
- 机器学习策略篇:详解如何使用来自不同分布的数据,进行训练和测试(Training and testing on different distributions)
如何使用来自不同分布的数据,进行训练和测试 深度学习算法对训练数据的胃口很大,当收集到足够多带标签的数据构成训练集时,算法效果最好,这导致很多团队用尽一切办法收集数据,然后把它们堆到训练集里,让训练的 ...
- 洛谷P1043
[NOIP2003 普及组] 数字游戏 题目描述 丁丁最近沉迷于一个数字游戏之中.这个游戏看似简单,但丁丁在研究了许多天之后却发觉原来在简单的规则下想要赢得这个游戏并不那么容易.游戏是这样的,在你面前 ...
- yolov5 筛选正样本流程 代码多图详解
yolov5正样本筛选原理 正样本全称是anchor正样本,正样本所指的对象是anchor box,即先验框. 先验框:从YOLO v2开始吸收了Faster RCNN的优点,设置了一定数量的预选框, ...
- Solo 开发者周刊 (第3期):如何打造令人惊艳的AI体验
这里会整合 Solo 社区每周推广内容.产品模块或活动投稿,每周五发布.在这期周刊中,我们将深入探讨开源软件产品的开发旅程,分享来自一线独立开发者的经验和见解.本杂志开源,欢迎投稿. 好文推荐 Plu ...
- FPGA CFGBVS 管脚接法
说明 新设计了1个KU040 FPGA板子,回来之后接上JTAG FPGA不识别.做如下检查: 1.电源测试点均正常: 2.查看贴片是否有漏焊,检查无异常,设计上NC的才NC: 3.反复检查JTAG接 ...
- 产品探秘:智影AI——你的创意视频制作神器!
只需3步,把小说变成视频.免费试用,首次注册赠送600积分. https://icomicai.com/ 在这个快节奏的时代,创意与效率并重成为了我们追求的新风尚.今天,就让我带你一起揭秘一款颠覆传统 ...
- 阅读翻译Mathematics for Machine Learning之2.5 Linear Independence
阅读翻译Mathematics for Machine Learning之2.5 Linear Independence 关于: 首次发表日期:2024-07-18 Mathematics for M ...
- ABC352
A link \(x\)停不到,\(y\)能停到. 要先判断是从前往后还是从后往前. 点击查看代码 #include<bits/stdc++.h> using namespace std; ...
- mybatis源码分析:Mapper接口是什么
在<mybatis源码分析:启动过程>中分析了mybatis的启动过程,mybatis的启动过程主要集中在解析其核心配置文件(mybatis-config.xml)上,把配置文件中的配置全 ...
- 《最新出炉》系列入门篇-Python+Playwright自动化测试-54- 上传文件(input控件) - 上篇
1.简介 在实际工作中,我们进行web自动化的时候,文件上传是很常见的操作,例如上传用户头像,上传身份证信息等.所以宏哥打算按上传文件的分类对其进行一下讲解和分享. 2.上传文件的API(input控 ...