1. Excel解析工具easyexcel全面探索
1.1. 簡介
之前我們想到Excel解析一般是使用POI,但POI存在一個嚴重的問題,就是非常消耗內存。所以阿里人員對它進行了重寫從而誕生了easyexcel,它解決了過於消耗內存問題,也對它進行了封裝讓使用者使用更加便利
接下來我先一一介紹它所有的功能細節、如何使用及部分源碼解析
1.2. Excel讀
1.2.1. 例子
/**
* 最簡單的讀
* <p>1. 創建excel對應的實體對象 參照{@link DemoData}
* <p>2. 由於默認異步讀取excel,所以需要創建excel一行一行的回調監聽器,參照{@link DemoDataListener}
* <p>3. 直接讀即可
*/
@Test
public void simpleRead() {
String fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
// 這里 需要指定讀用哪個class去讀,然后讀取第一個sheet 文件流會自動關閉
EasyExcel.read(fileName, DemoData.class, new DemoDataListener()).sheet().doRead();
}
- 官方說明也比較明確,使用簡單
fileName是路徑+文件名,DemoData是Excel數據對應的實體類,DemoDataListener這看名字就是監聽器,用來監聽處理讀取到的每一條數據
1.2.2. 源碼解析
1.2.2.1. 核心源碼XlsxSaxAnalyser
- 它核心的Excel解析我認為是這個類
XlsxSaxAnalyser,在它的構造方法中做了很多事
public XlsxSaxAnalyser(AnalysisContext analysisContext, InputStream decryptedStream) throws Exception {
...
//從這開始將數據讀取成inputStream流,緩存到了sheetMap
XSSFReader xssfReader = new XSSFReader(pkg);
analysisUse1904WindowDate(xssfReader, readWorkbookHolder);
stylesTable = xssfReader.getStylesTable();
sheetList = new ArrayList<ReadSheet>();
sheetMap = new HashMap<Integer, InputStream>();
XSSFReader.SheetIterator ite = (XSSFReader.SheetIterator)xssfReader.getSheetsData();
int index = 0;
if (!ite.hasNext()) {
throw new ExcelAnalysisException("Can not find any sheet!");
}
while (ite.hasNext()) {
InputStream inputStream = ite.next();
sheetList.add(new ReadSheet(index, ite.getSheetName()));
sheetMap.put(index, inputStream);
index++;
}
}
1.2.2.2. doRead
- 例子中真正開始做解析任務的是
doRead方法,不斷進入此方法,會看到真正執行的最后方法就是XlsxSaxAnalyser類的execute方法;可以看到如下方法中parseXmlSource解析的就是sheetMap緩存的真正數據
@Override
public void execute(List<ReadSheet> readSheetList, Boolean readAll) {
for (ReadSheet readSheet : sheetList) {
readSheet = SheetUtils.match(readSheet, readSheetList, readAll,
analysisContext.readWorkbookHolder().getGlobalConfiguration());
if (readSheet != null) {
analysisContext.currentSheet(readSheet);
parseXmlSource(sheetMap.get(readSheet.getSheetNo()), new XlsxRowHandler(analysisContext, stylesTable));
// The last sheet is read
analysisContext.readSheetHolder().notifyAfterAllAnalysed(analysisContext);
}
}
}
1.2.2.3. 概述DemoDataListener實現
- 對應我們用戶需要手寫的代碼,我們的監聽器
DemoDataListener中有兩個實現方法如下,invoke就對應了上述代碼中的parseXmlSource而doAfterAllAnalysed對應了上述方法中的notifyAfterAllAnalysed,分別表示了先解析每一條數據和當最后一頁讀取完畢通知所有監聽器
@Override
public void invoke(DemoData data, AnalysisContext context) {
LOGGER.info("解析到一條數據:{}", JSON.toJSONString(data));
list.add(data);
if (list.size() >= BATCH_COUNT) {
saveData();
list.clear();
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
saveData();
LOGGER.info("所有數據解析完成!");
}
1.2.2.4. parseXmlSource具體實現
- 看標識重點的地方,這是最核心的解析地
private void parseXmlSource(InputStream inputStream, ContentHandler handler) {
InputSource inputSource = new InputSource(inputStream);
try {
SAXParserFactory saxFactory = SAXParserFactory.newInstance();
saxFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
saxFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);
saxFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
SAXParser saxParser = saxFactory.newSAXParser();
XMLReader xmlReader = saxParser.getXMLReader();
xmlReader.setContentHandler(handler);
//重點
xmlReader.parse(inputSource);
inputStream.close();
} catch (ExcelAnalysisException e) {
throw e;
} catch (Exception e) {
throw new ExcelAnalysisException(e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
throw new ExcelAnalysisException("Can not close 'inputStream'!");
}
}
}
}
- 由於這層層深入非常多,我用一張截圖來表現它的調用形式

1.2.2.5. notifyAfterAllAnalysed具體實現
- 具體看
notifyAfterAllAnalysed的代碼,我們實現的DemoDataListener監聽器繼承AnalysisEventListener,而AnalysisEventListener實現ReadListener接口
@Override
public void notifyAfterAllAnalysed(AnalysisContext analysisContext) {
for (ReadListener readListener : readListenerList) {
readListener.doAfterAllAnalysed(analysisContext);
}
}
1.3. Excel寫
1.3.1. 例子
- 如下例子,使用還是簡單的,和讀比較類似
/**
* 最簡單的寫
* <p>1. 創建excel對應的實體對象 參照{@link com.alibaba.easyexcel.test.demo.write.DemoData}
* <p>2. 直接寫即可
*/
@Test
public void simpleWrite() {
String fileName = TestFileUtil.getPath() + "write" + System.currentTimeMillis() + ".xlsx";
// 這里 需要指定寫用哪個class去讀,然后寫到第一個sheet,名字為模板 然后文件流會自動關閉
// 如果這里想使用03 則 傳入excelType參數即可
EasyExcel.write(fileName, DemoData.class).sheet("模板").doWrite(data());
}
private List<DemoData> data() {
List<DemoData> list = new ArrayList<DemoData>();
for (int i = 0; i < 10; i++) {
DemoData data = new DemoData();
data.setString("字符串" + i);
data.setDate(new Date());
data.setDoubleData(0.56);
list.add(data);
}
return list;
}
1.3.2. 源碼解析
1.3.2.1. doWrite
- 和讀一樣
doWrite才是實際做事的,這次我們從這個入口跟進
public void doWrite(List data) {
if (excelWriter == null) {
throw new ExcelGenerateException("Must use 'EasyExcelFactory.write().sheet()' to call this method");
}
excelWriter.write(data, build());
excelWriter.finish();
}
1.3.2.2. write
- 很明顯,
write是核心,繼續進入ExcelWriter類,看名字addContent就是添加數據了,由excelBuilderExcel建造者來添加,這是ExcelBuilderImpl類
public ExcelWriter write(List data, WriteSheet writeSheet, WriteTable writeTable) {
excelBuilder.addContent(data, writeSheet, writeTable);
return this;
}
1.3.2.3. addContent
- 可以看到如下,顯示封裝和實例化一些數據,創建了
ExcelWriteAddExecutor寫數據執行器,核心就是add方法了
@Override
public void addContent(List data, WriteSheet writeSheet, WriteTable writeTable) {
try {
if (data == null) {
return;
}
context.currentSheet(writeSheet, WriteTypeEnum.ADD);
context.currentTable(writeTable);
if (excelWriteAddExecutor == null) {
excelWriteAddExecutor = new ExcelWriteAddExecutor(context);
}
//核心
excelWriteAddExecutor.add(data);
} catch (RuntimeException e) {
finish();
throw e;
} catch (Throwable e) {
finish();
throw new ExcelGenerateException(e);
}
}
1.3.2.4. add
- 可以看到很明顯在遍歷數據
addOneRowOfDataToExcel插入到Excel表了
public void add(List data) {
if (CollectionUtils.isEmpty(data)) {
return;
}
WriteSheetHolder writeSheetHolder = writeContext.writeSheetHolder();
int newRowIndex = writeSheetHolder.getNewRowIndexAndStartDoWrite();
if (writeSheetHolder.isNew() && !writeSheetHolder.getExcelWriteHeadProperty().hasHead()) {
newRowIndex += writeContext.currentWriteHolder().relativeHeadRowIndex();
}
// BeanMap is out of order,so use fieldList
List<Field> fieldList = new ArrayList<Field>();
for (int relativeRowIndex = 0; relativeRowIndex < data.size(); relativeRowIndex++) {
int n = relativeRowIndex + newRowIndex;
addOneRowOfDataToExcel(data.get(relativeRowIndex), n, relativeRowIndex, fieldList);
}
}
1.3.2.5. addOneRowOfDataToExcel
- 這里先是做創建Excel行的准備,包括行的一些屬性處理器需不需要處理,之后我們的例子是插入java對象,進入
addJavaObjectToExcel方法
private void addOneRowOfDataToExcel(Object oneRowData, int n, int relativeRowIndex, List<Field> fieldList) {
if (oneRowData == null) {
return;
}
WriteHandlerUtils.beforeRowCreate(writeContext, n, relativeRowIndex, Boolean.FALSE);
Row row = WorkBookUtil.createRow(writeContext.writeSheetHolder().getSheet(), n);
WriteHandlerUtils.afterRowCreate(writeContext, row, relativeRowIndex, Boolean.FALSE);
if (oneRowData instanceof List) {
addBasicTypeToExcel((List)oneRowData, row, relativeRowIndex);
} else {
addJavaObjectToExcel(oneRowData, row, relativeRowIndex, fieldList);
}
WriteHandlerUtils.afterRowDispose(writeContext, row, relativeRowIndex, Boolean.FALSE);
}
1.3.2.6. addJavaObjectToExcel
- 在
ExcelWriteAddExecutor執行器類中執行addJavaObjectToExcel,在這里進行了數據的解析,將數據解析成標題和內容,封裝成適合Excel的格式CellData,數據類型等,經過這步我們還沒看到文件流的生成,那么下一步了
private void addJavaObjectToExcel(Object oneRowData, Row row, int relativeRowIndex, List<Field> fieldList) {
WriteHolder currentWriteHolder = writeContext.currentWriteHolder();
BeanMap beanMap = BeanMap.create(oneRowData);
Set<String> beanMapHandledSet = new HashSet<String>();
int cellIndex = 0;
// If it's a class it needs to be cast by type
if (HeadKindEnum.CLASS.equals(writeContext.currentWriteHolder().excelWriteHeadProperty().getHeadKind())) {
Map<Integer, Head> headMap = writeContext.currentWriteHolder().excelWriteHeadProperty().getHeadMap();
Map<Integer, ExcelContentProperty> contentPropertyMap =
writeContext.currentWriteHolder().excelWriteHeadProperty().getContentPropertyMap();
for (Map.Entry<Integer, ExcelContentProperty> entry : contentPropertyMap.entrySet()) {
cellIndex = entry.getKey();
ExcelContentProperty excelContentProperty = entry.getValue();
String name = excelContentProperty.getField().getName();
if (writeContext.currentWriteHolder().ignore(name, cellIndex)) {
continue;
}
if (!beanMap.containsKey(name)) {
continue;
}
Head head = headMap.get(cellIndex);
WriteHandlerUtils.beforeCellCreate(writeContext, row, head, cellIndex, relativeRowIndex, Boolean.FALSE);
Cell cell = WorkBookUtil.createCell(row, cellIndex);
WriteHandlerUtils.afterCellCreate(writeContext, cell, head, relativeRowIndex, Boolean.FALSE);
Object value = beanMap.get(name);
CellData cellData = converterAndSet(currentWriteHolder, excelContentProperty.getField().getType(), cell,
value, excelContentProperty);
WriteHandlerUtils.afterCellDispose(writeContext, cellData, cell, head, relativeRowIndex, Boolean.FALSE);
beanMapHandledSet.add(name);
}
}
// Finish
if (beanMapHandledSet.size() == beanMap.size()) {
return;
}
if (cellIndex != 0) {
cellIndex++;
}
Map<String, Field> ignoreMap = writeContext.currentWriteHolder().excelWriteHeadProperty().getIgnoreMap();
initFieldList(oneRowData.getClass(), fieldList);
for (Field field : fieldList) {
String filedName = field.getName();
boolean uselessData = !beanMap.containsKey(filedName) || beanMapHandledSet.contains(filedName)
|| ignoreMap.containsKey(filedName) || writeContext.currentWriteHolder().ignore(filedName, cellIndex);
if (uselessData) {
continue;
}
Object value = beanMap.get(filedName);
if (value == null) {
continue;
}
WriteHandlerUtils.beforeCellCreate(writeContext, row, null, cellIndex, relativeRowIndex, Boolean.FALSE);
Cell cell = WorkBookUtil.createCell(row, cellIndex++);
WriteHandlerUtils.afterCellCreate(writeContext, cell, null, relativeRowIndex, Boolean.FALSE);
CellData cellData = converterAndSet(currentWriteHolder, value.getClass(), cell, value, null);
WriteHandlerUtils.afterCellDispose(writeContext, cellData, cell, null, relativeRowIndex, Boolean.FALSE);
}
}
1.3.2.7. finish
doWrite中之后還有一步finish
public void finish() {
excelBuilder.finish();
}
- 深入
ExcelBuilderImpl類
@Override
public void finish() {
if (context != null) {
context.finish();
}
}
- 到
WriteContextImpl寫內容實現類的finish方法中,我們可以看到writeWorkbookHolder.getWorkbook().write(writeWorkbookHolder.getOutputStream());這句是重點,將寫Excel持有容器中的內容流輸出;之后就是關閉流,刪除臨時文件的過程
@Override
public void finish() {
WriteHandlerUtils.afterWorkbookDispose(this);
if (writeWorkbookHolder == null) {
return;
}
Throwable throwable = null;
boolean isOutputStreamEncrypt = false;
try {
isOutputStreamEncrypt = doOutputStreamEncrypt07();
} catch (Throwable t) {
throwable = t;
}
if (!isOutputStreamEncrypt) {
try {
// 重點
writeWorkbookHolder.getWorkbook().write(writeWorkbookHolder.getOutputStream());
writeWorkbookHolder.getWorkbook().close();
} catch (Throwable t) {
throwable = t;
}
}
try {
Workbook workbook = writeWorkbookHolder.getWorkbook();
if (workbook instanceof SXSSFWorkbook) {
((SXSSFWorkbook)workbook).dispose();
}
} catch (Throwable t) {
throwable = t;
}
try {
if (writeWorkbookHolder.getAutoCloseStream() && writeWorkbookHolder.getOutputStream() != null) {
writeWorkbookHolder.getOutputStream().close();
}
} catch (Throwable t) {
throwable = t;
}
if (!isOutputStreamEncrypt) {
try {
doFileEncrypt07();
} catch (Throwable t) {
throwable = t;
}
}
try {
if (writeWorkbookHolder.getTempTemplateInputStream() != null) {
writeWorkbookHolder.getTempTemplateInputStream().close();
}
} catch (Throwable t) {
throwable = t;
}
clearEncrypt03();
if (throwable != null) {
throw new ExcelGenerateException("Can not close IO", throwable);
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Finished write.");
}
}
1.4. 文件上傳
- 它提供了一個接收
InputStream的參數,之后和Excel讀沒多大區別
/**
* 文件上傳
* <p>
* 1. 創建excel對應的實體對象 參照{@link UploadData}
* <p>
* 2. 由於默認異步讀取excel,所以需要創建excel一行一行的回調監聽器,參照{@link UploadDataListener}
* <p>
* 3. 直接讀即可
*/
@PostMapping("upload")
@ResponseBody
public String upload(MultipartFile file) throws IOException {
EasyExcel.read(file.getInputStream(), UploadData.class, new UploadDataListener()).sheet().doRead();
return "success";
}
1.5. 文件下載
- 寫入提供參數
OutputStream,其它和文件寫入差不多
/**
* 文件下載
* <p>
* 1. 創建excel對應的實體對象 參照{@link DownloadData}
* <p>
* 2. 設置返回的 參數
* <p>
* 3. 直接寫,這里注意,finish的時候會自動關閉OutputStream,當然你外面再關閉流問題不大
*/
@GetMapping("download")
public void download(HttpServletResponse response) throws IOException {
// 這里注意 有同學反應使用swagger 會導致各種問題,請直接用瀏覽器或者用postman
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
// 這里URLEncoder.encode可以防止中文亂碼 當然和easyexcel沒有關系
String fileName = URLEncoder.encode("測試", "UTF-8");
response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
EasyExcel.write(response.getOutputStream(), DownloadData.class).sheet("模板").doWrite(data());
}
1.6. 讀取技巧
1.6.1. Excel讀取多頁
- 以上都是最基礎的單頁讀寫,在我們調用
sheet()方法時,實際上都是默認第1頁,那么如何讀取多頁?
/**
* 讀多個或者全部sheet,這里注意一個sheet不能讀取多次,多次讀取需要重新讀取文件
* <p>
* 1. 創建excel對應的實體對象 參照{@link DemoData}
* <p>
* 2. 由於默認異步讀取excel,所以需要創建excel一行一行的回調監聽器,參照{@link DemoDataListener}
* <p>
* 3. 直接讀即可
*/
@Test
public void repeatedRead() {
String fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
// 讀取全部sheet
// 這里需要注意 DemoDataListener的doAfterAllAnalysed 會在每個sheet讀取完畢后調用一次。然后所有sheet都會往同一個DemoDataListener里面寫
EasyExcel.read(fileName, DemoData.class, new DemoDataListener()).doReadAll();
// 讀取部分sheet
fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
ExcelReader 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);
// 這里千萬別忘記關閉,讀的時候會創建臨時文件,到時磁盤會崩的
excelReader.finish();
}
- 可以看到
doReadAll方法可以讀取所有sheet頁面 - 若要讀取單獨的頁面,用第二種方式
readSheet(index),index為頁面位置,從0開始計數
1.6.2. 自定義字段轉換
- 在讀取寫入的時候,我們可能會有這樣的需求:比如日期格式轉換,字符串添加固定前綴后綴等等,此時我們可以進行自定義編寫
@Data
public class ConverterData {
/**
* 我自定義 轉換器,不管數據庫傳過來什么 。我給他加上“自定義:”
*/
@ExcelProperty(converter = CustomStringStringConverter.class)
private String string;
/**
* 這里用string 去接日期才能格式化。我想接收年月日格式
*/
@DateTimeFormat("yyyy年MM月dd日HH時mm分ss秒")
private String date;
/**
* 我想接收百分比的數字
*/
@NumberFormat("#.##%")
private String doubleData;
}
- 如上面的
CustomStringStringConverter類為自定義轉換器,可以對字符串進行一定修改,而日期數字的格式化,它已經有提供注解了DateTimeFormat和NumberFormat - 轉換器如下,實現
Converter接口后即可使用supportExcelTypeKey這是判斷單元格類型,convertToJavaData這是讀取轉換,convertToExcelData這是寫入轉換
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.CellData;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
public class CustomStringStringConverter implements Converter<String> {
@Override
public Class supportJavaTypeKey() {
return String.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
/**
* 這里讀的時候會調用
*/
@Override
public String convertToJavaData(CellData cellData, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) {
return "自定義:" + cellData.getStringValue();
}
/**
* 這里是寫的時候會調用 不用管
*/
@Override
public CellData convertToExcelData(String value, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) {
return new CellData(value);
}
}
- 這里解析結果截取部分如下,原數據是
字符串0 2020/1/1 1:01 1
解析到一條數據:{"date":"2020年01月01日01時01分01秒","doubleData":"100%","string":"自定義:字符串0"}
1.6.3. 指定表頭行數
EasyExcel.read(fileName, DemoData.class, new DemoDataListener()).sheet()
// 這里可以設置1,因為頭就是一行。如果多行頭,可以設置其他值。不傳入也可以,因為默認會根據DemoData 來解析,他沒有指定頭,也就是默認1行
.headRowNumber(1).doRead();
1.6.4. 讀取表頭數據
- 只要在實現了
AnalysisEventListener接口的監聽器中,重寫invokeHeadMap方法即可
/**
* 這里會一行行的返回頭
*
* @param headMap
* @param context
*/
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
LOGGER.info("解析到一條頭數據:{}", JSON.toJSONString(headMap));
}
1.6.5. 轉換異常處理
- 只要在實現了
AnalysisEventListener接口的監聽器中,重寫onException方法即可
@Override
public void onException(Exception exception, AnalysisContext context) {
LOGGER.error("解析失敗,但是繼續解析下一行:{}", exception.getMessage());
if (exception instanceof ExcelDataConvertException) {
ExcelDataConvertException excelDataConvertException = (ExcelDataConvertException)exception;
LOGGER.error("第{}行,第{}列解析異常", excelDataConvertException.getRowIndex(),
excelDataConvertException.getColumnIndex());
}
}
1.6.6. 讀取單元格參數和類型
- 將類屬性用
CellData封裝起來
@Data
public class CellDataReadDemoData {
private CellData<String> string;
// 這里注意 雖然是日期 但是 類型 存儲的是number 因為excel 存儲的就是number
private CellData<Date> date;
private CellData<Double> doubleData;
// 這里並不一定能完美的獲取 有些公式是依賴性的 可能會讀不到 這個問題后續會修復
private CellData<String> formulaValue;
}
- 這樣讀取到的數據如下,會包含單元格數據類型
解析到一條數據:{"date":{"data":1577811661000,"dataFormat":22,"dataFormatString":"m/d/yy h:mm","formula":false,"numberValue":43831.0423726852,"type":"NUMBER"},"doubleData":{"data":1.0,"formula":false,"numberValue":1,"type":"NUMBER"},"formulaValue":{"data":"字符串01","formula":true,"formulaValue":"_xlfn.CONCAT(A2,C2)","stringValue":"字符串01","type":"STRING"},"string":{"data":"字符串0","dataFormat":0,"dataFormatString":"General","formula":false,"stringValue":"字符串0","type":"STRING"}}
1.6.7. 同步返回
- 不推薦使用,但如果特定情況一定要用,可以如下,主要為
doReadSync方法,直接返回List
/**
* 同步的返回,不推薦使用,如果數據量大會把數據放到內存里面
*/
@Test
public void synchronousRead() {
String fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
// 這里 需要指定讀用哪個class去讀,然后讀取第一個sheet 同步讀取會自動finish
List<Object> list = EasyExcel.read(fileName).head(DemoData.class).sheet().doReadSync();
for (Object obj : list) {
DemoData data = (DemoData)obj;
LOGGER.info("讀取到數據:{}", JSON.toJSONString(data));
}
// 這里 也可以不指定class,返回一個list,然后讀取第一個sheet 同步讀取會自動finish
list = EasyExcel.read(fileName).sheet().doReadSync();
for (Object obj : list) {
// 返回每條數據的鍵值對 表示所在的列 和所在列的值
Map<Integer, String> data = (Map<Integer, String>)obj;
LOGGER.info("讀取到數據:{}", JSON.toJSONString(data));
}
}
1.6.8. 無對象的讀
- 顧名思義,不創建實體對象來讀取Excel數據,那么我們就用Map接收,但這種對日期不友好,對於簡單字段的讀取可以使用
- 其它都一樣,監聽器的繼承中泛型參數變為Map即可
public class NoModleDataListener extends AnalysisEventListener<Map<Integer, String>> {
...
}
- 結果截取如下
解析到一條數據:{0:"字符串0",1:"2020-01-01 01:01:01",2:"1"}
1.7. 寫入技巧
1.7.1. 排除特定字段和只寫入特定字段
- 使用
excludeColumnFiledNames來排除特定字段寫入,用includeColumnFiledNames表示只寫入特定字段
/**
* 根據參數只導出指定列
* <p>
* 1. 創建excel對應的實體對象 參照{@link DemoData}
* <p>
* 2. 根據自己或者排除自己需要的列
* <p>
* 3. 直接寫即可
*/
@Test
public void excludeOrIncludeWrite() {
String fileName = TestFileUtil.getPath() + "excludeOrIncludeWrite" + System.currentTimeMillis() + ".xlsx";
// 根據用戶傳入字段 假設我們要忽略 date
Set<String> excludeColumnFiledNames = new HashSet<String>();
excludeColumnFiledNames.add("date");
// 這里 需要指定寫用哪個class去讀,然后寫到第一個sheet,名字為模板 然后文件流會自動關閉
EasyExcel.write(fileName, DemoData.class).excludeColumnFiledNames(excludeColumnFiledNames).sheet("模板")
.doWrite(data());
fileName = TestFileUtil.getPath() + "excludeOrIncludeWrite" + System.currentTimeMillis() + ".xlsx";
// 根據用戶傳入字段 假設我們只要導出 date
Set<String> includeColumnFiledNames = new HashSet<String>();
includeColumnFiledNames.add("date");
// 這里 需要指定寫用哪個class去讀,然后寫到第一個sheet,名字為模板 然后文件流會自動關閉
EasyExcel.write(fileName, DemoData.class).includeColumnFiledNames(includeColumnFiledNames).sheet("模板")
.doWrite(data());
}
1.7.2. 指定寫入列
- 寫入列的順序可以進行指定,在實體類注解上指定index,從小到大,從左到右排列
@Data
public class IndexData {
@ExcelProperty(value = "字符串標題", index = 0)
private String string;
@ExcelProperty(value = "日期標題", index = 1)
private Date date;
/**
* 這里設置3 會導致第二列空的
*/
@ExcelProperty(value = "數字標題", index = 3)
private Double doubleData;
}
1.7.3. 復雜頭寫入
- 如下圖這種復雜頭

- 我們可以通過修改實體類注解實現
@Data
public class ComplexHeadData {
@ExcelProperty({"主標題", "字符串標題"})
private String string;
@ExcelProperty({"主標題", "日期標題"})
private Date date;
@ExcelProperty({"主標題", "數字標題"})
private Double doubleData;
}
1.7.4. 重復多次寫入
- 分為三種:1. 重復寫入同一個sheet;2. 同一個對象寫入不同sheet;3. 不同的對象寫入不同的sheet
/**
* 重復多次寫入
* <p>
* 1. 創建excel對應的實體對象 參照{@link ComplexHeadData}
* <p>
* 2. 使用{@link ExcelProperty}注解指定復雜的頭
* <p>
* 3. 直接調用二次寫入即可
*/
@Test
public void repeatedWrite() {
// 方法1 如果寫到同一個sheet
String fileName = TestFileUtil.getPath() + "repeatedWrite" + System.currentTimeMillis() + ".xlsx";
// 這里 需要指定寫用哪個class去讀
ExcelWriter excelWriter = EasyExcel.write(fileName, DemoData.class).build();
// 這里注意 如果同一個sheet只要創建一次
WriteSheet writeSheet = EasyExcel.writerSheet("模板").build();
// 去調用寫入,這里我調用了五次,實際使用時根據數據庫分頁的總的頁數來
for (int i = 0; i < 5; i++) {
// 分頁去數據庫查詢數據 這里可以去數據庫查詢每一頁的數據
List<DemoData> data = data();
writeSheet.setSheetName("模板");
excelWriter.write(data, writeSheet);
}
/// 千萬別忘記finish 會幫忙關閉流
excelWriter.finish();
// 方法2 如果寫到不同的sheet 同一個對象
fileName = TestFileUtil.getPath() + "repeatedWrite" + System.currentTimeMillis() + ".xlsx";
// 這里 指定文件
excelWriter = EasyExcel.write(fileName, DemoData.class).build();
// 去調用寫入,這里我調用了五次,實際使用時根據數據庫分頁的總的頁數來。這里最終會寫到5個sheet里面
for (int i = 0; i < 5; i++) {
// 每次都要創建writeSheet 這里注意必須指定sheetNo
writeSheet = EasyExcel.writerSheet(i, "模板"+i).build();
// 分頁去數據庫查詢數據 這里可以去數據庫查詢每一頁的數據
List<DemoData> data = data();
excelWriter.write(data, writeSheet);
}
/// 千萬別忘記finish 會幫忙關閉流
excelWriter.finish();
// 方法3 如果寫到不同的sheet 不同的對象
fileName = TestFileUtil.getPath() + "repeatedWrite" + System.currentTimeMillis() + ".xlsx";
// 這里 指定文件
excelWriter = EasyExcel.write(fileName).build();
// 去調用寫入,這里我調用了五次,實際使用時根據數據庫分頁的總的頁數來。這里最終會寫到5個sheet里面
for (int i = 0; i < 5; i++) {
// 每次都要創建writeSheet 這里注意必須指定sheetNo。這里注意DemoData.class 可以每次都變,我這里為了方便 所以用的同一個class 實際上可以一直變
writeSheet = EasyExcel.writerSheet(i, "模板"+i).head(DemoData.class).build();
// 分頁去數據庫查詢數據 這里可以去數據庫查詢每一頁的數據
List<DemoData> data = data();
excelWriter.write(data, writeSheet);
}
/// 千萬別忘記finish 會幫忙關閉流
excelWriter.finish();
}
1.7.5. 圖片導出
- 對圖片的導出,可能會有這樣的需求,它提供了四種數據類型的導出,還是很豐富的
@Test
public void imageWrite() throws Exception {
String fileName = TestFileUtil.getPath() + "imageWrite" + System.currentTimeMillis() + ".xlsx";
// 如果使用流 記得關閉
InputStream inputStream = null;
try {
List<ImageData> list = new ArrayList<ImageData>();
ImageData imageData = new ImageData();
list.add(imageData);
String imagePath = TestFileUtil.getPath() + "converter" + File.separator + "img.jpg";
// 放入四種類型的圖片 實際使用只要選一種即可
imageData.setByteArray(FileUtils.readFileToByteArray(new File(imagePath)));
imageData.setFile(new File(imagePath));
imageData.setString(imagePath);
inputStream = FileUtils.openInputStream(new File(imagePath));
imageData.setInputStream(inputStream);
EasyExcel.write(fileName, ImageData.class).sheet().doWrite(list);
} finally {
if (inputStream != null) {
inputStream.close();
}
}
}
- 圖片類為
@Data
@ContentRowHeight(100)
@ColumnWidth(100 / 8)
public class ImageData {
private File file;
private InputStream inputStream;
/**
* 如果string類型 必須指定轉換器,string默認轉換成string
*/
@ExcelProperty(converter = StringImageConverter.class)
private String string;
private byte[] byteArray;
}
導出結果:兩行四列,每列都對應一張圖片,四種導出類型均可

- 其中
StringImageConverter自定義轉換器為
public class StringImageConverter implements Converter<String> {
@Override
public Class supportJavaTypeKey() {
return String.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.IMAGE;
}
@Override
public String convertToJavaData(CellData cellData, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) {
throw new UnsupportedOperationException("Cannot convert images to string");
}
@Override
public CellData convertToExcelData(String value, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) throws IOException {
return new CellData(FileUtils.readFileToByteArray(new File(value)));
}
}
1.7.6. 字段寬高設置
- 設置實體類注解屬性即可
@Data
@ContentRowHeight(10)
@HeadRowHeight(20)
@ColumnWidth(25)
public class WidthAndHeightData {
@ExcelProperty("字符串標題")
private String string;
@ExcelProperty("日期標題")
private Date date;
/**
* 寬度為50
*/
@ColumnWidth(50)
@ExcelProperty("數字標題")
private Double doubleData;
}
1.7.7. 自定義樣式
- 實現會比較復雜,需要做頭策略,內容策略,字體大小等
@Test
public void styleWrite() {
String fileName = TestFileUtil.getPath() + "styleWrite" + System.currentTimeMillis() + ".xlsx";
// 頭的策略
WriteCellStyle headWriteCellStyle = new WriteCellStyle();
// 背景設置為紅色
headWriteCellStyle.setFillForegroundColor(IndexedColors.RED.getIndex());
WriteFont headWriteFont = new WriteFont();
headWriteFont.setFontHeightInPoints((short)20);
headWriteCellStyle.setWriteFont(headWriteFont);
// 內容的策略
WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
// 這里需要指定 FillPatternType 為FillPatternType.SOLID_FOREGROUND 不然無法顯示背景顏色.頭默認了 FillPatternType所以可以不指定
contentWriteCellStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND);
// 背景綠色
contentWriteCellStyle.setFillForegroundColor(IndexedColors.GREEN.getIndex());
WriteFont contentWriteFont = new WriteFont();
// 字體大小
contentWriteFont.setFontHeightInPoints((short)20);
contentWriteCellStyle.setWriteFont(contentWriteFont);
// 這個策略是 頭是頭的樣式 內容是內容的樣式 其他的策略可以自己實現
HorizontalCellStyleStrategy horizontalCellStyleStrategy =
new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);
// 這里 需要指定寫用哪個class去讀,然后寫到第一個sheet,名字為模板 然后文件流會自動關閉
EasyExcel.write(fileName, DemoData.class).registerWriteHandler(horizontalCellStyleStrategy).sheet("模板")
.doWrite(data());
}
- 效果如下

1.7.8. 單元格合並
@Test
public void mergeWrite() {
String fileName = TestFileUtil.getPath() + "mergeWrite" + System.currentTimeMillis() + ".xlsx";
// 每隔2行會合並。當然其他合並策略也可以自己寫
LoopMergeStrategy loopMergeStrategy = new LoopMergeStrategy(2, 0);
// 這里 需要指定寫用哪個class去讀,然后寫到第一個sheet,名字為模板 然后文件流會自動關閉
EasyExcel.write(fileName, DemoData.class).registerWriteHandler(loopMergeStrategy).sheet("模板").doWrite(data());
}
- 效果如下,第一列單元格數據,2,3兩行合並

1.7.9. 自動列寬
- 根據作者描述,POI對中文的自動列寬適配不友好,easyexcel對數字也不能准確適配列寬,他提供的適配策略可以用,但不能精確適配,可以自己重寫
- 想用就注冊處理器
LongestMatchColumnWidthStyleStrategy
@Test
public void longestMatchColumnWidthWrite() {
String fileName =
TestFileUtil.getPath() + "longestMatchColumnWidthWrite" + System.currentTimeMillis() + ".xlsx";
// 這里 需要指定寫用哪個class去讀,然后寫到第一個sheet,名字為模板 然后文件流會自動關閉
EasyExcel.write(fileName, LongestMatchColumnWidthData.class)
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()).sheet("模板").doWrite(dataLong());
}
1.7.10. 下拉,超鏈接
- 下拉,超鏈接等功能需要自定義實現
@Test
public void customHandlerWrite() {
String fileName = TestFileUtil.getPath() + "customHandlerWrite" + System.currentTimeMillis() + ".xlsx";
// 這里 需要指定寫用哪個class去讀,然后寫到第一個sheet,名字為模板 然后文件流會自動關閉
EasyExcel.write(fileName, DemoData.class).registerWriteHandler(new CustomSheetWriteHandler())
.registerWriteHandler(new CustomCellWriteHandler()).sheet("模板").doWrite(data());
}
- 其中主要為處理器
CustomCellWriteHandler類,其實現CellWriteHandler接口,我們在后處理方法afterCellDispose做處理
public class CustomCellWriteHandler implements CellWriteHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(CustomCellWriteHandler.class);
@Override
public void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row,
Head head, Integer columnIndex, Integer relativeRowIndex, Boolean isHead) {
}
@Override
public void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell,
Head head, Integer relativeRowIndex, Boolean isHead) {
}
@Override
public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder,
List<CellData> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
// 這里可以對cell進行任何操作
LOGGER.info("第{}行,第{}列寫入完成。", cell.getRowIndex(), cell.getColumnIndex());
if (isHead && cell.getColumnIndex() == 0) {
CreationHelper createHelper = writeSheetHolder.getSheet().getWorkbook().getCreationHelper();
Hyperlink hyperlink = createHelper.createHyperlink(HyperlinkType.URL);
hyperlink.setAddress("https://github.com/alibaba/easyexcel");
cell.setHyperlink(hyperlink);
}
}
}
1.7.11. 不創建對象的寫
- 在設置write的時候不設置對象類,在head里添加
List<List<String>>的對象頭
@Test
public void noModleWrite() {
// 寫法1
String fileName = TestFileUtil.getPath() + "noModleWrite" + System.currentTimeMillis() + ".xlsx";
// 這里 需要指定寫用哪個class去讀,然后寫到第一個sheet,名字為模板 然后文件流會自動關閉
EasyExcel.write(fileName).head(head()).sheet("模板").doWrite(dataList());
}
private List<List<String>> head() {
List<List<String>> list = new ArrayList<List<String>>();
List<String> head0 = new ArrayList<String>();
head0.add("字符串" + System.currentTimeMillis());
List<String> head1 = new ArrayList<String>();
head1.add("數字" + System.currentTimeMillis());
List<String> head2 = new ArrayList<String>();
head2.add("日期" + System.currentTimeMillis());
list.add(head0);
list.add(head1);
list.add(head2);
return list;
}
1.8. 總結
- 不知不覺列出了這么多easyexcel的使用技巧和方式,這里應該囊括了大部分我們工作中常用到的excel讀寫技巧,歡迎收藏查閱
