以导出用户信息为例,接下来手把手教大家如何使用EasyExcel实现导出功能!
在EasyExcel中,以面向对象思想来实现导入导出,无论是导入数据还是导出数据都可以想象成具体某个对象的集合,所以为了实现导出用户信息功能,首先创建一个用户对象UserDO实体类,用于封装用户信息:
/** * 用户信息 * * @author william@StarImmortal */@DatapublicclassUserDO {
@ExcelProperty("用户编号")
@ColumnWidth(20)
privateLongid;
@ExcelProperty("用户名")
@ColumnWidth(20)
privateStringusername;
@ExcelIgnoreprivateStringpassword;
@ExcelProperty("昵称")
@ColumnWidth(20)
privateStringnickname;
@ExcelProperty("生日")
@ColumnWidth(20)
@DateTimeFormat("yyyy-MM-dd")
privateDatebirthday;
@ExcelProperty("手机号")
@ColumnWidth(20)
privateStringphone;
@ExcelProperty("身高(米)")
@NumberFormat("#.##")
@ColumnWidth(20)
privateDoubleheight;
@ExcelProperty(value = "性别", converter = GenderConverter.class)
@ColumnWidth(10)
privateIntegergender;
}
上面代码中类属性上使用了EasyExcel核心注解:
@ExcelProperty:核心注解,value属性可用来设置表头名称,converter属性可以用来设置类型转换器;
@ColumnWidth:用于设置表格列的宽度;
@DateTimeFormat:用于设置日期转换格式;
@NumberFormat:用于设置数字转换格式。
在EasyExcel中,如果想实现枚举类型到字符串类型转换(例如gender属性:1 -> 男,2 -> 女),需实现Converter接口来自定义转换器,下面为自定义GenderConverter性别转换器代码实现:
/** * Excel 性别转换器 * * @author william@StarImmortal */publicclassGenderConverterimplementsConverter
@OverridepublicClass> supportJavaTypeKey() {
returnInteger.class;
}
@OverridepublicCellDataTypeEnumsupportExcelTypeKey() {
returnCellDataTypeEnum.STRING;
}
@OverridepublicIntegerconvertToJavaData(ReadConverterContext> context) {
returnGenderEnum.convert(context.getReadCellData().getStringValue()).getValue();
}
@OverridepublicWriteCellData> convertToExcelData(WriteConverterContext
returnnewWriteCellData<>(GenderEnum.convert(context.getValue()).getDescription());
}
}
/** * 性别枚举 * * @author william@StarImmortal */@Getter@AllArgsConstructorpublicenumGenderEnum {
/** * 未知 */UNKNOWN(0, "未知"),
/** * 男性 */MALE(1, "男性"),
/** * 女性 */FEMALE(2, "女性");
privatefinalIntegervalue;
@JsonFormatprivatefinalStringdescription;
publicstaticGenderEnumconvert(Integervalue) {
returnStream.of(values())
.filter(bean -> bean.value.equals(value))
.findAny()
.orElse(UNKNOWN);
}
publicstaticGenderEnumconvert(Stringdescription) {
returnStream.of(values())
.filter(bean -> bean.description.equals(description))
.findAny()
.orElse(UNKNOWN);
}
}
/** * EasyExcel导入导出 * * @author william@StarImmortal */@RestController@RequestMapping("/excel")
publicclassExcelController {
@GetMapping("/export/user")
publicvoidexportUserExcel(HttpServletResponseresponse) {
try {
this.setExcelResponseProp(response, "用户列表");
List
EasyExcel.write(response.getOutputStream())
.head(UserDO.class)
.excelType(ExcelTypeEnum.XLSX)
.sheet("用户列表")
.doWrite(userList);
} catch (IOExceptione) {
thrownewRuntimeException(e);
}
}
/** * 设置响应结果 * * @param response 响应结果对象 * @param rawFileName 文件名 * @throws UnsupportedEncodingException 不支持编码异常 */privatevoidsetExcelResponseProp(HttpServletResponseresponse, StringrawFileName) throwsUnsupportedEncodingException {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
StringfileName = URLEncoder.encode(rawFileName, "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
}
/** * 读取用户列表数据 * * @return 用户列表数据 * @throws IOException IO异常 */privateList
ObjectMapperobjectMapper = newObjectMapper();
ClassPathResourceclassPathResource = newClassPathResource("mock/users.json");
InputStreaminputStream = classPathResource.getInputStream();
returnobjectMapper.readValue(inputStream, newTypeReference>() {
});
}
}
运行项目,通过 Postman 或者 Apifox 工具来进行接口测试
注意:在 Apifox 中访问接口后无法直接下载,需要点击返回结果中的下载图标才行,点击之后方可对Excel文件进行保存。
接口地址:http://localhost:8080/excel/export/user
由于 EasyPoi 支持嵌套对象导出,直接使用内置 @ExcelCollection 注解即可实现,遗憾的是 EasyExcel 不支持一对多导出,只能自行实现,通过此issues了解到,项目维护者建议通过自定义合并策略方式来实现一对多导出。
解决思路:只需把订单主键相同的列中需要合并的列给合并了,就可以实现这种一对多嵌套信息的导出
创建一个自定义注解,用于标记哪些属性需要合并单元格,哪个属性是主键:
/** * 用于判断是否需要合并以及合并的主键 * * @author william@StarImmortal */@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documentedpublic @interface ExcelMerge {
/** * 是否合并单元格 * * @return true || false */booleanmerge() defaulttrue;
/** * 是否为主键(即该字段相同的行合并) * * @return true || false */booleanisPrimaryKey() defaultfalse;
}
在需要合并单元格的属性上设置 @ExcelMerge 注解,二级表头通过设置 @ExcelProperty 注解中 value 值为数组形式来实现该效果:
/** * @author william@StarImmortal */@DatapublicclassOrderBO {
@ExcelProperty(value = "订单主键")
@ColumnWidth(16)
@ExcelMerge(merge = true, isPrimaryKey = true)
privateStringid;
@ExcelProperty(value = "订单编号")
@ColumnWidth(20)
@ExcelMerge(merge = true)
privateStringorderId;
@ExcelProperty(value = "收货地址")
@ExcelMerge(merge = true)
@ColumnWidth(20)
privateStringaddress;
@ExcelProperty(value = "创建时间")
@ColumnWidth(20)
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
@ExcelMerge(merge = true)
privateDatecreateTime;
@ExcelProperty(value = {"商品信息", "商品编号"})
@ColumnWidth(20)
privateStringproductId;
@ExcelProperty(value = {"商品信息", "商品名称"})
@ColumnWidth(20)
privateStringname;
@ExcelProperty(value = {"商品信息", "商品标题"})
@ColumnWidth(30)
privateStringsubtitle;
@ExcelProperty(value = {"商品信息", "品牌名称"})
@ColumnWidth(20)
privateStringbrandName;
@ExcelProperty(value = {"商品信息", "商品价格"})
@ColumnWidth(20)
privateBigDecimalprice;
@ExcelProperty(value = {"商品信息", "商品数量"})
@ColumnWidth(20)
privateIntegercount;
}
导出之前,需要对数据进行处理,将订单数据进行平铺,orderList为平铺前格式,exportData为平铺后格式:
当 Excel 中两列主键相同时,合并被标记需要合并的列:
/** * 自定义单元格合并策略 * * @author william@StarImmortal */publicclassExcelMergeStrategyimplementsRowWriteHandler {
/** * 主键下标 */privateIntegerprimaryKeyIndex;
/** * 需要合并的列的下标集合 */privatefinalList
/** * 数据类型 */privatefinalClass> elementType;
publicExcelMergeStrategy(Class> elementType) {
this.elementType = elementType;
}
@OverridepublicvoidafterRowDispose(WriteSheetHolderwriteSheetHolder, WriteTableHolderwriteTableHolder, Rowrow, IntegerrelativeRowIndex, BooleanisHead) {
// 判断是否为标题if (isHead) {
return;
}
// 获取当前工作表Sheetsheet = writeSheetHolder.getSheet();
// 初始化主键下标和需要合并字段的下标if (primaryKeyIndex == null) {
this.initPrimaryIndexAndMergeIndex(writeSheetHolder);
}
// 判断是否需要和上一行进行合并// 不能和标题合并,只能数据行之间合并if (row.getRowNum() <= 1) {
return;
}
// 获取上一行数据RowlastRow = sheet.getRow(row.getRowNum() - 1);
// 将本行和上一行是同一类型的数据(通过主键字段进行判断),则需要合并if (lastRow.getCell(primaryKeyIndex).getStringCellValue().equalsIgnoreCase(row.getCell(primaryKeyIndex).getStringCellValue())) {
for (IntegermergeIndex : mergeColumnIndexList) {
CellRangeAddresscellRangeAddress = newCellRangeAddress(row.getRowNum() - 1, row.getRowNum(), mergeIndex, mergeIndex);
sheet.addMergedRegionUnsafe(cellRangeAddress);
}
}
}
/** * 初始化主键下标和需要合并字段的下标 * * @param writeSheetHolder WriteSheetHolder */privatevoidinitPrimaryIndexAndMergeIndex(WriteSheetHolderwriteSheetHolder) {
// 获取当前工作表Sheetsheet = writeSheetHolder.getSheet();
// 获取标题行RowtitleRow = sheet.getRow(0);
// 获取所有属性字段Field[] fields = this.elementType.getDeclaredFields();
// 遍历所有字段for (Fieldfield : fields) {
// 获取@ExcelProperty注解,用于获取该字段对应列的下标ExcelPropertyexcelProperty = field.getAnnotation(ExcelProperty.class);
// 判断是否为空if (null == excelProperty) {
continue;
}
// 获取自定义注解,用于合并单元格ExcelMergeexcelMerge = field.getAnnotation(ExcelMerge.class);
// 判断是否需要合并if (null == excelMerge) {
continue;
}
for (inti = 0; i < fields.length; i++) {
Cellcell = 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) {
thrownewIllegalStateException("使用@ExcelMerge注解必须指定主键");
}
}
}
将自定义合并策略 ExcelMergeStrategy 通过 registerWriteHandler 注册上去:
/** * EasyExcel导入导出 * * @author william@StarImmortal */@RestController@RequestMapping("/excel")
publicclassExcelController {
@GetMapping("/export/order")
publicvoidexportOrderExcel(HttpServletResponseresponse) {
try {
this.setExcelResponseProp(response, "订单列表");
List
List
EasyExcel.write(response.getOutputStream())
.head(OrderBO.class)
.registerWriteHandler(newExcelMergeStrategy(OrderBO.class))
.excelType(ExcelTypeEnum.XLSX)
.sheet("订单列表")
.doWrite(exportData);
} catch (IOExceptione) {
thrownewRuntimeException(e);
}
}
/** * 设置响应结果 * * @param response 响应结果对象 * @param rawFileName 文件名 * @throws UnsupportedEncodingException 不支持编码异常 */privatevoidsetExcelResponseProp(HttpServletResponseresponse, StringrawFileName) throwsUnsupportedEncodingException {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
StringfileName = URLEncoder.encode(rawFileName, "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
}
}
运行项目,通过 Postman 或者 Apifox 工具来进行接口测试
注意:在 Apifox 中访问接口后无法直接下载,需要点击返回结果中的下载图标才行,点击之后方可对Excel文件进行保存。
接口地址:http://localhost:8080/excel/export/order
以导入用户信息为例,接下来手把手教大家如何使用EasyExcel实现导入功能!
/** * EasyExcel导入导出 * * @author william@StarImmortal */@RestController@RequestMapping("/excel")
@Api(tags = "EasyExcel")
publicclassExcelController {
@PostMapping("/import/user")
publicResponseVOimportUserExcel(@RequestPart(value = "file") MultipartFilefile) {
try {
List
.head(UserDO.class)
.sheet()
.doReadSync();
returnResponseVO.success(userList);
} catch (IOExceptione) {
returnResponseVO.error();
}
}
}