Spring Boot 集成 EasyExcel 3.x
本章節(jié)將介紹 Spring Boot 集成 EasyExcel(優(yōu)雅實現(xiàn)Excel導(dǎo)入導(dǎo)出)。
?? Spring Boot 2.x 實踐案例(代碼倉庫)
介紹
EasyExcel 是一個基于 Java 的、快速、簡潔、解決大文件內(nèi)存溢出的 Excel 處理工具。它能讓你在不用考慮性能、內(nèi)存的等因素的情況下,快速完成 Excel 的讀、寫等功能。
EasyExcel文檔地址:https://easyexcel.opensource.alibaba.com/
快速開始
引入依賴
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.1.3</version>
</dependency>
簡單導(dǎo)出
以導(dǎo)出用戶信息為例,接下來手把手教大家如何使用EasyExcel實現(xiàn)導(dǎo)出功能!
定義實體類
在EasyExcel中,以面向?qū)ο笏枷雭韺崿F(xiàn)導(dǎo)入導(dǎo)出,無論是導(dǎo)入數(shù)據(jù)還是導(dǎo)出數(shù)據(jù)都可以想象成具體某個對象的集合,所以為了實現(xiàn)導(dǎo)出用戶信息功能,首先創(chuàng)建一個用戶對象UserDO
實體類,用于封裝用戶信息:
/**
* 用戶信息
*
* @author william@StarImmortal
*/
@Data
public class UserDO {
@ExcelProperty("用戶編號")
@ColumnWidth(20)
private Long id;
@ExcelProperty("用戶名")
@ColumnWidth(20)
private String username;
@ExcelIgnore
private String password;
@ExcelProperty("昵稱")
@ColumnWidth(20)
private String nickname;
@ExcelProperty("生日")
@ColumnWidth(20)
@DateTimeFormat("yyyy-MM-dd")
private Date birthday;
@ExcelProperty("手機(jī)號")
@ColumnWidth(20)
private String phone;
@ExcelProperty("身高(米)")
@NumberFormat("#.##")
@ColumnWidth(20)
private Double height;
@ExcelProperty(value = "性別", converter = GenderConverter.class)
@ColumnWidth(10)
private Integer gender;
}
上面代碼中類屬性上使用了EasyExcel核心注解:
- @ExcelProperty:核心注解,
value
屬性可用來設(shè)置表頭名稱,converter
屬性可以用來設(shè)置類型轉(zhuǎn)換器; - @ColumnWidth:用于設(shè)置表格列的寬度;
- @DateTimeFormat:用于設(shè)置日期轉(zhuǎn)換格式;
- @NumberFormat:用于設(shè)置數(shù)字轉(zhuǎn)換格式。
自定義轉(zhuǎn)換器
在EasyExcel中,如果想實現(xiàn)枚舉類型到字符串類型轉(zhuǎn)換(例如gender
屬性:1 -> 男
,2 -> 女
),需實現(xiàn)Converter
接口來自定義轉(zhuǎn)換器,下面為自定義GenderConverter
性別轉(zhuǎn)換器代碼實現(xiàn):
/**
* Excel 性別轉(zhuǎn)換器
*
* @author william@StarImmortal
*/
public class GenderConverter implements Converter<Integer> {
@Override
public Class<?> supportJavaTypeKey() {
return Integer.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
@Override
public Integer convertToJavaData(ReadConverterContext<?> context) {
return GenderEnum.convert(context.getReadCellData().getStringValue()).getValue();
}
@Override
public WriteCellData<?> convertToExcelData(WriteConverterContext<Integer> context) {
return new WriteCellData<>(GenderEnum.convert(context.getValue()).getDescription());
}
}
/**
* 性別枚舉
*
* @author william@StarImmortal
*/
@Getter
@AllArgsConstructor
public enum GenderEnum {
/**
* 未知
*/
UNKNOWN(0, "未知"),
/**
* 男性
*/
MALE(1, "男性"),
/**
* 女性
*/
FEMALE(2, "女性");
private final Integer value;
@JsonFormat
private final String description;
public static GenderEnum convert(Integer value) {
return Stream.of(values())
.filter(bean -> bean.value.equals(value))
.findAny()
.orElse(UNKNOWN);
}
public static GenderEnum convert(String description) {
return Stream.of(values())
.filter(bean -> bean.description.equals(description))
.findAny()
.orElse(UNKNOWN);
}
}
定義接口
/**
* EasyExcel導(dǎo)入導(dǎo)出
*
* @author william@StarImmortal
*/
@RestController
@RequestMapping("/excel")
public class ExcelController {
@GetMapping("/export/user")
public void exportUserExcel(HttpServletResponse response) {
try {
this.setExcelResponseProp(response, "用戶列表");
List<UserDO> userList = this.getUserList();
EasyExcel.write(response.getOutputStream())
.head(UserDO.class)
.excelType(ExcelTypeEnum.XLSX)
.sheet("用戶列表")
.doWrite(userList);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 設(shè)置響應(yīng)結(jié)果
*
* @param response 響應(yīng)結(jié)果對象
* @param rawFileName 文件名
* @throws UnsupportedEncodingException 不支持編碼異常
*/
private void setExcelResponseProp(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");
}
/**
* 讀取用戶列表數(shù)據(jù)
*
* @return 用戶列表數(shù)據(jù)
* @throws IOException IO異常
*/
private List<UserDO> getUserList() throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
ClassPathResource classPathResource = new ClassPathResource("mock/users.json");
InputStream inputStream = classPathResource.getInputStream();
return objectMapper.readValue(inputStream, new TypeReference<List<UserDO>>() {
});
}
}
測試接口
運行項目,通過 Postman
或者 Apifox
工具來進(jìn)行接口測試
注意:在 Apifox
中訪問接口后無法直接下載,需要點擊返回結(jié)果中的下載圖標(biāo)
才行,點擊之后方可對Excel文件進(jìn)行保存。
接口地址:http://localhost:8080/excel/export/user
復(fù)雜導(dǎo)出
由于 EasyPoi 支持嵌套對象導(dǎo)出,直接使用內(nèi)置 @ExcelCollection
注解即可實現(xiàn),遺憾的是 EasyExcel 不支持一對多導(dǎo)出,只能自行實現(xiàn),通過此issues了解到,項目維護(hù)者建議通過自定義合并策略方式來實現(xiàn)一對多導(dǎo)出。
解決思路:只需把訂單主鍵
相同的列中需要合并的列給合并了,就可以實現(xiàn)這種一對多嵌套信息的導(dǎo)出
自定義注解
創(chuàng)建一個自定義注解,用于標(biāo)記哪些屬性需要合并單元格,哪個屬性是主鍵:
/**
* 用于判斷是否需要合并以及合并的主鍵
*
* @author william@StarImmortal
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExcelMerge {
/**
* 是否合并單元格
*
* @return true || false
*/
boolean merge() default true;
/**
* 是否為主鍵(即該字段相同的行合并)
*
* @return true || false
*/
boolean isPrimaryKey() default false;
}
定義實體類
在需要合并單元格的屬性上設(shè)置 @ExcelMerge
注解,二級表頭通過設(shè)置 @ExcelProperty
注解中 value 值為數(shù)組形式來實現(xiàn)該效果:
/**
* @author william@StarImmortal
*/
@Data
public class OrderBO {
@ExcelProperty(value = "訂單主鍵")
@ColumnWidth(16)
@ExcelMerge(merge = true, isPrimaryKey = true)
private String id;
@ExcelProperty(value = "訂單編號")
@ColumnWidth(20)
@ExcelMerge(merge = true)
private String orderId;
@ExcelProperty(value = "收貨地址")
@ExcelMerge(merge = true)
@ColumnWidth(20)
private String address;
@ExcelProperty(value = "創(chuàng)建時間")
@ColumnWidth(20)
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
@ExcelMerge(merge = true)
private Date createTime;
@ExcelProperty(value = {"商品信息", "商品編號"})
@ColumnWidth(20)
private String productId;
@ExcelProperty(value = {"商品信息", "商品名稱"})
@ColumnWidth(20)
private String name;
@ExcelProperty(value = {"商品信息", "商品標(biāo)題"})
@ColumnWidth(30)
private String subtitle;
@ExcelProperty(value = {"商品信息", "品牌名稱"})
@ColumnWidth(20)
private String brandName;
@ExcelProperty(value = {"商品信息", "商品價格"})
@ColumnWidth(20)
private BigDecimal price;
@ExcelProperty(value = {"商品信息", "商品數(shù)量"})
@ColumnWidth(20)
private Integer count;
}
數(shù)據(jù)映射與平鋪
導(dǎo)出之前,需要對數(shù)據(jù)進(jìn)行處理,將訂單數(shù)據(jù)進(jìn)行平鋪,orderList
為平鋪前格式,exportData
為平鋪后格式:
自定義單元格合并策略
當(dāng) Excel 中兩列主鍵相同時,合并被標(biāo)記需要合并的列:
/**
* 自定義單元格合并策略
*
* @author william@StarImmortal
*/
public class ExcelMergeStrategy implements RowWriteHandler {
/**
* 主鍵下標(biāo)
*/
private Integer primaryKeyIndex;
/**
* 需要合并的列的下標(biāo)集合
*/
private final List<Integer> mergeColumnIndexList = new ArrayList<>();
/**
* 數(shù)據(jù)類型
*/
private final Class<?> elementType;
public ExcelMergeStrategy(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 = writeSheetHolder.getSheet();
// 初始化主鍵下標(biāo)和需要合并字段的下標(biāo)
if (primaryKeyIndex == null) {
this.initPrimaryIndexAndMergeIndex(writeSheetHolder);
}
// 判斷是否需要和上一行進(jìn)行合并
// 不能和標(biāo)題合并,只能數(shù)據(jù)行之間合并
if (row.getRowNum() <= 1) {
return;
}
// 獲取上一行數(shù)據(jù)
Row lastRow = sheet.getRow(row.getRowNum() - 1);
// 將本行和上一行是同一類型的數(shù)據(jù)(通過主鍵字段進(jìn)行判斷),則需要合并
if (lastRow.getCell(primaryKeyIndex).getStringCellValue().equalsIgnoreCase(row.getCell(primaryKeyIndex).getStringCellValue())) {
for (Integer mergeIndex : mergeColumnIndexList) {
CellRangeAddress cellRangeAddress = new CellRangeAddress(row.getRowNum() - 1, row.getRowNum(), mergeIndex, mergeIndex);
sheet.addMergedRegionUnsafe(cellRangeAddress);
}
}
}
/**
* 初始化主鍵下標(biāo)和需要合并字段的下標(biāo)
*
* @param writeSheetHolder WriteSheetHolder
*/
private void initPrimaryIndexAndMergeIndex(WriteSheetHolder writeSheetHolder) {
// 獲取當(dāng)前工作表
Sheet sheet = writeSheetHolder.getSheet();
// 獲取標(biāo)題行
Row titleRow = sheet.getRow(0);
// 獲取所有屬性字段
Field[] fields = this.elementType.getDeclaredFields();
// 遍歷所有字段
for (Field field : fields) {
// 獲取@ExcelProperty注解,用于獲取該字段對應(yīng)列的下標(biāo)
ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
// 判斷是否為空
if (null == excelProperty) {
continue;
}
// 獲取自定義注解,用于合并單元格
ExcelMerge excelMerge = field.getAnnotation(ExcelMerge.class);
// 判斷是否需要合并
if (null == excelMerge) {
continue;
}
for (int i = 0; i < fields.length; i++) {
Cell cell = titleRow.getCell(i);
if (null == cell) {
continue;
}
// 將字段和表頭匹配上
if (excelProperty.value()[0].equalsIgnoreCase(cell.getStringCellValue())) {
if (excelMerge.isPrimaryKey()) {
primaryKeyIndex = i;
}
if (excelMerge.merge()) {
mergeColumnIndexList.add(i);
}
}
}
}
// 沒有指定主鍵,則異常
if (null == this.primaryKeyIndex) {
throw new IllegalStateException("使用@ExcelMerge注解必須指定主鍵");
}
}
}
定義接口
將自定義合并策略 ExcelMergeStrategy
通過 registerWriteHandler
注冊上去:
/**
* EasyExcel導(dǎo)入導(dǎo)出
*
* @author william@StarImmortal
*/
@RestController
@RequestMapping("/excel")
public class ExcelController {
@GetMapping("/export/order")
public void exportOrderExcel(HttpServletResponse response) {
try {
this.setExcelResponseProp(response, "訂單列表");
List<OrderDO> orderList = this.getOrderList();
List<OrderBO> exportData = this.convert(orderList);
EasyExcel.write(response.getOutputStream())
.head(OrderBO.class)
.registerWriteHandler(new ExcelMergeStrategy(OrderBO.class))
.excelType(ExcelTypeEnum.XLSX)
.sheet("訂單列表")
.doWrite(exportData);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 設(shè)置響應(yīng)結(jié)果
*
* @param response 響應(yīng)結(jié)果對象
* @param rawFileName 文件名
* @throws UnsupportedEncodingException 不支持編碼異常
*/
private void setExcelResponseProp(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");
}
}
測試接口
運行項目,通過 Postman
或者 Apifox
工具來進(jìn)行接口測試
注意:在 Apifox
中訪問接口后無法直接下載,需要點擊返回結(jié)果中的下載圖標(biāo)
才行,點擊之后方可對Excel文件進(jìn)行保存。
接口地址:http://localhost:8080/excel/export/order
簡單導(dǎo)入
以導(dǎo)入用戶信息為例,接下來手把手教大家如何使用EasyExcel實現(xiàn)導(dǎo)入功能!文章來源:http://www.zghlxwxcb.cn/news/detail-780210.html
/**
* EasyExcel導(dǎo)入導(dǎo)出
*
* @author william@StarImmortal
*/
@RestController
@RequestMapping("/excel")
@Api(tags = "EasyExcel")
public class ExcelController {
@PostMapping("/import/user")
public ResponseVO importUserExcel(@RequestPart(value = "file") MultipartFile file) {
try {
List<UserDO> userList = EasyExcel.read(file.getInputStream())
.head(UserDO.class)
.sheet()
.doReadSync();
return ResponseVO.success(userList);
} catch (IOException e) {
return ResponseVO.error();
}
}
}
文章來源地址http://www.zghlxwxcb.cn/news/detail-780210.html
參考資料
- 項目地址:https://github.com/alibaba/easyexcel
- 官方文檔:https://www.yuque.com/easyexcel/doc/easyexcel
- 一對多導(dǎo)出優(yōu)雅方案:https://github.com/alibaba/easyexcel/issues/1780
到了這里,關(guān)于Spring Boot 集成 EasyExcel 3.x 優(yōu)雅實現(xiàn)Excel導(dǎo)入導(dǎo)出的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!