1、问题
工作中遇到一个需求,接收一个zip包,读取其中的excel文件并处理,解决用户需要多次选择目录和文件的痛点,该zip包包含多级目录
2、依赖
需要用到apache的Workbook类来操作Excel,引入以下依赖
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>3.9</version>
</dependency>
3、具体实现
public List<Workbook> getExcelsFromZip(MultipartFile multipartFile) {
ZipInputStream zis = null;
List<Workbook> workbooks = new ArrayList<>();
try {
// 构建zip流
zis = new ZipInputStream(multipartFile.getInputStream());
// 获取文件条目,此方法读取的问zip包中所有条目(目录和文件),多级目录下的所有文件和目录均会被读取
ZipEntry zipEntry = zis.getNextEntry();
while (zipEntry != null) {
// 判断类型是否为文件
if (!zipEntry.isDirectory()) {
// 获取文件名
String fileName = zipEntry.getName();
/* 后缀名 */
int suffixIndex = fileName.lastIndexOf(".");
String suffix = suffixIndex >= 0 ? fileName.substring(suffixIndex) : "";
// 判断是否为excel文件
if (".xls".equals(suffix) || ".xlsx".equals(suffix)) {
/* 将文件写入到byte数组中 */
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
// 在调用了getNextEntry()后,zis就指向了获取到的ZipEntry,读取zis流实际就是读取当前ZipEntry,不会读取到其他条目
while ((len = zis.read(buffer)) > 0) {
out.write(buffer, 0, len);
}
// 从输出流中取出byte数组,使用byte数组里的文件数据构建一个输入流
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
/* 构建Workbook */
Workbook wb;
if (".xls".equals(suffix)) {
// 老版excel格式,使用HSSFWorkbook
wb = new HSSFWorkbook(in);
}else {
// 新版excel格式,使用XSSFWorkbook
wb = new XSSFWorkbook(in);
}
workbooks.add(wb);
// 关闭当前ZipEntry,确保不会重复处理
zis.closeEntry();
// 获取下一条目
zipEntry = zis.getNextEntry();
}
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
if (zis != null) {
try {
zis.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
return workbooks;
}
4、技术要点
(1)ZipInputStream.getNextEntry()方法
【1】该方法可获取到zip包内的各级目录及各级目录下的文件,并不需要进行递归处理。
【2】ZipInputStream实例调用该方法后,将会指向获取到的条目,这时使用ZipInputStream实例操作的就是获取到的ZipEntry对象。可通过ZipInputStream.closeEntry()和ZipInputStream.getNextEntry()迭代到下一个ZipEntry条目。
(2)为什么选择ByteArrayInputStream和ByteArrayInputStream获取Workbook
二者是在内存中处理字节的IO流,不同于磁盘IO流会在磁盘中创建临时文件读写,处理速度会更快,在内存中处理数据也符合业务需求。
(3)ByteArrayInputStream和ByteArrayInputStream不需要关闭吗?
前面说到这两个流都是在内存中操作数据,没有占用系统的IO资源,所以并不需要显式关闭。查看这两个类的close方法,会发现方法中没有做任何操作。同理,ByteArrayInputStream也不需要进行flush(),该类的flush()方法中也没有进行任何操作。
(3)为什么不使用Workbook wb = WorkbookFactory.create(zis);直接转化
实测使用WorkbookFactory.create(zis)会关闭流,导致无法读取后续条目