开心一刻

记得小时候,家里丢了钱,是我拿的,可爸妈却一口咬定是弟弟拿的

爸爸把弟弟打的遍体鳞伤,弟弟气愤的斜视着我

我不敢直视弟弟,目光转向爸爸说到:爸爸,你看他,好像还不服

问题描述

项目基于 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 文件是个压缩包!

相信大家没疑问了吧;我们来对比下两个目录

虽然差异文件挺多,但可以归为两类

  1. 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 中还有一个差异:

    创建时间不同是正常的,这个差异可以忽略

  2. dimension 差异

    xl\worksheets 目录下存放的是 sheet 相关的 xml,但是名字是 sheet1 ~ sheetn,而不是我们代码中指定的 ab,有多少个 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 进行以下处理

  1. 解压生成好的 Excel 2007 文件
  2. 对差异文件进行修改,将对应的差异项修改成标准值
  3. 重新打包成 Excel 2007 文件,并替换掉之前的旧 Excel 2007 文件

这样是不是就实现需求了?方案有了那就试呗

  1. 解压

    就用 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);
    }
    }
    }
    }
  2. 修改

    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);
    }
    });
    };
    }

    这个代码稍微复杂一点,但可以归纳为以下几步

    1. 遍历 sheet xml文件的内容,得到列数和行数

    2. 根据列数去推算出最大列坐标(B),再根据行数(2)得到结束坐标(B2),那么 ref 的值也就是:A1:B2

      这里有个小坑,当数据只有一行一列时,新版的 ref 的值与旧版的 ref 值一致,都是 A1,但上述代码得到却是 A1:A1,所以还需要兼容调整下,至于如何调整,就交给你们了,我这里只是提示你们要注意这个坑!!!

    3. 进行 sheet xml 数据拷贝,并用 <dimension ref=\"A1:B2 替换掉 <dimension ref=\"A1,最后用新的 sheet xml 文件替换旧的

  3. 打包

    需要修改的 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();
    }
    }
    }

    没什么复杂点,相信你们都能看懂

  4. 串联

    将上面 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 也能正常打开,给新客户测试,也能正常导入,简直完美!

    总结

    1. Excel 2007 文件是集 xml、图片等文件的压缩包

    2. 引入新功能时,一定不能影响已有功能

      都说了能不动就别动,非要去调整,出生产事故了吧

    3. 可以通过解压、修改、打包的方式,修改Excel 2007文件的元数据

    4. 解压与打包都用 commons-compress,用别的可能会有惊吓!

不升级 POI 版本,如何生成符合新版标准的Excel 2007文件的更多相关文章

  1. spring boot下使用logback或log4j生成符合Logstash标准的JSON格式

    spring boot下使用logback或log4j生成符合Logstash标准的JSON格式 一.依赖 由于配置中使用了json格式的日志输出,所以需要引入如下依赖 "net.logst ...

  2. 不升级Element-UI 版本为时间选择器增加标记功能

    Element-UI里的date-picker是个优秀的时间选择器,支持的选项很多,定制型很强.不过date-picker在2.12版本之前并不支持自定义单元格样式,也就是2.12的cellClass ...

  3. 非关系型数据库来了,CRL快速开发框架升级到版本4

    轮子?,我很任性,我要造不一样的轮子,同时支持关系型和非关系型的框架有没有 新版数据查询作了些调整,抽象了LabmdaQueryy和DBExtend,升级到版本4,非关系数据库MongoDB被支持了! ...

  4. 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 有一些虚 ...

  5. 升级R版本后,更新Package

    升级R版本后,若重新安装所有的package将非常麻烦,可以尝试运行一下程序: 1)在旧版本中的R中运行 #--run in the old version of R setwd("C:/T ...

  6. 10、借助POI实现Java生成并打印excel报表(1)

    10.1.了解 Apache POI 实际开发中,用到最多的是把数据库中数据导出生成报表,尤其是在生产管理或者财务系统中用的非常普遍.生成报表格式一般是EXCEL或者PDF .利用Apache  PO ...

  7. jQuery1.9+ 废弃的函数和方法 升级Jquery版本遇到的问题

    面临问题 很久没关注JQuery了,今天突然想升级一下系统中使用的jquery版本,突然发现,升级JQuery版本到1.9之后出现了很多问题,比如:$.browser is undefined.突然就 ...

  8. 06.升级git版本及命令学习

    博客为日常工作学习积累总结: 1.升级git版本: 参考博客:https://blog.csdn.net/yuexiahunone/article/details/78647565由于新的版本可以使用 ...

  9. 在centos6.5上升级php-libxml版本到2.9.0

    当前系统,软件版本说明: php libxml glibc 2.12 zlib xz-libs 需求: 应开发的需求,线上环境,php-libxml版本升级到2.8以上. 升级步骤:1.安装工具集 y ...

  10. CENTOS 7 升级内核版本(附带升级脚本)

    写在前面的话 对于系统而言,除非是那种安全性要求非常高的公司或者经常会有第三方安全机构对其漏洞扫描的才容易涉及到系统的内核升级,比如之前呆过一个公司,因为需要做三级等保的原因,就会涉及到系统扫描,这时 ...

随机推荐

  1. oeasy教您玩转linux010206toilet

    我们来回顾一下 上一部分我们都讲了什么? 用apt查询并下载了figlet 玩了一下字符画 设置了字符画的字体 但是没有修改颜色 这次我们来找找另一个命令toilet apt search toile ...

  2. Flink 内存配置学习总结

    设置进程内存(Process Memory) Apache Flink通过严格控制其各种组件的内存使用,在JVM之上提供高效的工作负载. 配置总内存(Total Memory) Flink JVM进程 ...

  3. Django 安全之跨站点请求伪造(CSRF)保护

    Django 安全之跨站点请求伪造(CSRF)保护 by:授客 QQ:1033553122 测试环境 Win7 Django 1.11   跨站点请求伪造(CSRF)保护 中间件配置 默认的CSRF中 ...

  4. CF916C 题解

    CF916C 题解 思路 思考发现,如果我们让很多边的边权变得非常大,而故意留下 \(1\) 到 \(n\) 的某一条路径,使整条路径之和甚至还没有剩下一条边的权值大,这条路径显然就是最短路了. 更重 ...

  5. UE5 打不开

    在游戏开发中,出现了UE打不开的情况 初步推测,新增了接口Attacker, 而我们的DefaultPawn可能一下子实现了两个接口造成的 而这两个接口都在一个插件里,一个是c++实现的,一个是蓝图实 ...

  6. css 蛇形排序

    先看效果 需求: 一个[ 4  * ?]的网格布局,奇数行 布局 从左往右,偶数行 布局 从右往左. 思路1: js将数组按4个每份进行分割,将偶数份进行反向,然后再将分割后的数据,重新组装.( 太麻 ...

  7. 【SpringMVC】05 RestFul风格

    什么是RestFul风格? 一个资源定位和资源操作的风格,不是标准,也不是协议, 基于此风格的路径访问可以隐藏真实的参数传递,以提高网站的安全访问 以往的请求参数: jdbc:mysql://loca ...

  8. linux测试cpu性能的命令

    linux测试cpu性能的命令 在Linux中,可以使用多种命令来测试CPU性能.以下是一些常用的命令: stress: 一个通用的压力测试工具,可以生成CPU.内存.IO等负载. 安装: sudo ...

  9. tf.metrics 使用过程中发现的一些问题

    起因是看到了这么一个帖子: http://www.cocoachina.com/cms/wap.php?action=article&id=86347 简短来说就是下面的代码   运行起来结果 ...

  10. Ubuntu 18.04.4 导入docker镜像,启动镜像,保存容器为镜像,导出镜像

    1.  查看  docker 版本 sudo docker version 2. 查看本地库中的镜像 sudo docker images 3.   查看  正在运行的  容器 sudo docker ...