操作Excel實(shí)現(xiàn)導(dǎo)入導(dǎo)出是個(gè)非常常見(jiàn)的需求,,之前介紹了一款非常好用的工具EasyPoi 。有讀者提出在數(shù)據(jù)量大的情況下,,EasyPoi占用內(nèi)存大,,性能不夠好。今天給大家推薦一款性能更好的Excel導(dǎo)入導(dǎo)出工具EasyExcel
,,希望對(duì)大家有所幫助,!
EasyExcel簡(jiǎn)介
EasyExcel是一款阿里開(kāi)源的Excel導(dǎo)入導(dǎo)出工具,具有處理快速,、占用內(nèi)存小,、使用方便的特點(diǎn),在Github上已有22k+
Star,,可見(jiàn)其非常流行,。
EasyExcel讀取75M(46W行25列)的Excel,僅需使用64M內(nèi)存,,耗時(shí)20s,,極速模式還可以更快!
集成
在SpringBoot中集成EasyExcel非常簡(jiǎn)單,僅需一個(gè)依賴(lài)即可,。
<!--EasyExcel相關(guān)依賴(lài)-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.0.5</version>
</dependency>
使用
EasyExcel和EasyPoi的使用非常類(lèi)似,,都是通過(guò)注解來(lái)控制導(dǎo)入導(dǎo)出。接下來(lái)我們以會(huì)員信息和訂單信息的導(dǎo)入導(dǎo)出為例,,分別實(shí)現(xiàn)下簡(jiǎn)單的單表導(dǎo)出和具有一對(duì)多關(guān)系的復(fù)雜導(dǎo)出,。
簡(jiǎn)單導(dǎo)出
我們以會(huì)員信息的導(dǎo)出為例,來(lái)體驗(yàn)下EasyExcel的導(dǎo)出功能,。
- 首先創(chuàng)建一個(gè)會(huì)員對(duì)象
Member
,,封裝會(huì)員信息,這里使用了EasyExcel的注解,;
/**
* 購(gòu)物會(huì)員
* Created by macro on 2021/10/12.
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class Member {
@ExcelProperty("ID")
@ColumnWidth(10)
private Long id;
@ExcelProperty("用戶(hù)名")
@ColumnWidth(20)
private String username;
@ExcelIgnore
private String password;
@ExcelProperty("昵稱(chēng)")
@ColumnWidth(20)
private String nickname;
@ExcelProperty("出生日期")
@ColumnWidth(20)
@DateTimeFormat("yyyy-MM-dd")
private Date birthday;
@ExcelProperty("手機(jī)號(hào)")
@ColumnWidth(20)
private String phone;
@ExcelIgnore
private String icon;
@ExcelProperty(value = "性別", converter = GenderConverter.class)
@ColumnWidth(10)
private Integer gender;
}
上面代碼使用到了EasyExcel的核心注解,,我們分別來(lái)了解下:
- @ExcelProperty:核心注解,
value
屬性可用來(lái)設(shè)置表頭名稱(chēng),,converter
屬性可以用來(lái)設(shè)置類(lèi)型轉(zhuǎn)換器,; - @ColumnWidth:用于設(shè)置表格列的寬度;
- @DateTimeFormat:用于設(shè)置日期轉(zhuǎn)換格式,。
在EasyExcel中,,如果你想實(shí)現(xiàn)枚舉類(lèi)型到字符串的轉(zhuǎn)換(比如gender屬性中,0->男
,,1->女
),,需要自定義轉(zhuǎn)換器,下面為自定義的GenderConverter
代碼實(shí)現(xiàn),;
/**
* excel性別轉(zhuǎn)換器
* Created by macro on 2021/12/29.
*/
public class GenderConverter implements Converter<Integer> {
@Override
public Class<?> supportJavaTypeKey() {
//對(duì)象屬性類(lèi)型
return Integer.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
//CellData屬性類(lèi)型
return CellDataTypeEnum.STRING;
}
@Override
public Integer convertToJavaData(ReadConverterContext<?> context) throws Exception {
//CellData轉(zhuǎn)對(duì)象屬性
String cellStr = context.getReadCellData().getStringValue();
if (StrUtil.isEmpty(cellStr)) return null;
if ("男".equals(cellStr)) {
return 0;
} else if ("女".equals(cellStr)) {
return 1;
} else {
return null;
}
}
@Override
public WriteCellData<?> convertToExcelData(WriteConverterContext<Integer> context) throws Exception {
//對(duì)象屬性轉(zhuǎn)CellData
Integer cellValue = context.getValue();
if (cellValue == null) {
return new WriteCellData<>("");
}
if (cellValue == 0) {
return new WriteCellData<>("男");
} else if (cellValue == 1) {
return new WriteCellData<>("女");
} else {
return new WriteCellData<>("");
}
}
}
- 接下來(lái)我們?cè)贑ontroller中添加一個(gè)接口,,用于導(dǎo)出會(huì)員列表到Excel,還需給響應(yīng)頭設(shè)置下載excel的屬性,,具體代碼如下,;
/**
* EasyExcel導(dǎo)入導(dǎo)出測(cè)試Controller
* Created by macro on 2021/10/12.
*/
@Controller
@Api(tags = "EasyExcelController", description = "EasyExcel導(dǎo)入導(dǎo)出測(cè)試")
@RequestMapping("/easyExcel")
public class EasyExcelController {
@SneakyThrows(IOException.class)
@ApiOperation(value = "導(dǎo)出會(huì)員列表Excel")
@RequestMapping(value = "/exportMemberList", method = RequestMethod.GET)
public void exportMemberList(HttpServletResponse response) {
setExcelRespProp(response, "會(huì)員列表");
List<Member> memberList = LocalJsonUtil.getListFromJson("json/members.json", Member.class);
EasyExcel.write(response.getOutputStream())
.head(Member.class)
.excelType(ExcelTypeEnum.XLSX)
.sheet("會(huì)員列表")
.doWrite(memberList);
}
/**
* 設(shè)置excel下載響應(yīng)頭屬性
*/
private void setExcelRespProp(HttpServletResponse response, String rawFileName) throws UnsupportedEncodingException {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode(rawFileName, "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
}
}
- 運(yùn)行項(xiàng)目,通過(guò)Swagger測(cè)試接口,,注意在Swagger中訪(fǎng)問(wèn)接口無(wú)法直接下載,,需要點(diǎn)擊返回結(jié)果中的
下載按鈕
才行,訪(fǎng)問(wèn)地址:http://localhost:8088/swagger-ui/
- 下載完成后,,查看下文件,,一個(gè)標(biāo)準(zhǔn)的Excel文件已經(jīng)被導(dǎo)出了。
簡(jiǎn)單導(dǎo)入
接下來(lái)我們以會(huì)員信息的導(dǎo)入為例,,來(lái)體驗(yàn)下EasyExcel的導(dǎo)入功能,。
- 在Controller中添加會(huì)員信息導(dǎo)入的接口,這里需要注意的是使用
@RequestPart
注解修飾文件上傳參數(shù),,否則在Swagger中就沒(méi)法顯示上傳按鈕了,;
/**
* EasyExcel導(dǎo)入導(dǎo)出測(cè)試Controller
* Created by macro on 2021/10/12.
*/
@Controller
@Api(tags = "EasyExcelController", description = "EasyExcel導(dǎo)入導(dǎo)出測(cè)試")
@RequestMapping("/easyExcel")
public class EasyExcelController {
@SneakyThrows
@ApiOperation("從Excel導(dǎo)入會(huì)員列表")
@RequestMapping(value = "/importMemberList", method = RequestMethod.POST)
@ResponseBody
public CommonResult importMemberList(@RequestPart("file") MultipartFile file) {
List<Member> memberList = EasyExcel.read(file.getInputStream())
.head(Member.class)
.sheet()
.doReadSync();
return CommonResult.success(memberList);
}
}
- 然后在Swagger中測(cè)試接口,,選擇之前導(dǎo)出的Excel文件即可,導(dǎo)入成功后會(huì)返回解析到的數(shù)據(jù),。
復(fù)雜導(dǎo)出
當(dāng)然EasyExcel也可以實(shí)現(xiàn)更加復(fù)雜的導(dǎo)出,,比如導(dǎo)出一個(gè)嵌套了商品信息的訂單列表,下面我們來(lái)實(shí)現(xiàn)下,!
使用EasyPoi實(shí)現(xiàn)
之前我們使用過(guò)EasyPoi實(shí)現(xiàn)該功能,由于EasyPoi本來(lái)就支持嵌套對(duì)象的導(dǎo)出,,直接使用內(nèi)置的@ExcelCollection
注解即可實(shí)現(xiàn),,非常方便也符合面向?qū)ο蟮乃枷搿?/p>
尋找方案
由于EasyExcel本身并不支持這種一對(duì)多的信息導(dǎo)出,所以我們得自行實(shí)現(xiàn)下,,這里分享一個(gè)我平時(shí)常用的快速查找解決方案
的辦法,。
我們可以直接從開(kāi)源項(xiàng)目的issues
里面去搜索,比如搜索下一對(duì)多
,,會(huì)直接找到有無(wú)一對(duì)多導(dǎo)出比較優(yōu)雅的方案
這個(gè)issue,。
從此issue的回復(fù)我們可以發(fā)現(xiàn),項(xiàng)目維護(hù)者建議創(chuàng)建自定義合并策略
來(lái)實(shí)現(xiàn),,有位回復(fù)的老哥已經(jīng)給出了實(shí)現(xiàn)代碼,,接下來(lái)我們就用這個(gè)方案來(lái)實(shí)現(xiàn)下。
解決思路
為什么自定義單元格合并策略能實(shí)現(xiàn)一對(duì)多的列表信息的導(dǎo)出呢,?首先我們來(lái)看下將嵌套數(shù)據(jù)平鋪,,不進(jìn)行合并導(dǎo)出的Excel。
看完之后我們很容易理解解決思路,,只要把訂單ID
相同的列中需要合并的列給合并了,,就可以實(shí)現(xiàn)這種一對(duì)多嵌套信息的導(dǎo)出了。
實(shí)現(xiàn)過(guò)程
- 首先我們得把原來(lái)嵌套的訂單商品信息給平鋪了,,創(chuàng)建一個(gè)專(zhuān)門(mén)的導(dǎo)出對(duì)象
OrderData
,,包含訂單和商品信息,二級(jí)表頭可以通過(guò)設(shè)置@ExcelProperty
的value為數(shù)組來(lái)實(shí)現(xiàn),;
/**
* 訂單導(dǎo)出
* Created by macro on 2021/12/30.
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class OrderData {
@ExcelProperty(value = "訂單ID")
@ColumnWidth(10)
@CustomMerge(needMerge = true, isPk = true)
private String id;
@ExcelProperty(value = "訂單編碼")
@ColumnWidth(20)
@CustomMerge(needMerge = true)
private String orderSn;
@ExcelProperty(value = "創(chuàng)建時(shí)間")
@ColumnWidth(20)
@DateTimeFormat("yyyy-MM-dd")
@CustomMerge(needMerge = true)
private Date createTime;
@ExcelProperty(value = "收貨地址")
@CustomMerge(needMerge = true)
@ColumnWidth(20)
private String receiverAddress;
@ExcelProperty(value = {"商品信息", "商品編碼"})
@ColumnWidth(20)
private String productSn;
@ExcelProperty(value = {"商品信息", "商品名稱(chēng)"})
@ColumnWidth(20)
private String name;
@ExcelProperty(value = {"商品信息", "商品標(biāo)題"})
@ColumnWidth(30)
private String subTitle;
@ExcelProperty(value = {"商品信息", "品牌名稱(chēng)"})
@ColumnWidth(20)
private String brandName;
@ExcelProperty(value = {"商品信息", "商品價(jià)格"})
@ColumnWidth(20)
private BigDecimal price;
@ExcelProperty(value = {"商品信息", "商品數(shù)量"})
@ColumnWidth(20)
private Integer count;
}
- 然后將原來(lái)嵌套的
Order
對(duì)象列表轉(zhuǎn)換為OrderData
對(duì)象列表,;
/**
* EasyExcel導(dǎo)入導(dǎo)出測(cè)試Controller
* Created by macro on 2021/10/12.
*/
@Controller
@Api(tags = "EasyExcelController", description = "EasyExcel導(dǎo)入導(dǎo)出測(cè)試")
@RequestMapping("/easyExcel")
public class EasyExcelController {
private List<OrderData> convert(List<Order> orderList) {
List<OrderData> result = new ArrayList<>();
for (Order order : orderList) {
List<Product> productList = order.getProductList();
for (Product product : productList) {
OrderData orderData = new OrderData();
BeanUtil.copyProperties(product,orderData);
BeanUtil.copyProperties(order,orderData);
result.add(orderData);
}
}
return result;
}
}
- 再創(chuàng)建一個(gè)自定義注解
CustomMerge
,用于標(biāo)記哪些屬性需要合并,,哪個(gè)是主鍵,;
/**
* 自定義注解,用于判斷是否需要合并以及合并的主鍵
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface CustomMerge {
/**
* 是否需要合并單元格
*/
boolean needMerge() default false;
/**
* 是否是主鍵,即該字段相同的行合并
*/
boolean isPk() default false;
}
- 再創(chuàng)建自定義單元格合并策略類(lèi)
CustomMergeStrategy
,,當(dāng)Excel中兩列主鍵相同時(shí),,合并被標(biāo)記需要合并的列;
/**
* 自定義單元格合并策略
*/
public class CustomMergeStrategy implements RowWriteHandler {
/**
* 主鍵下標(biāo)
*/
private Integer pkIndex;
/**
* 需要合并的列的下標(biāo)集合
*/
private List<Integer> needMergeColumnIndex = new ArrayList<>();
/**
* DTO數(shù)據(jù)類(lèi)型
*/
private Class<?> elementType;
public CustomMergeStrategy(Class<?> elementType) {
this.elementType = elementType;
}
@Override
public void afterRowDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Integer relativeRowIndex, Boolean isHead) {
// 如果是標(biāo)題,則直接返回
if (isHead) {
return;
}
// 獲取當(dāng)前sheet
Sheet sheet = writeSheetHolder.getSheet();
// 獲取標(biāo)題行
Row titleRow = sheet.getRow(0);
if (null == pkIndex) {
this.lazyInit(writeSheetHolder);
}
// 判斷是否需要和上一行進(jìn)行合并
// 不能和標(biāo)題合并,,只能數(shù)據(jù)行之間合并
if (row.getRowNum() <= 1) {
return;
}
// 獲取上一行數(shù)據(jù)
Row lastRow = sheet.getRow(row.getRowNum() - 1);
// 將本行和上一行是同一類(lèi)型的數(shù)據(jù)(通過(guò)主鍵字段進(jìn)行判斷),,則需要合并
if (lastRow.getCell(pkIndex).getStringCellValue().equalsIgnoreCase(row.getCell(pkIndex).getStringCellValue())) {
for (Integer needMerIndex : needMergeColumnIndex) {
CellRangeAddress cellRangeAddress = new CellRangeAddress(row.getRowNum() - 1, row.getRowNum(),
needMerIndex, needMerIndex);
sheet.addMergedRegionUnsafe(cellRangeAddress);
}
}
}
/**
* 初始化主鍵下標(biāo)和需要合并字段的下標(biāo)
*/
private void lazyInit(WriteSheetHolder writeSheetHolder) {
// 獲取當(dāng)前sheet
Sheet sheet = writeSheetHolder.getSheet();
// 獲取標(biāo)題行
Row titleRow = sheet.getRow(0);
// 獲取DTO的類(lèi)型
Class<?> eleType = this.elementType;
// 獲取DTO所有的屬性
Field[] fields = eleType.getDeclaredFields();
// 遍歷所有的字段,因?yàn)槭腔贒TO的字段來(lái)構(gòu)建excel,所以字段數(shù) >= excel的列數(shù)
for (Field theField : fields) {
// 獲取@ExcelProperty注解,,用于獲取該字段對(duì)應(yīng)在excel中的列的下標(biāo)
ExcelProperty easyExcelAnno = theField.getAnnotation(ExcelProperty.class);
// 為空,則表示該字段不需要導(dǎo)入到excel,直接處理下一個(gè)字段
if (null == easyExcelAnno) {
continue;
}
// 獲取自定義的注解,,用于合并單元格
CustomMerge customMerge = theField.getAnnotation(CustomMerge.class);
// 沒(méi)有@CustomMerge注解的默認(rèn)不合并
if (null == customMerge) {
continue;
}
for (int index = 0; index < fields.length; index++) {
Cell theCell = titleRow.getCell(index);
// 當(dāng)配置為不需要導(dǎo)出時(shí),返回的為null,,這里作一下判斷,,防止NPE
if (null == theCell) {
continue;
}
// 將字段和excel的表頭匹配上
if (easyExcelAnno.value()[0].equalsIgnoreCase(theCell.getStringCellValue())) {
if (customMerge.isPk()) {
pkIndex = index;
}
if (customMerge.needMerge()) {
needMergeColumnIndex.add(index);
}
}
}
}
// 沒(méi)有指定主鍵,則異常
if (null == this.pkIndex) {
throw new IllegalStateException("使用@CustomMerge注解必須指定主鍵");
}
}
}
- 接下來(lái)在Controller中添加導(dǎo)出訂單列表的接口,,將我們自定義的合并策略
CustomMergeStrategy
給注冊(cè)上去,;
/**
* EasyExcel導(dǎo)入導(dǎo)出測(cè)試Controller
* Created by macro on 2021/10/12.
*/
@Controller
@Api(tags = "EasyExcelController", description = "EasyExcel導(dǎo)入導(dǎo)出測(cè)試")
@RequestMapping("/easyExcel")
public class EasyExcelController {
@SneakyThrows
@ApiOperation(value = "導(dǎo)出訂單列表Excel")
@RequestMapping(value = "/exportOrderList", method = RequestMethod.GET)
public void exportOrderList(HttpServletResponse response) {
List<Order> orderList = getOrderList();
List<OrderData> orderDataList = convert(orderList);
setExcelRespProp(response, "訂單列表");
EasyExcel.write(response.getOutputStream())
.head(OrderData.class)
.registerWriteHandler(new CustomMergeStrategy(OrderData.class))
.excelType(ExcelTypeEnum.XLSX)
.sheet("訂單列表")
.doWrite(orderDataList);
}
}
- 在Swagger中訪(fǎng)問(wèn)接口測(cè)試,導(dǎo)出訂單列表對(duì)應(yīng)Excel,;
- 下載完成后,,查看下文件,由于EasyExcel需要自己來(lái)實(shí)現(xiàn),,對(duì)比之前使用EasyPoi來(lái)實(shí)現(xiàn)麻煩了不少,。
其他使用
由于EasyExcel的官方文檔介紹的比較簡(jiǎn)單,如果你想要更深入地進(jìn)行使用的話(huà),,建議大家看下官方Demo,。
總結(jié)
體驗(yàn)了一把EasyExcel,使用還是挺方便的,,性能也很優(yōu)秀,。但是比較常見(jiàn)的一對(duì)多導(dǎo)出實(shí)現(xiàn)比較復(fù)雜,而且功能也不如EasyPoi 強(qiáng)大,。如果你的Excel導(dǎo)出數(shù)據(jù)量不大的話(huà),,可以使用EasyPoi,如果數(shù)據(jù)量大,,比較在意性能的話(huà),,還是使用EasyExcel吧。
參考資料
- 項(xiàng)目地址:https://github.com/alibaba/easyexcel
- 官方文檔:https://www./easyexcel/doc/easyexcel
項(xiàng)目源碼地址
https://github.com/macrozheng/mall-learning/tree/master/mall-tiny-easyexcel
微信8.0將好友放開(kāi)到了一萬(wàn),,小伙伴可以加我大號(hào)了,,先到先得,再滿(mǎn)就真沒(méi)了
掃描下方二維碼即可加我微信啦,,2022,,抱團(tuán)取暖,一起牛逼,。
推薦閱讀