一、需求描述
最近系統後臺有個關於使用Excel文件導出數據的需求,首先想到了POI和阿里的EasyExcel。經過技術選型和結合具體場景,最終決定使用EasyExcel。
使用過程中,遇到了一些問題,然後去查找資料,發現大多數資料都比較老舊,都是使用1.x或者一些beta版本,爲了解決這個問題,我就自己去查閱了資料,爲了方便後續使用,在此記錄一下,也和大家分享一下,如何優雅的使用EasyExcel導入和導出文件。
二、性能比較
主流office文檔操作組件性能比較。
組件 | 功能簡介 | 使用場景 | 測試環境 | 內存消耗 | 讀取時間 | 寫入時間 | 文件大小 |
---|---|---|---|---|---|---|---|
poi | 1. 對Microsoft Office格式檔案讀和寫的功能 2. HSSF提供讀寫Excel XLS 3. HPSF提供讀寫OLE2 Property Sets 4. POIFS提供讀寫OLE2 Filesystem |
1. 操作Excel XLS 2. HSSFWorkbook只能解析2003之前版本xls格式 3. 使用HSSF時sheet最大行數65536,最大列數256 |
Win64 4核8g jdk1.8 5萬行2列excel xls | R:206.88MB W:138.34MB | 1049ms | 2005ms | 4.15MB |
poi-ooxml | 1. poi升級擴展版本 2. XSSF提供讀寫XLSX 3. XSLF提供讀寫PPTX 4. XWPF提供讀寫DOCX 5. CommonSS讀寫XLS、XLSX |
1. 操作pptx、docx、xlsx等 2. XSSF基於內存寫入方式,一個sheet最大行數1048576,最大列數16384 3. SXSSF是在XSSF基礎上基於內存+磁盤寫入方式,用於大數據量的導出 |
Win64 4核8g jdk1.8 5萬行2列excel xlsx/5萬行word docx | XSSF-R:185.04MB XSSF-W:405.58MB SXSSF-R:140.34MB SXSSF-W:41.83MB XWPF-R:23.14MB XWPF-W:158.21MB | XSSF:2502ms SXSSF:1354ms XWPF:634ms | XSSF:4644ms SXSSF:1417ms XWPF:21555ms | XSSF:1.36MB SXSSF:1.33MB XWPF:999KB |
poi-scratchpad | 1. HWPF提供讀寫Word DOC 2. HSLF提供讀寫PPT 3. HDGF提供讀Visio VSD 4. HPBF提供讀Publisher PUB 5. HSMF提供讀Outlook MSG |
1. 操作PPT、DOC、VSD、PUB、MSG等格式 2. HWPFDocument寫doc文件必須要先有doc文件 3. 不建議使用HWPF等低版本office |
Win64 4核8g jdk1.8 5萬行word doc | R:81.80MB W:90.39MB | 221ms | 538ms | 3.74MB |
easyexcel | 阿里開源,重寫了poi對Excel2007版的解析,不會出現OOM,2003版依賴POI的sax模式 | xls、xlsx操作 | Win64 4核8g jdk1.8 5萬行2列xlsx | R:80.10MB W:60.56MB | 1053ms | 1149ms | 1.33MB |
三、項目代碼
源碼地址:https://github.com/wangming2674/easyexcel-basic-demo
1.項目依賴
demo項目基於springboot2.2.1版本。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.1.RELEASE</version>
<relativePath/>
</parent>
<groupId>com.evan.easyexcel</groupId>
<artifactId>easyexcel-basic-demo</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>easyexcel-basic-demo</name>
<description>Easy excel 2.x import and export demo</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>4.1.0</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.0</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-scratchpad</artifactId>
<version>4.1.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.1.6</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.ExcelApplication 主啓動類
package com.evan.easyexcel;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ExcelApplication {
public static void main(String[] args) {
SpringApplication.run(ExcelApplication.class, args);
}
}
3.定義導入和導出模型
package com.evan.easyexcel.model;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.alibaba.excel.annotation.write.style.ContentRowHeight;
import com.alibaba.excel.annotation.write.style.HeadRowHeight;
import lombok.Data;
/**
* @ClassName ExportModel
* @Description 導出模型
* @Author Evan Wang
* @Version 1.0.0
* @Date 2020/4/1 20:55
*/
@ContentRowHeight(20)
@HeadRowHeight(25)
@ColumnWidth(25)
@Data
public class ExportModel {
@ExcelProperty(value = "姓名" ,index = 0)
private String name;
@ExcelProperty(value = "性別" ,index = 1)
private String sex;
@ExcelProperty(value = "年齡" ,index = 2)
private Integer age;
}
package com.evan.easyexcel.model;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
/**
* @ClassName ImportModel
* @Description 導入模型
* @Author Evan Wang
* @Version 1.0.0
* @Date 2020/4/1 20:54
*/
@Data
public class ImportModel {
@ExcelProperty(index = 0)
private String date;
@ExcelProperty(index = 1)
private String author;
@ExcelProperty(index = 2)
private String book;
}
4.Object與實體類轉換工具類
package com.evan.easyexcel.utils.common;
import org.springframework.beans.BeanUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* @ClassName ExportModel
* @Description Object與實體類轉換工具類
* @Author Evan Wang
* @Version 1.0.0
* @Date 2020/4/1 20:59
*/
public class BeanConvert {
private BeanConvert(){}
/**
* 將List<Object> 轉換爲List<Bean>
* @param sources 源對象
* @param targetClass 目標類
* @param <T>
* @return
*/
public static <T> List<T> objectConvertBean(List<?> sources, Class<T> targetClass) {
List<?> sourcesObj = sources;
if (sourcesObj == null) {
sourcesObj = Collections.emptyList();
}
List<T> targets = new ArrayList<>(sourcesObj.size());
convert(sourcesObj, targets, targetClass);
return targets;
}
/**
* 複製源對象到目的對象
* 注意:
* org.springframework.beans.BeanUtils.copyProperties 是一個Spring提供的名稱相同的工具類
* 但它不支持類型自動轉換,如果某個類型屬性不同,則不予轉換那個屬性
* org.apache.commons.beanutils.BeanUtils 是一個Apache提供的名稱相同的工具類
* 支持類型自動轉換,如Date類型會自動轉換爲字符串
* @param sources 源對象
* @param targets 目的對象
* @param targetClass 目標類
* @param <T>
*/
private static <T> void convert(List<?> sources, List<T> targets, Class<T> targetClass) {
if (sources == null) {
return;
}
if (targets == null) {
return;
}
targets.clear();
for (Object obj : sources) {
try {
T target = targetClass.newInstance();
targets.add(target);
BeanUtils.copyProperties(obj, target);
} catch (Exception e) {
return;
}
}
}
}
5.ExcelListener 監聽器
package com.evan.easyexcel.utils.excel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.evan.easyexcel.model.ImportModel;
import java.util.ArrayList;
import java.util.List;
/**
* @Author Evan Wang
* @Description 監聽類
* @Date 2019-09-16
*/
public class ExcelListener extends AnalysisEventListener {
//可以通過實例獲取該值
private List<Object> dataList = new ArrayList<>();
@Override
public void invoke(Object object, AnalysisContext context) {
//數據存儲到list,供批量處理,或後續自己業務邏輯處理。
dataList.add(object);
handleBusinessLogic();
/*
如數據過大,可以進行定量分批處理
if(dataList.size()>=200){
handleBusinessLogic();
dataList.clear();
}
*/
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
//非必要語句,查看導入的數據
System.out.println("導入的數據 " + dataList.toString());
//解析結束銷燬不用的資源
dataList.clear();
}
//根據業務自行實現該方法,例如將解析好的dataList存儲到數據庫中
private void handleBusinessLogic() {
}
public List<Object> getDataList() {
return dataList;
}
public void setDataList(List<Object> dataList) {
this.dataList = dataList;
}
}
6.ExcelUtil (核心類)
ExcelUtil對easyexcel2.X進行封裝,實現一個方法完成簡單的excel導入和導出。
package com.evan.easyexcel.utils.excel;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelReader;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.read.metadata.ReadSheet;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.metadata.style.WriteFont;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
import com.evan.easyexcel.utils.common.BeanConvert;
import org.apache.poi.ss.usermodel.FillPatternType;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
/**
*
* @Author Evan Wang
* @Description Excel讀寫工具類
* @Date 2019-09-16
*/
public class ExcelUtil {
private ExcelUtil(){}
/**
* 讀取Excel(一個sheet)
* @param excel 文件
* @param clazz 實體類
* @param sheetNo sheet序號
* @return 返回實體列表(需轉換)
*/
public static <T> List<T> readExcel(MultipartFile excel, Class<T> clazz,int sheetNo) {
ExcelListener excelListener = new ExcelListener();
ExcelReader excelReader = getReader(excel,clazz,excelListener);
if (excelReader == null) {
return new ArrayList<>();
}
ReadSheet readSheet = EasyExcel.readSheet(sheetNo).build();
excelReader.read(readSheet);
excelReader.finish();
return BeanConvert.objectConvertBean(excelListener.getDataList(), clazz);
}
/**
* 讀取Excel(多個sheet可以用同一個實體類解析)
* @param excel 文件
* @param clazz 實體類
* @return 返回實體列表(需轉換)
*/
public static <T> List<T> readExcel(MultipartFile excel, Class<T> clazz) {
ExcelListener excelListener = new ExcelListener();
ExcelReader excelReader = getReader(excel,clazz,excelListener);
if (excelReader == null) {
return new ArrayList<>();
}
List<ReadSheet> readSheetList = excelReader.excelExecutor().sheetList();
for (ReadSheet readSheet:readSheetList){
excelReader.read(readSheet);
}
excelReader.finish();
return BeanConvert.objectConvertBean(excelListener.getDataList(), clazz);
}
/**
* 導出Excel(一個sheet)
*
* @param response HttpServletResponse
* @param list 數據list
* @param fileName 導出的文件名
* @param sheetName 導入文件的sheet名
* @param clazz 實體類
*/
public static <T> void writeExcel(HttpServletResponse response,List<T> list, String fileName, String sheetName, Class<T> clazz) {
OutputStream outputStream = getOutputStream(response, fileName);
ExcelWriter excelWriter = EasyExcel.write(outputStream, clazz).build();
WriteSheet writeSheet = EasyExcel.writerSheet(sheetName).build();
excelWriter.write(list, writeSheet);
excelWriter.finish();
}
/**
* 導出Excel(帶樣式)
*
* @return
*/
public static <T> void writeStyleExcel(HttpServletResponse response,List<T> list, String fileName, String sheetName, Class<T> clazz) {
//表頭策略
WriteCellStyle headWriteCellStyle = new WriteCellStyle();
//背景淺灰
headWriteCellStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.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.LIGHT_GREEN.getIndex());
WriteFont contentWriteFont = new WriteFont();
//字體大小
contentWriteFont.setFontHeightInPoints((short)15);
contentWriteCellStyle.setWriteFont(contentWriteFont);
HorizontalCellStyleStrategy horizontalCellStyleStrategy = new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);
OutputStream outputStream = getOutputStream(response, fileName);
EasyExcel.write(outputStream, clazz).registerWriteHandler(horizontalCellStyleStrategy).sheet(sheetName).doWrite(list);
}
/**
* 導出Excel(動態表頭)
* write時不傳入class,table時傳入並設置needHead爲false
* @return
*/
public static <T> void writeDynamicHeadExcel(HttpServletResponse response,List<T> list, String fileName, String sheetName, Class<T> clazz,List<List<String>> headList) {
OutputStream outputStream = getOutputStream(response, fileName);
EasyExcel.write(outputStream)
.head(headList)
.sheet(sheetName)
.table().head(clazz).needHead(Boolean.FALSE)
.doWrite(list);
}
/**
* 導出時生成OutputStream
*/
private static OutputStream getOutputStream(HttpServletResponse response,String fileName) {
//創建本地文件
String filePath = fileName + ".xlsx";
File file = new File(filePath);
try {
if (!file.exists() || file.isDirectory()) {
file.createNewFile();
}
fileName = new String(filePath.getBytes(), "ISO-8859-1");
response.addHeader("Content-Disposition", "filename=" + fileName);
return response.getOutputStream();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* 返回ExcelReader
* @param excel 文件
* @param clazz 實體類
* @param excelListener
*/
private static <T> ExcelReader getReader(MultipartFile excel, Class<T> clazz, ExcelListener excelListener) {
String filename = excel.getOriginalFilename();
try {
if (filename == null || (!filename.toLowerCase().endsWith(".xls") && !filename.toLowerCase().endsWith(".xlsx"))) {
return null;
}
InputStream inputStream = new BufferedInputStream(excel.getInputStream());
ExcelReader excelReader = EasyExcel.read(inputStream, clazz, excelListener).build();
inputStream.close();
return excelReader;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
7.ExcelTestController
package com.evan.easyexcel.controller;
import com.evan.easyexcel.model.ExportModel;
import com.evan.easyexcel.model.ImportModel;
import com.evan.easyexcel.utils.excel.ExcelUtil;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.List;
/**
* @ClassName ExcelTestController
* @Description
* @Author Evan Wang
* @Version 1.0.0
* @Date 2020/4/1 21:25
*/
@RestController
@RequestMapping(value = "/easyExcel")
public class ExcelTestController {
@PostMapping(value = "/import")
public List<ImportModel> read(MultipartFile excel) {
return ExcelUtil.readExcel(excel, ImportModel.class, 0);
}
@GetMapping(value = "/export")
public void writeExcel(HttpServletResponse response) {
List<ExportModel> list = getList();
String fileName = "Excel導出測試";
String sheetName = "sheet1";
ExcelUtil.writeDynamicHeadExcel(response, list, fileName, sheetName, ExportModel.class, head());
}
private List<ExportModel> getList() {
List<ExportModel> modelList = new ArrayList<>();
ExportModel firstModel = new ExportModel();
firstModel.setName("李明");
firstModel.setSex("男");
firstModel.setAge(20);
modelList.add(firstModel);
ExportModel secondModel = new ExportModel();
secondModel.setName("珍妮");
secondModel.setSex("女");
secondModel.setAge(19);
modelList.add(secondModel);
return modelList;
}
private List<List<String>> head() {
List<List<String>> headList = new ArrayList<>();
List<String> nameHead = new ArrayList<>();
nameHead.add("姓名");
List<String> genderHead = new ArrayList<>();
genderHead.add("性別");
List<String> ageHead = new ArrayList<>();
ageHead.add("年齡");
headList.add(nameHead);
headList.add(genderHead);
headList.add(ageHead);
return headList;
}
}
8.application.yml 端口號配置
server:
port: 8090
四、項目測試
1.導出測試
先啓動項目,然後在瀏覽器中訪問如下地址。
http://localhost:8090/easyExcel/export
然後可以看到,瀏覽器彈出了下載框,自動開始下載。
打開下載好的Excel,如果看到如下內容,證明導出成功。
2.導入測試
注:這裏我把提前準備好的Excel文件放在如下路徑供大家測試使用。
要導入的Excel文件內容:
導入測試這裏我使用的測試工具是postman,大家也可以根據需要選擇自己喜歡的測試工具,注意箭頭位置一定要跟我選擇的一致,否則不能導入。
http://localhost:8090/easyExcel/import
選完後,點擊send,成功請求後查看控制檯,出現以下內容,代表導入成功。
導入的數據 [ImportModel(date=20200329, author=李明, book=測試書籍一), ImportModel(date=20200330, author=珍妮, book=測試書籍二), ImportModel(date=20200331, author=李雷, book=測試書籍三), ImportModel(date=20200401, author=韓梅梅, book=測試書籍四)]
五、總結
到此爲止,EasyExcel 2.x版本的代碼示範就結束了,需要源碼的小夥伴可以訪問我的git獲取。
雖然上面已經寫過了,但是避免粗心的小夥伴看不到,再重複一下。
源碼地址:https://github.com/wangming2674/easyexcel-basic-demo
另外,我在應用時,是前後端分離架構,前端用的vue.js。
在請求時,也可能遇見一些小坑,如果有問題,可以加入技術交流qq羣:805069260。
在羣裏能直接聯繫到我,或者在我的博客留言,看到的話我都會第一時間爲你解決問題的。
如果幫到你的話,麻煩幫我點個贊或者star一下項目,謝謝。