Browse Source

Merge pull request #2533 from alibaba/developing

Developing
pull/2565/head v3.1.1
Jiaju Zhuang 2 years ago committed by GitHub
parent
commit
38aa8ff90a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 52
      README.md
  2. 53
      abouteasyexcel.md
  3. 46
      easyexcel-core/src/main/java/com/alibaba/excel/analysis/v07/XlsxSaxAnalyser.java
  4. 25
      easyexcel-core/src/main/java/com/alibaba/excel/analysis/v07/handlers/HyperlinkTagHandler.java
  5. 5
      easyexcel-core/src/main/java/com/alibaba/excel/constant/ExcelXmlConstants.java
  6. 8
      easyexcel-core/src/main/java/com/alibaba/excel/read/metadata/holder/xlsx/XlsxReadSheetHolder.java
  7. 9
      easyexcel-core/src/main/java/com/alibaba/excel/read/metadata/holder/xlsx/XlsxReadWorkbookHolder.java
  8. 44
      easyexcel-test/src/test/java/com/alibaba/easyexcel/test/core/extra/ExtraDataTest.java
  9. 129
      easyexcel-test/src/test/java/com/alibaba/easyexcel/test/demo/fill/FillTest.java
  10. 42
      easyexcel-test/src/test/java/com/alibaba/easyexcel/test/demo/read/ReadTest.java
  11. 107
      easyexcel-test/src/test/java/com/alibaba/easyexcel/test/demo/write/WriteTest.java
  12. BIN
      easyexcel-test/src/test/resources/extra/extraRelationships.xlsx
  13. 4
      pom.xml
  14. 3
      update.md

52
README.md

@ -26,66 +26,22 @@ EasyExcel
Java解析、生成Excel比较有名的框架有Apache poi、jxl。但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。easyexcel重写了poi对07版Excel的解析,一个3M的excel用POI sax解析依然需要100M左右内存,改用easyexcel可以降低到几M,并且再大的excel也不会出现内存溢出;03版依赖POI的sax模式,在上层做了模型转换的封装,让使用者更加简单方便
## 64M内存20秒读取75M(46W行25列)的Excel(3.0.2+版本)
当然还有极速模式能更快,但是内存占用会在100M多一点
当然还有[极速模式](https://easyexcel.opensource.alibaba.com/qa/read#%E5%BC%80%E5%90%AF%E6%80%A5%E9%80%9F%E6%A8%A1%E5%BC%8F)能更快,但是内存占用会在100M多一点
![img](img/readme/large.png)
## 关于版本选择
如果项目中没有使用过poi,且jdk版本在8-17之间,直接使用最新版本,别犹豫。以下表格适用于不满足以上2个情况的。
| 版本 | poi依赖版本 (支持范围) | jdk版本支持范围 | 备注 |
|--------------------|-----------------------|--------------|---------------------------------------------|
| 3.1.0+ | 4.1.2 (4.1.2 - 5.2.2) | jkd8 - jdk17 | 推荐使用,会更新的版本 |
| 3.0.0-beta1 - 3.0.5 | 4.1.2 (4.1.2 - 5.2.2) | jkd8 - jdk11 | 不推荐项目新引入此版本,除非超级严重bug,否则不再更新 |
| 2.0.0-beta1-2.2.11 | 3.17 (3.17 - 4.1.2) | jdk6 - jdk11 | 不推荐项目新引入此版本,除非是jdk6否则不推荐使用,除非超级严重bug,否则不再更新 |
| 1+版本 | 3.17 (3.17 - 4.1.2) | jdk6 - jdk11 | 不推荐项目新引入此版本,超级严重bug,也不再更新 |
注意: 3+版本的的easyexcel,使用poi 5+版本时,需要手动排除:poi-ooxml-schemas,例如:
```xml
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.1.0</version>
<exclusions>
<exclusion>
<artifactId>poi-ooxml-schemas</artifactId>
<groupId>org.apache.poi</groupId>
</exclusion>
</exclusions>
</dependency>
```
### 关于版本升级
* 不建议跨大版本升级 尤其跨2个大版本
* 2+ 升级到 3+ 一些不兼容的地方
* 使用了自定义拦截器去修改样式的会出问题(不会编译报错)
* 读的时候`invoke`里面抛出异常,不会再额外封装一层`ExcelAnalysisException` (不会编译报错)
* 样式等注解涉及到 `boolean` or 一些枚举 值的 有变动,新增默认值(会编译报错,注解改就行)
* 大版本升级后建议相关内容重新测试下
### 最新版本
```xml
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.1.0</version>
<version>3.1.1</version>
</dependency>
```
### easyexcel人员招募
由于工作较忙,有意愿做开源的同学可以报名,主要负责群里回答&issue处理,当然也可以做一些PR.
由于开源没有任何物质回报,然后现在的维护同学也是课余时间维护的,所以想加入的同学需要持之以恒,而不是一时兴起.
要求如下:
* 有一定java编码能力 & 良好的编码习惯
* 了解easyexcel 读&写的原理
* 热爱开源项目
* 能长期坚持的去做
* 相对工作没那么忙
## 相关文档
* [快速开始](https://www.yuque.com/easyexcel/doc/easyexcel)
* [关于软件](/abouteasyexcel.md)
* [快速开始](https://easyexcel.opensource.alibaba.com/docs/current/)
* [更新记事](/update.md)
* [贡献代码](https://www.yuque.com/easyexcel/doc/contribute)
* [贡献代码](https://easyexcel.opensource.alibaba.com/community/contribute)
## 维护者
姬朋飞(玉霄)、庄家钜、怀宇

53
abouteasyexcel.md

@ -1,53 +0,0 @@
# easyexcel要去解决的问题
## Excel读写时候内存溢出
虽然POI是目前使用最多的用来做excel解析的框架,但这个框架并不那么完美。大部分使用POI都是使用他的userModel模式。userModel的好处是上手容易使用简单,随便拷贝个代码跑一下,剩下就是写业务转换了,虽然转换也要写上百行代码,相对比较好理解。然而userModel模式最大的问题是在于非常大的内存消耗,一个几兆的文件解析要用掉上百兆的内存。现在很多应用采用这种模式,之所以还正常在跑一定是并发不大,并发上来后一定会OOM或者频繁的full gc。
## 其他开源框架使用复杂
对POI有过深入了解的估计才知道原来POI还有SAX模式。但SAX模式相对比较复杂,excel有03和07两种版本,两个版本数据存储方式截然不同,sax解析方式也各不一样。想要了解清楚这两种解析方式,才去写代码测试,估计两天时间是需要的。再加上即使解析完,要转换到自己业务模型还要很多繁琐的代码。总体下来感觉至少需要三天,由于代码复杂,后续维护成本巨大。
## 其他开源框架存在一些BUG修复不及时
由于我们的系统大多数都是大并发的情况下运行的,在大并发情况下,我们会发现poi存在一些bug,如果让POI团队修复估计遥遥无期了。所以我们在easyexcel对这些bug做了规避。
如下一段报错就是在大并发情况下poi抛的一个异常。
```
Caused by: java.io.IOException: Could not create temporary directory '/home/admin/dio2o/.default/temp/poifiles'
at org.apache.poi.util.DefaultTempFileCreationStrategy.createTempDirectory(DefaultTempFileCreationStrategy.java:93) ~[poi-3.15.jar:3.15]
at org.apache.poi.util.DefaultTempFileCreationStrategy.createPOIFilesDirectory(DefaultTempFileCreationStrategy.java:82) ~[poi-3.15.jar:3.15]
```
报错地方poi源码如下
```
private void createTempDirectory(File directory) throws IOException {
if (!(directory.exists() || directory.mkdirs()) || !directory.isDirectory()) {
throw new IOException("Could not create temporary directory '" + directory + "'");
}
}
```
仔细看代码容易明白如果在并发情况下,如果2个线程同时判断directory.exists()都 为false,但执行directory.mkdirs()如果一些线程优先执行完,另外一个线程就会返回false。最终 throw new IOException("Could not create temporary directory '" + directory + "'")。针对这个问题easyexcel在写文件时候首先创建了该临时目录,避免poi在并发创建时候引起不该有的报错。
## Excel格式分析格式分析
- xls是Microsoft Excel2007前excel的文件存储格式,实现原理是基于微软的ole db是微软com组件的一种实现,本质上也是一个微型数据库,由于微软的东西很多不开源,另外也已经被淘汰,了解它的细节意义不大,底层的编程都是基于微软的com组件去开发的。
- xlsx是Microsoft Excel2007后excel的文件存储格式,实现是基于openXml和zip技术。这种存储简单,安全传输方便,同时处理数据也变的简单。
- csv 我们可以理解为纯文本文件,可以被excel打开。他的格式非常简单,解析起来和解析文本文件一样。
## 核心原理
写有大量数据的xlsx文件时,POI为我们提供了SXSSFWorkBook类来处理,这个类的处理机制是当内存中的数据条数达到一个极限数量的时候就flush这部分数据,再依次处理余下的数据,这个在大多数场景能够满足需求。
读有大量数据的文件时,使用WorkBook处理就不行了,因为POI对文件是先将文件中的cell读入内存,生成一个树的结构(针对Excel中的每个sheet,使用TreeMap存储sheet中的行)。如果数据量比较大,则同样会产生java.lang.OutOfMemoryError: Java heap space错误。POI官方推荐使用“XSSF and SAX(event API)”方式来解决。
分析清楚POI后要解决OOM有3个关键。
### 1、文件解压文件读取通过文件形式
![屏幕快照 2018-01-22 上午8.52.08.png](http://ata2-img.cn-hangzhou.img-pub.aliyun-inc.com/e3a3500014c95f7118d8c200a51acab4.png)
### 2、避免将全部全部数据一次加载到内存
采用sax模式一行一行解析,并将一行的解析结果以观察者的模式通知处理。
![基础模板1 (2).png](http://ata2-img.cn-hangzhou.img-pub.aliyun-inc.com/82bb195ac62532963b2364d2e4da23e5.png)
### 3、抛弃不重要的数据
Excel解析时候会包含样式,字体,宽度等数据,但这些数据是我们不关心的,如果将这部分数据抛弃可以大大降低内存使用。Excel中数据如下Style占了相当大的空间。

46
easyexcel-core/src/main/java/com/alibaba/excel/analysis/v07/XlsxSaxAnalyser.java

@ -8,6 +8,7 @@ import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import javax.xml.parsers.ParserConfigurationException;
@ -25,17 +26,23 @@ import com.alibaba.excel.metadata.CellExtra;
import com.alibaba.excel.read.metadata.ReadSheet;
import com.alibaba.excel.read.metadata.holder.xlsx.XlsxReadWorkbookHolder;
import com.alibaba.excel.util.FileUtils;
import com.alibaba.excel.util.MapUtils;
import com.alibaba.excel.util.SheetUtils;
import com.alibaba.excel.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.openxml4j.opc.PackageAccess;
import org.apache.poi.openxml4j.opc.PackagePart;
import org.apache.poi.openxml4j.opc.PackagePartName;
import org.apache.poi.openxml4j.opc.PackageRelationshipCollection;
import org.apache.poi.openxml4j.opc.PackagingURIHelper;
import org.apache.poi.ss.util.CellAddress;
import org.apache.poi.xssf.eventusermodel.XSSFReader;
import org.apache.poi.xssf.model.CommentsTable;
import org.apache.poi.xssf.model.SharedStringsTable;
import org.apache.poi.xssf.usermodel.XSSFComment;
import org.apache.poi.xssf.usermodel.XSSFRelation;
import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTWorkbook;
@ -52,6 +59,20 @@ import org.xml.sax.XMLReader;
@Slf4j
public class XlsxSaxAnalyser implements ExcelReadExecutor {
/**
* Storage sheet SharedStrings
*/
public static final PackagePartName SHARED_STRINGS_PART_NAME;
static {
try {
SHARED_STRINGS_PART_NAME = PackagingURIHelper.createPartName("/xl/sharedStrings.xml");
} catch (InvalidFormatException e) {
log.error("Initialize the XlsxSaxAnalyser failure", e);
throw new ExcelAnalysisException("Initialize the XlsxSaxAnalyser failure", e);
}
}
private final XlsxReadContext xlsxReadContext;
private final List<ReadSheet> sheetList;
private final Map<Integer, InputStream> sheetMap;
@ -68,11 +89,9 @@ public class XlsxSaxAnalyser implements ExcelReadExecutor {
OPCPackage pkg = readOpcPackage(xlsxReadWorkbookHolder, decryptedStream);
xlsxReadWorkbookHolder.setOpcPackage(pkg);
ArrayList<PackagePart> packageParts = pkg.getPartsByContentType(XSSFRelation.SHARED_STRINGS.getContentType());
if (!CollectionUtils.isEmpty(packageParts)) {
PackagePart sharedStringsTablePackagePart = packageParts.get(0);
// Read the Shared information Strings
PackagePart sharedStringsTablePackagePart = pkg.getPart(SHARED_STRINGS_PART_NAME);
if (sharedStringsTablePackagePart != null) {
// Specify default cache
defaultReadCache(xlsxReadWorkbookHolder, sharedStringsTablePackagePart);
@ -89,6 +108,9 @@ public class XlsxSaxAnalyser implements ExcelReadExecutor {
sheetList = new ArrayList<>();
sheetMap = new HashMap<>();
commentsTableMap = new HashMap<>();
Map<Integer, PackageRelationshipCollection> packageRelationshipCollectionMap = MapUtils.newHashMap();
xlsxReadWorkbookHolder.setPackageRelationshipCollectionMap(packageRelationshipCollectionMap);
XSSFReader.SheetIterator ite = (XSSFReader.SheetIterator)xssfReader.getSheetsData();
int index = 0;
if (!ite.hasNext()) {
@ -104,6 +126,20 @@ public class XlsxSaxAnalyser implements ExcelReadExecutor {
commentsTableMap.put(index, commentsTable);
}
}
if (xlsxReadContext.readWorkbookHolder().getExtraReadSet().contains(CellExtraTypeEnum.HYPERLINK)) {
PackageRelationshipCollection packageRelationshipCollection = Optional.ofNullable(ite.getSheetPart())
.map(packagePart -> {
try {
return packagePart.getRelationships();
} catch (InvalidFormatException e) {
log.warn("Reading the Relationship failed", e);
return null;
}
}).orElse(null);
if (packageRelationshipCollection != null) {
packageRelationshipCollectionMap.put(index, packageRelationshipCollection);
}
}
index++;
}
}

25
easyexcel-core/src/main/java/com/alibaba/excel/analysis/v07/handlers/HyperlinkTagHandler.java

@ -1,5 +1,9 @@
package com.alibaba.excel.analysis.v07.handlers;
import java.util.Optional;
import org.apache.poi.openxml4j.opc.PackageRelationship;
import org.apache.poi.openxml4j.opc.PackageRelationshipCollection;
import org.xml.sax.Attributes;
import com.alibaba.excel.constant.ExcelXmlConstants;
@ -23,13 +27,32 @@ public class HyperlinkTagHandler extends AbstractXlsxTagHandler {
@Override
public void startElement(XlsxReadContext xlsxReadContext, String name, Attributes attributes) {
String ref = attributes.getValue(ExcelXmlConstants.ATTRIBUTE_REF);
String location = attributes.getValue(ExcelXmlConstants.ATTRIBUTE_LOCATION);
if (StringUtils.isEmpty(ref)) {
return;
}
// Hyperlink has 2 case:
// case 1,In the 'location' tag
String location = attributes.getValue(ExcelXmlConstants.ATTRIBUTE_LOCATION);
if (location != null) {
CellExtra cellExtra = new CellExtra(CellExtraTypeEnum.HYPERLINK, location, ref);
xlsxReadContext.readSheetHolder().setCellExtra(cellExtra);
xlsxReadContext.analysisEventProcessor().extra(xlsxReadContext);
return;
}
// case 2, In the 'r:id' tag, Then go to 'PackageRelationshipCollection' to get inside
String rId = attributes.getValue(ExcelXmlConstants.ATTRIBUTE_RID);
PackageRelationshipCollection packageRelationshipCollection = xlsxReadContext.xlsxReadSheetHolder()
.getPackageRelationshipCollection();
if (rId == null || packageRelationshipCollection == null) {
return;
}
Optional.ofNullable(packageRelationshipCollection.getRelationshipByID(rId))
.map(PackageRelationship::getTargetURI)
.ifPresent(uri -> {
CellExtra cellExtra = new CellExtra(CellExtraTypeEnum.HYPERLINK, uri.toString(), ref);
xlsxReadContext.readSheetHolder().setCellExtra(cellExtra);
xlsxReadContext.analysisEventProcessor().extra(xlsxReadContext);
});
}
}

5
easyexcel-core/src/main/java/com/alibaba/excel/constant/ExcelXmlConstants.java

@ -49,6 +49,11 @@ public class ExcelXmlConstants {
*/
public static final String ATTRIBUTE_LOCATION = "location";
/**
* rId attribute
*/
public static final String ATTRIBUTE_RID = "r:id";
/**
* Cell range split
*/

8
easyexcel-core/src/main/java/com/alibaba/excel/read/metadata/holder/xlsx/XlsxReadSheetHolder.java

@ -10,6 +10,7 @@ import com.alibaba.excel.read.metadata.holder.ReadWorkbookHolder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import org.apache.poi.openxml4j.opc.PackageRelationshipCollection;
/**
* sheet holder
@ -36,9 +37,16 @@ public class XlsxReadSheetHolder extends ReadSheetHolder {
* Formula for current label.
*/
private StringBuilder tempFormula;
/**
* excel Relationship
*/
private PackageRelationshipCollection packageRelationshipCollection;
public XlsxReadSheetHolder(ReadSheet readSheet, ReadWorkbookHolder readWorkbookHolder) {
super(readSheet, readWorkbookHolder);
this.tagDeque = new LinkedList<String>();
packageRelationshipCollection
= ((XlsxReadWorkbookHolder)readWorkbookHolder).getPackageRelationshipCollectionMap().get(
readSheet.getSheetNo());
}
}

9
easyexcel-core/src/main/java/com/alibaba/excel/read/metadata/holder/xlsx/XlsxReadWorkbookHolder.java

@ -15,6 +15,7 @@ import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.openxml4j.opc.PackageRelationshipCollection;
import org.apache.poi.xssf.model.StylesTable;
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
@ -51,6 +52,11 @@ public class XlsxReadWorkbookHolder extends ReadWorkbookHolder {
*/
private Map<Integer, DataFormatData> dataFormatDataCache;
/**
* excel Relationship, key: sheetNo value: PackageRelationshipCollection
*/
private Map<Integer, PackageRelationshipCollection> packageRelationshipCollectionMap;
public XlsxReadWorkbookHolder(ReadWorkbook readWorkbook) {
super(readWorkbook);
this.saxParserFactoryName = readWorkbook.getXlsxSAXParserFactoryName();
@ -65,6 +71,9 @@ public class XlsxReadWorkbookHolder extends ReadWorkbookHolder {
return null;
}
XSSFCellStyle xssfCellStyle = stylesTable.getStyleAt(dateFormatIndexInteger);
if (xssfCellStyle == null) {
return null;
}
dataFormatData.setIndex(xssfCellStyle.getDataFormat());
dataFormatData.setFormat(BuiltinFormats.getBuiltinFormat(dataFormatData.getIndex(),
xssfCellStyle.getDataFormatString(), globalConfiguration().getLocale()));

44
easyexcel-test/src/test/java/com/alibaba/easyexcel/test/core/extra/ExtraDataTest.java

@ -2,6 +2,7 @@ package com.alibaba.easyexcel.test.core.extra;
import java.io.File;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.slf4j.Logger;
@ -9,10 +10,13 @@ import org.slf4j.LoggerFactory;
import com.alibaba.easyexcel.test.util.TestFileUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.enums.CellExtraTypeEnum;
import com.alibaba.excel.metadata.CellExtra;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.fastjson.JSON;
/**
*
* @author Jiaju Zhuang
*/
public class ExtraDataTest {
@ -20,10 +24,13 @@ public class ExtraDataTest {
private static File file03;
private static File file07;
private static File extraRelationships;
@BeforeClass
public static void init() {
file03 = TestFileUtil.readFile("extra" + File.separator + "extra.xls");
file07 = TestFileUtil.readFile("extra" + File.separator + "extra.xlsx");
extraRelationships = TestFileUtil.readFile("extra" + File.separator + "extraRelationships.xlsx");
}
@Test
@ -36,6 +43,41 @@ public class ExtraDataTest {
read(file03);
}
@Test
public void t03Read() {
EasyExcel.read(extraRelationships, ExtraData.class, new ReadListener() {
@Override
public void invoke(Object data, AnalysisContext context) {
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
}
@Override
public void extra(CellExtra extra, AnalysisContext context) {
LOGGER.info("extra data:{}", JSON.toJSONString(extra));
switch (extra.getType()) {
case HYPERLINK:
if ("222222222".equals(extra.getText())) {
Assert.assertEquals(1, (int)extra.getRowIndex());
Assert.assertEquals(0, (int)extra.getColumnIndex());
} else if ("333333333333".equals(extra.getText())) {
Assert.assertEquals(1, (int)extra.getRowIndex());
Assert.assertEquals(1, (int)extra.getColumnIndex());
} else {
Assert.fail("Unknown hyperlink!");
}
break;
default:
}
}
})
.extraRead(CellExtraTypeEnum.HYPERLINK)
.sheet()
.doRead();
}
private void read(File file) {
EasyExcel.read(file, ExtraData.class, new ExtraDataListener()).extraRead(CellExtraTypeEnum.COMMENT)
.extraRead(CellExtraTypeEnum.HYPERLINK).extraRead(CellExtraTypeEnum.MERGE).sheet().doRead();

129
easyexcel-test/src/test/java/com/alibaba/easyexcel/test/demo/fill/FillTest.java

@ -83,28 +83,13 @@ public class FillTest {
return data();
});
// 方案3.1 分多次 填充 会使用文件缓存(省内存) 使用 try-with-resources @since 3.1.0
// 方案3 分多次 填充 会使用文件缓存(省内存)
fileName = TestFileUtil.getPath() + "listFill" + System.currentTimeMillis() + ".xlsx";
try (ExcelWriter excelWriter = EasyExcel.write(fileName).withTemplate(templateFileName).build()) {
WriteSheet writeSheet = EasyExcel.writerSheet().build();
excelWriter.fill(data(), writeSheet);
excelWriter.fill(data(), writeSheet);
}
// 方案3.2 分多次 填充 会使用文件缓存(省内存) 不使用 try-with-resources
fileName = TestFileUtil.getPath() + "listFill" + System.currentTimeMillis() + ".xlsx";
ExcelWriter excelWriter = null;
try {
excelWriter = EasyExcel.write(fileName).withTemplate(templateFileName).build();
WriteSheet writeSheet = EasyExcel.writerSheet().build();
excelWriter.fill(data(), writeSheet);
excelWriter.fill(data(), writeSheet);
} finally {
// 千万别忘记close 会帮忙关闭流
if (excelWriter != null) {
excelWriter.close();
}
}
}
/**
@ -120,7 +105,7 @@ public class FillTest {
TestFileUtil.getPath() + "demo" + File.separator + "fill" + File.separator + "complex.xlsx";
String fileName = TestFileUtil.getPath() + "complexFill" + System.currentTimeMillis() + ".xlsx";
// 方案1 : 使用 try-with-resources @since 3.1.0
// 方案1
try (ExcelWriter excelWriter = EasyExcel.write(fileName).withTemplate(templateFileName).build()) {
WriteSheet writeSheet = EasyExcel.writerSheet().build();
// 这里注意 入参用了forceNewRow 代表在写入list的时候不管list下面有没有空行 都会创建一行,然后下面的数据往后移动。默认 是false,会直接使用下一行,如果没有则创建。
@ -135,29 +120,6 @@ public class FillTest {
map.put("total", 1000);
excelWriter.fill(map, writeSheet);
}
// 方案2 : 不使用 try-with-resources
ExcelWriter excelWriter = null;
try {
excelWriter = EasyExcel.write(fileName).withTemplate(templateFileName).build();
WriteSheet writeSheet = EasyExcel.writerSheet().build();
// 这里注意 入参用了forceNewRow 代表在写入list的时候不管list下面有没有空行 都会创建一行,然后下面的数据往后移动。默认 是false,会直接使用下一行,如果没有则创建。
// forceNewRow 如果设置了true,有个缺点 就是他会把所有的数据都放到内存了,所以慎用
// 简单的说 如果你的模板有list,且list不是最后一行,下面还有数据需要填充 就必须设置 forceNewRow=true 但是这个就会把所有数据放到内存 会很耗内存
// 如果数据量大 list不是最后一行 参照下一个
FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();
excelWriter.fill(data(), fillConfig, writeSheet);
excelWriter.fill(data(), fillConfig, writeSheet);
Map<String, Object> map = MapUtils.newHashMap();
map.put("date", "2019年10月9日13:28:28");
map.put("total", 1000);
excelWriter.fill(map, writeSheet);
} finally {
// 千万别忘记close 会帮忙关闭流
if (excelWriter != null) {
excelWriter.close();
}
}
}
/**
@ -177,7 +139,7 @@ public class FillTest {
String fileName = TestFileUtil.getPath() + "complexFillWithTable" + System.currentTimeMillis() + ".xlsx";
// 方案1 : 使用 try-with-resources @since 3.1.0
// 方案1
try (ExcelWriter excelWriter = EasyExcel.write(fileName).withTemplate(templateFileName).build()) {
WriteSheet writeSheet = EasyExcel.writerSheet().build();
// 直接写入数据
@ -204,42 +166,6 @@ public class FillTest {
// 总体上写法比较复杂 但是也没有想到好的版本 异步的去写入excel 不支持行的删除和移动,也不支持备注这种的写入,所以也排除了可以
// 新建一个 然后一点点复制过来的方案,最后导致list需要新增行的时候,后面的列的数据没法后移,后续会继续想想解决方案
}
// 方案2 : 不使用 try-with-resources
ExcelWriter excelWriter = null;
try {
excelWriter = EasyExcel.write(fileName).withTemplate(templateFileName).build();
WriteSheet writeSheet = EasyExcel.writerSheet().build();
// 直接写入数据
excelWriter.fill(data(), writeSheet);
excelWriter.fill(data(), writeSheet);
// 写入list之前的数据
Map<String, Object> map = new HashMap<String, Object>();
map.put("date", "2019年10月9日13:28:28");
excelWriter.fill(map, writeSheet);
// list 后面还有个统计 想办法手动写入
// 这里偷懒直接用list 也可以用对象
List<List<String>> totalListList = ListUtils.newArrayList();
List<String> totalList = ListUtils.newArrayList();
totalListList.add(totalList);
totalList.add(null);
totalList.add(null);
totalList.add(null);
// 第四列
totalList.add("统计:1000");
// 这里是write 别和fill 搞错了
excelWriter.write(totalListList, writeSheet);
// 总体上写法比较复杂 但是也没有想到好的版本 异步的去写入excel 不支持行的删除和移动,也不支持备注这种的写入,所以也排除了可以
// 新建一个 然后一点点复制过来的方案,最后导致list需要新增行的时候,后面的列的数据没法后移,后续会继续想想解决方案
} finally {
// 千万别忘记close 会帮忙关闭流
if (excelWriter != null) {
excelWriter.close();
}
}
}
/**
@ -255,7 +181,7 @@ public class FillTest {
TestFileUtil.getPath() + "demo" + File.separator + "fill" + File.separator + "horizontal.xlsx";
String fileName = TestFileUtil.getPath() + "horizontalFill" + System.currentTimeMillis() + ".xlsx";
// 方案1 : 使用 try-with-resources @since 3.1.0
// 方案1
try (ExcelWriter excelWriter = EasyExcel.write(fileName).withTemplate(templateFileName).build()) {
WriteSheet writeSheet = EasyExcel.writerSheet().build();
FillConfig fillConfig = FillConfig.builder().direction(WriteDirectionEnum.HORIZONTAL).build();
@ -266,25 +192,6 @@ public class FillTest {
map.put("date", "2019年10月9日13:28:28");
excelWriter.fill(map, writeSheet);
}
// 方案2 : 不使用 try-with-resources
ExcelWriter excelWriter = null;
try {
excelWriter = EasyExcel.write(fileName).withTemplate(templateFileName).build();
WriteSheet writeSheet = EasyExcel.writerSheet().build();
FillConfig fillConfig = FillConfig.builder().direction(WriteDirectionEnum.HORIZONTAL).build();
excelWriter.fill(data(), fillConfig, writeSheet);
excelWriter.fill(data(), fillConfig, writeSheet);
Map<String, Object> map = new HashMap<String, Object>();
map.put("date", "2019年10月9日13:28:28");
excelWriter.fill(map, writeSheet);
} finally {
// 千万别忘记close 会帮忙关闭流
if (excelWriter != null) {
excelWriter.close();
}
}
}
/**
@ -301,7 +208,7 @@ public class FillTest {
String fileName = TestFileUtil.getPath() + "compositeFill" + System.currentTimeMillis() + ".xlsx";
// 方案1 : 使用 try-with-resources @since 3.1.0
// 方案1
try (ExcelWriter excelWriter = EasyExcel.write(fileName).withTemplate(templateFileName).build()) {
WriteSheet writeSheet = EasyExcel.writerSheet().build();
FillConfig fillConfig = FillConfig.builder().direction(WriteDirectionEnum.HORIZONTAL).build();
@ -319,32 +226,6 @@ public class FillTest {
excelWriter.fill(map, writeSheet);
}
// 方案2 : 不使用 try-with-resources
ExcelWriter excelWriter = null;
try {
excelWriter = EasyExcel.write(fileName).withTemplate(templateFileName).build();
WriteSheet writeSheet = EasyExcel.writerSheet().build();
FillConfig fillConfig = FillConfig.builder().direction(WriteDirectionEnum.HORIZONTAL).build();
// 如果有多个list 模板上必须有{前缀.} 这里的前缀就是 data1,然后多个list必须用 FillWrapper包裹
excelWriter.fill(new FillWrapper("data1", data()), fillConfig, writeSheet);
excelWriter.fill(new FillWrapper("data1", data()), fillConfig, writeSheet);
excelWriter.fill(new FillWrapper("data2", data()), writeSheet);
excelWriter.fill(new FillWrapper("data2", data()), writeSheet);
excelWriter.fill(new FillWrapper("data3", data()), writeSheet);
excelWriter.fill(new FillWrapper("data3", data()), writeSheet);
Map<String, Object> map = new HashMap<String, Object>();
//map.put("date", "2019年10月9日13:28:28");
map.put("date", new Date());
excelWriter.fill(map, writeSheet);
} finally {
// 千万别忘记close 会帮忙关闭流
if (excelWriter != null) {
excelWriter.close();
}
}
}
private List<FillData> data() {

42
easyexcel-test/src/test/java/com/alibaba/easyexcel/test/demo/read/ReadTest.java

@ -47,7 +47,7 @@ public class ReadTest {
// since: 3.0.0-beta1
String fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
// 这里 需要指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭
// 这里每次会读取3000条数据 然后返回过来 直接调用使用数据就行
// 这里每次会读取100条数据 然后返回过来 直接调用使用数据就行
EasyExcel.read(fileName, DemoData.class, new PageReadListener<DemoData>(dataList -> {
for (DemoData demoData : dataList) {
log.info("读取到一条数据{}", JSON.toJSONString(demoData));
@ -98,7 +98,7 @@ public class ReadTest {
// 这里 需要指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭
EasyExcel.read(fileName, DemoData.class, new DemoDataListener()).sheet().doRead();
// 写法4: 使用 try-with-resources @since 3.1.0
// 写法4
fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
// 一个文件一个reader
try (ExcelReader excelReader = EasyExcel.read(fileName, DemoData.class, new DemoDataListener()).build()) {
@ -107,23 +107,6 @@ public class ReadTest {
// 读取一个sheet
excelReader.read(readSheet);
}
// 写法5: 不使用 try-with-resources
fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
// 一个文件一个reader
ExcelReader excelReader = null;
try {
excelReader = EasyExcel.read(fileName, DemoData.class, new DemoDataListener()).build();
// 构建一个sheet 这里可以指定名字或者no
ReadSheet readSheet = EasyExcel.readSheet(0).build();
// 读取一个sheet
excelReader.read(readSheet);
} finally {
if (excelReader != null) {
// 这里千万别忘记关闭,读的时候会创建临时文件,到时磁盘会崩的
excelReader.close();
}
}
}
/**
@ -162,7 +145,7 @@ public class ReadTest {
// 读取部分sheet
fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
// 写法1: 使用 try-with-resources @since 3.1.0
// 写法1
try (ExcelReader excelReader = EasyExcel.read(fileName).build()) {
// 这里为了简单 所以注册了 同样的head 和Listener 自己使用功能必须不同的Listener
ReadSheet readSheet1 =
@ -172,25 +155,6 @@ public class ReadTest {
// 这里注意 一定要把sheet1 sheet2 一起传进去,不然有个问题就是03版的excel 会读取多次,浪费性能
excelReader.read(readSheet1, readSheet2);
}
// 写法2: 不使用 try-with-resources
ExcelReader excelReader = null;
try {
excelReader = EasyExcel.read(fileName).build();
// 这里为了简单 所以注册了 同样的head 和Listener 自己使用功能必须不同的Listener
ReadSheet readSheet1 =
EasyExcel.readSheet(0).head(DemoData.class).registerReadListener(new DemoDataListener()).build();
ReadSheet readSheet2 =
EasyExcel.readSheet(1).head(DemoData.class).registerReadListener(new DemoDataListener()).build();
// 这里注意 一定要把sheet1 sheet2 一起传进去,不然有个问题就是03版的excel 会读取多次,浪费性能
excelReader.read(readSheet1, readSheet2);
} finally {
if (excelReader != null) {
// 这里千万别忘记关闭,读的时候会创建临时文件,到时磁盘会崩的
excelReader.close();
}
}
}
/**

107
easyexcel-test/src/test/java/com/alibaba/easyexcel/test/demo/write/WriteTest.java

@ -86,28 +86,13 @@ public class WriteTest {
// 如果这里想使用03 则 传入excelType参数即可
EasyExcel.write(fileName, DemoData.class).sheet("模板").doWrite(data());
// 写法3:使用 try-with-resources @since 3.1.0
// 写法3
fileName = TestFileUtil.getPath() + "simpleWrite" + System.currentTimeMillis() + ".xlsx";
// 这里 需要指定写用哪个class去写
try (ExcelWriter excelWriter = EasyExcel.write(fileName, DemoData.class).build()) {
WriteSheet writeSheet = EasyExcel.writerSheet("模板").build();
excelWriter.write(data(), writeSheet);
}
// 写法4: 不使用 try-with-resources
fileName = TestFileUtil.getPath() + "simpleWrite" + System.currentTimeMillis() + ".xlsx";
// 这里 需要指定写用哪个class去写
ExcelWriter excelWriter = null;
try {
excelWriter = EasyExcel.write(fileName, DemoData.class).build();
WriteSheet writeSheet = EasyExcel.writerSheet("模板").build();
excelWriter.write(data(), writeSheet);
} finally {
// 千万别忘记close 会帮忙关闭流
if (excelWriter != null) {
excelWriter.close();
}
}
}
/**
@ -185,7 +170,7 @@ public class WriteTest {
*/
@Test
public void repeatedWrite() {
// 方法1.1: 如果写到同一个sheet 使用 try-with-resources @since 3.1.0
// 方法1: 如果写到同一个sheet
String fileName = TestFileUtil.getPath() + "repeatedWrite" + System.currentTimeMillis() + ".xlsx";
// 这里 需要指定写用哪个class去写
try (ExcelWriter excelWriter = EasyExcel.write(fileName, DemoData.class).build()) {
@ -199,28 +184,7 @@ public class WriteTest {
}
}
// 方法1.2: 如果写到同一个sheet 不使用 try-with-resources
fileName = TestFileUtil.getPath() + "repeatedWrite" + System.currentTimeMillis() + ".xlsx";
ExcelWriter writer = null;
try {
// 这里 需要指定写用哪个class去写
writer = EasyExcel.write(fileName, DemoData.class).build();
// 这里注意 如果同一个sheet只要创建一次
WriteSheet writeSheet = EasyExcel.writerSheet("模板").build();
// 去调用写入,这里我调用了五次,实际使用时根据数据库分页的总的页数来
for (int i = 0; i < 5; i++) {
// 分页去数据库查询数据 这里可以去数据库查询每一页的数据
List<DemoData> data = data();
writer.write(data, writeSheet);
}
} finally {
// 千万别忘记close 会帮忙关闭流
if (writer != null) {
writer.close();
}
}
// 方法2.1: 如果写到不同的sheet 同一个对象 使用 try-with-resources @since 3.1.0
// 方法2: 如果写到不同的sheet 同一个对象
fileName = TestFileUtil.getPath() + "repeatedWrite" + System.currentTimeMillis() + ".xlsx";
// 这里 指定文件
try (ExcelWriter excelWriter = EasyExcel.write(fileName, DemoData.class).build()) {
@ -234,27 +198,7 @@ public class WriteTest {
}
}
// 方法2.2: 如果写到不同的sheet 同一个对象 不使用 try-with-resources
fileName = TestFileUtil.getPath() + "repeatedWrite" + System.currentTimeMillis() + ".xlsx";
try {
// 这里 指定文件
writer = EasyExcel.write(fileName, DemoData.class).build();
// 去调用写入,这里我调用了五次,实际使用时根据数据库分页的总的页数来。这里最终会写到5个sheet里面
for (int i = 0; i < 5; i++) {
// 每次都要创建writeSheet 这里注意必须指定sheetNo 而且sheetName必须不一样
WriteSheet writeSheet = EasyExcel.writerSheet(i, "模板" + i).build();
// 分页去数据库查询数据 这里可以去数据库查询每一页的数据
List<DemoData> data = data();
writer.write(data, writeSheet);
}
} finally {
// 千万别忘记close 会帮忙关闭流
if (writer != null) {
writer.close();
}
}
// 方法3.1 如果写到不同的sheet 不同的对象 使用 try-with-resources @since 3.1.0
// 方法3 如果写到不同的sheet 不同的对象
fileName = TestFileUtil.getPath() + "repeatedWrite" + System.currentTimeMillis() + ".xlsx";
// 这里 指定文件
try (ExcelWriter excelWriter = EasyExcel.write(fileName).build()) {
@ -269,26 +213,6 @@ public class WriteTest {
}
}
// 方法3.2 如果写到不同的sheet 不同的对象 不使用 try-with-resources
fileName = TestFileUtil.getPath() + "repeatedWrite" + System.currentTimeMillis() + ".xlsx";
try {
// 这里 指定文件
writer = EasyExcel.write(fileName).build();
// 去调用写入,这里我调用了五次,实际使用时根据数据库分页的总的页数来。这里最终会写到5个sheet里面
for (int i = 0; i < 5; i++) {
// 每次都要创建writeSheet 这里注意必须指定sheetNo 而且sheetName必须不一样。这里注意DemoData.class 可以每次都变,我这里为了方便 所以用的同一个class
// 实际上可以一直变
WriteSheet writeSheet = EasyExcel.writerSheet(i, "模板" + i).head(DemoData.class).build();
// 分页去数据库查询数据 这里可以去数据库查询每一页的数据
List<DemoData> data = data();
writer.write(data, writeSheet);
}
} finally {
// 千万别忘记close 会帮忙关闭流
if (writer != null) {
writer.close();
}
}
}
/**
@ -649,7 +573,7 @@ public class WriteTest {
@Test
public void tableWrite() {
String fileName = TestFileUtil.getPath() + "tableWrite" + System.currentTimeMillis() + ".xlsx";
// 方法1 这里直接写多个table的案例了,如果只有一个 也可以直一行代码搞定,参照其他案, 使用 try-with-resources @since 3.1.0
// 方法1 这里直接写多个table的案例了,如果只有一个 也可以直一行代码搞定,参照其他案
// 这里 需要指定写用哪个class去写
try (ExcelWriter excelWriter = EasyExcel.write(fileName, DemoData.class).build()) {
// 把sheet设置为不需要头 不然会输出sheet的头 这样看起来第一个table 就有2个头了
@ -662,27 +586,6 @@ public class WriteTest {
// 第二次写如也会创建头,然后在第一次的后面写入数据
excelWriter.write(data(), writeSheet, writeTable1);
}
// 方法2 这里直接写多个table的案例了,如果只有一个 也可以直一行代码搞定,参照其他案, 不使用 try-with-resources
// 这里 需要指定写用哪个class去写
ExcelWriter excelWriter = null;
try {
excelWriter = EasyExcel.write(fileName, DemoData.class).build();
// 把sheet设置为不需要头 不然会输出sheet的头 这样看起来第一个table 就有2个头了
WriteSheet writeSheet = EasyExcel.writerSheet("模板").needHead(Boolean.FALSE).build();
// 这里必须指定需要头,table 会继承sheet的配置,sheet配置了不需要,table 默认也是不需要
WriteTable writeTable0 = EasyExcel.writerTable(0).needHead(Boolean.TRUE).build();
WriteTable writeTable1 = EasyExcel.writerTable(1).needHead(Boolean.TRUE).build();
// 第一次写入会创建头
excelWriter.write(data(), writeSheet, writeTable0);
// 第二次写如也会创建头,然后在第一次的后面写入数据
excelWriter.write(data(), writeSheet, writeTable1);
} finally {
// 千万别忘记close 会帮忙关闭流
if (excelWriter != null) {
excelWriter.close();
}
}
}
/**

BIN
easyexcel-test/src/test/resources/extra/extraRelationships.xlsx

Binary file not shown.

4
pom.xml

@ -20,7 +20,7 @@
<properties>
<revision>3.1.0</revision>
<revision>3.1.1</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jdk.version>1.8</jdk.version>
<gpg.skip>true</gpg.skip>
@ -145,7 +145,7 @@
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.78</version>
<version>2.0.6</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>

3
update.md

@ -1,3 +1,6 @@
# 3.1.1
* 修复部分xlsx无法读取超链接的bug
# 3.1.0
* 支持jdk17,去除cglib&asm依赖,改成重新拷贝一份 [Issue #2240](https://github.com/alibaba/easyexcel/issues/2240)
* 升级ehcache 到 3.9.9 ,为了兼容jdk17

Loading…
Cancel
Save