前言
做后端开发经常需要导出Excel
, 一般有两种选择POI
、JXLS
和easyexcel
.
其实底层都是使用POI
来做的.
三种框架分析
Apache
名下的POI
- 优点: 微软全家桶基本都可以做, 什么
Word
、Excel
、PPT
.
- 缺点: 所有格式都要通过代码完成, 代码又长又臭.
JXLS
专门用来操作Excel
, 使用JXLS
模板语法可以书写Excel
模板, 底层使用org.jxls.transform.poi.PoiTransformer
和POI
做对接.
- 优点: 使用
JXLS
模板语法, 可以将格式和代码解耦, 快速开发, 代码精简.
- 缺点: 需要额外的学习成本学习
JXLS
模板语法(学什么不是学
easyexcel
是阿里基于POI
开发的Excel
解析工具.
- 优点: 据
README
说, 解决POI
内存溢出问题.
- 缺点: 没用过不评价
emoji乱码代码单元测试
复现下测试环境, 依赖pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <dependencies> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>3.17</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>3.17</version> </dependency>
</dependencies>
|
生成Excel
的代码.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| public class Main { public static final String UNICODE = "\uD850\uDD95\uD83D\uDD1Dbiu~\t\uE110better me\uE110人\uD83C\uDF1D";
public static void main(String[] args) { createXLSX("C:/test.xlsx"); createXLS("C:/test.xls"); }
public static void createXLSX(String filename) { try(XSSFWorkbook workbook = new XSSFWorkbook(); FileOutputStream fos =new FileOutputStream(filename)) {
XSSFSheet sheet = workbook.createSheet("TestSheet"); XSSFRow row = sheet.createRow(0); XSSFCell cell1 = row.createCell(0); cell1.setCellValue(UNICODE);
workbook.write(fos); } catch (IOException e) { e.printStackTrace(); } } public static void createXLS(String filename) { try(HSSFWorkbook workbook = new HSSFWorkbook(); FileOutputStream fos =new FileOutputStream(filename)) {
HSSFSheet sheet = workbook.createSheet("TestSheet"); HSSFRow row = sheet.createRow(0); HSSFCell cell1 = row.createCell(0); cell1.setCellValue(UNICODE);
workbook.write(fos); } catch (IOException e) { e.printStackTrace(); } } }
|
运行完毕, 可以看到xls
能正常输出𤆕🔝biu~ better me人🌝
, 而xlsx
只能输出????biu~ better me人??
.
emoji
表情不能输出.
解决方案(简单版)
解决方案有两种.
- 升级
POI
为4.0.0
以上.
- 替换
xmlbeans
为3.0.0
以上.
本质都是替换xmlbeans
.
问题原因
为什么xls
没问题, xlsx
有问题?
因为xls
的HSSFRichTextString
使用了UnicodeString
进行编码, 内联字符串, 没有重用字符串.
而xlsx
为了解决内存问题, 将相同字符串先写入sharedStrings.xml
, 重用字符串.
我们可以将一个xlsx
改为zip
解压, 在里面可以找到一个sharedStrings.xml
文件, 里面就存储着我们的字符串.
1 2 3 4
| <?xml version="1.0" encoding="UTF-8"?> <sst count="1" uniqueCount="1" xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"> <si><t>????biu~ better me人??</t></si> </sst>
|
那很明显就是写入xml
的时候出了问题.
Write 16 bits character to .xlsx file using Apache POI in Java提到了org.apache.xmlbeans.impl.store.Saver
的isBadChar
方法.
点击查看源码L1559
1 2 3 4 5 6 7 8 9 10
| abstract class Saver { private boolean isBadChar(char ch) { return ! ( (ch >= 0x20 && ch <= 0xD7FF ) || (ch >= 0xE000 && ch <= 0xFFFD) || (ch >= 0x10000 && ch <= 0x10FFFF) || (ch == 0x9) || (ch == 0xA) || (ch == 0xD) ); } }
|
以🌝
这个字符为例, 它的Unicode
编码为\uD83C\uDF1D
, 一个char
是存不下的, 要用两个char
.
那么第一个char
传入isBadChar
, 得到true
. 第二个char
传入也得到true
.
那么我们看下哪里有调用到这个isBadChar
方法.
点击查看源码L2310
1 2 3 4 5 6 7 8 9 10 11
| abstract class Saver { private void entitizeAndWriteCommentText(int bufLimit) { for (int i = 0; i < bufLimit; i++) { char ch = _buf[ i ]; if (isBadChar(ch)) _buf[i] = '?'; } } }
|
有很多地方都有调用, 但是基本都是一个逻辑, 如果是true
, 则替换为?
, 所以我们才在sharedStrings.xml
看到一堆??
.
那这就是xmlbeans
这个库的问题了, 不是我们代码的锅(甩锅大成功!
xmlbeans 3.0.0 解决方案
再看看xmlbeans 3.0.0
怎么解决的.
点击查看源码L286
1 2 3 4 5 6 7 8 9 10 11 12
| abstract class Saver { private boolean isBadChar(char ch) { return ! ( Character.isHighSurrogate(ch) || Character.isLowSurrogate(ch) || (ch >= 0x20 && ch <= 0xD7FF ) || (ch >= 0xE000 && ch <= 0xFFFD) || (ch >= 0x10000 && ch <= 0x10FFFF) || (ch == 0x9) || (ch == 0xA) || (ch == 0xD) ); } }
|
以🌝
这个字符为例, 它的Unicode
编码为\uD83C\uDF1D
, 一个char
是存不下的, 要用两个char
.
那么第一个char
传入isBadChar
, 得到false
. 第二个char
传入也得到false
.
所以也就不会被替换成?
, bug
解决.
参考资料