当前位置:   article > 正文

SpringBoot集成EasyExcel 3.x:高效实现Excel数据的优雅导入与导出_springboot导入excel

springboot导入excel

目录

介绍

快速开始

引入依赖

简单导出

定义实体类

自定义转换器

定义接口

测试接口

复杂导出

自定义注解

定义实体类

数据映射与平铺

自定义单元格合并策略

定义接口

测试接口

一对多导出

自定义单元格合并策略

测试数据

简单导入

定义接口

测试接口 

参考资料


 应对多人同时导出Excel导致的服务器崩溃入口:构建高效排队导出

介绍

EasyExcel 是一个基于 Java 的、快速、简洁、解决大文件内存溢出的 Excel 处理工具。它能让你在不用考虑性能、内存的等因素的情况下,快速完成 Excel 的读、写等功能。

EasyExcel文档地址:https://easyexcel.opensource.alibaba.com/

快速开始

引入依赖

  1. <dependency>
  2. <groupId>com.alibaba</groupId>
  3. <artifactId>easyexcel</artifactId>
  4. <version>3.1.3</version>
  5. </dependency>

简单导出

以导出用户信息为例,接下来手把手教大家如何使用EasyExcel实现导出功能!

定义实体类

在EasyExcel中,以面向对象思想来实现导入导出,无论是导入数据还是导出数据都可以想象成具体某个对象的集合,所以为了实现导出用户信息功能,首先创建一个用户对象UserDO实体类,用于封装用户信息:

  1. /**
  2. * 用户信息
  3. *
  4. * @author yun
  5. */
  6. @Data
  7. public class UserDO {
  8. @ExcelProperty("用户编号")
  9. @ColumnWidth(20)
  10. private Long id;
  11. @ExcelProperty("用户名")
  12. @ColumnWidth(20)
  13. private String username;
  14. @ExcelIgnore
  15. private String password;
  16. @ExcelProperty("昵称")
  17. @ColumnWidth(20)
  18. private String nickname;
  19. @ExcelProperty("生日")
  20. @ColumnWidth(20)
  21. @DateTimeFormat("yyyy-MM-dd")
  22. private Date birthday;
  23. @ExcelProperty("手机号")
  24. @ColumnWidth(20)
  25. private String phone;
  26. @ExcelProperty("身高(米)")
  27. @NumberFormat("#.##")
  28. @ColumnWidth(20)
  29. private Double height;
  30. @ExcelProperty(value = "性别", converter = GenderConverter.class)
  31. @ColumnWidth(10)
  32. private Integer gender;
  33. }

上面代码中类属性上使用了EasyExcel核心注解:

  • @ExcelProperty:核心注解,value属性可用来设置表头名称,converter属性可以用来设置类型转换器

  • @ColumnWidth:用于设置表格列的宽度;

  • @DateTimeFormat:用于设置日期转换格式;

  • @NumberFormat:用于设置数字转换格式。

自定义转换器

在EasyExcel中,如果想实现枚举类型到字符串类型转换(例如gender属性:1 -> 男,2 -> 女),需实现Converter接口来自定义转换器,下面为自定义GenderConverter性别转换器代码实现:

  1. /**
  2. * Excel 性别转换器
  3. *
  4. * @author yun
  5. */
  6. public class GenderConverter implements Converter<Integer> {
  7. @Override
  8. public Class<?> supportJavaTypeKey() {
  9. return Integer.class;
  10. }
  11. @Override
  12. public CellDataTypeEnum supportExcelTypeKey() {
  13. return CellDataTypeEnum.STRING;
  14. }
  15. @Override
  16. public Integer convertToJavaData(ReadConverterContext<?> context) {
  17. return GenderEnum.convert(context.getReadCellData().getStringValue()).getValue();
  18. }
  19. @Override
  20. public WriteCellData<?> convertToExcelData(WriteConverterContext<Integer> context) {
  21. return new WriteCellData<>(GenderEnum.convert(context.getValue()).getDescription());
  22. }
  23. }
  1. /**
  2. * 性别枚举
  3. *
  4. * @author yun
  5. */
  6. @Getter
  7. @AllArgsConstructor
  8. public enum GenderEnum {
  9. /**
  10. * 未知
  11. */
  12. UNKNOWN(0, "未知"),
  13. /**
  14. * 男性
  15. */
  16. MALE(1, "男性"),
  17. /**
  18. * 女性
  19. */
  20. FEMALE(2, "女性");
  21. private final Integer value;
  22. @JsonFormat
  23. private final String description;
  24. public static GenderEnum convert(Integer value) {
  25. return Stream.of(values())
  26. .filter(bean -> bean.value.equals(value))
  27. .findAny()
  28. .orElse(UNKNOWN);
  29. }
  30. public static GenderEnum convert(String description) {
  31. return Stream.of(values())
  32. .filter(bean -> bean.description.equals(description))
  33. .findAny()
  34. .orElse(UNKNOWN);
  35. }
  36. }
定义接口
  1. /**
  2. * EasyExcel导入导出
  3. *
  4. * @author yun
  5. */
  6. @RestController
  7. @RequestMapping("/excel")
  8. public class ExcelController {
  9. @GetMapping("/export/user")
  10. public void exportUserExcel(HttpServletResponse response) {
  11. try {
  12. this.setExcelResponseProp(response, "用户列表");
  13. List<UserDO> userList = this.getUserList();
  14. EasyExcel.write(response.getOutputStream())
  15. .head(UserDO.class)
  16. .excelType(ExcelTypeEnum.XLSX)
  17. .sheet("用户列表")
  18. .doWrite(userList);
  19. } catch (IOException e) {
  20. throw new RuntimeException(e);
  21. }
  22. }
  23. /**
  24. * 设置响应结果
  25. *
  26. * @param response 响应结果对象
  27. * @param rawFileName 文件名
  28. * @throws UnsupportedEncodingException 不支持编码异常
  29. */
  30. private void setExcelResponseProp(HttpServletResponse response, String rawFileName) throws UnsupportedEncodingException {
  31. response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
  32. response.setCharacterEncoding("utf-8");
  33. String fileName = URLEncoder.encode(rawFileName, "UTF-8").replaceAll("\+", "%20");
  34. response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
  35. }
  36. /**
  37. * 读取用户列表数据
  38. *
  39. * @return 用户列表数据
  40. * @throws IOException IO异常
  41. */
  42. private List<UserDO> getUserList() throws IOException {
  43. ObjectMapper objectMapper = new ObjectMapper();
  44. ClassPathResource classPathResource = new ClassPathResource("mock/users.json");
  45. InputStream inputStream = classPathResource.getInputStream();
  46. return objectMapper.readValue(inputStream, new TypeReference<List<UserDO>>() {
  47. });
  48. }
  49. }
测试接口

运行项目,通过 Postman 或者 Apifox 工具来进行接口测试

注意:在 Apifox 中访问接口后无法直接下载,需要点击返回结果中的下载图标才行,点击之后方可对Excel文件进行保存。

接口地址:http://localhost:8080/excel/export/user

复杂导出

由于 EasyPoi 支持嵌套对象导出,直接使用内置 @ExcelCollection 注解即可实现,遗憾的是 EasyExcel 不支持一对多导出,只能自行实现,通过此issues了解到,项目维护者建议通过自定义合并策略方式来实现一对多导出。

解决思路:只需把订单主键相同的列中需要合并的列给合并了,就可以实现这种一对多嵌套信息的导出

自定义注解

创建一个自定义注解,用于标记哪些属性需要合并单元格,哪个属性是主键:

  1. /**
  2. * 用于判断是否需要合并以及合并的主键
  3. *
  4. * @author yun
  5. */
  6. @Target({ElementType.FIELD})
  7. @Retention(RetentionPolicy.RUNTIME)
  8. @Documented
  9. public @interface ExcelMerge {
  10. /**
  11. * 是否合并单元格
  12. *
  13. * @return true || false
  14. */
  15. boolean merge() default true;
  16. /**
  17. * 是否为主键(即该字段相同的行合并)
  18. *
  19. * @return true || false
  20. */
  21. boolean isPrimaryKey() default false;
  22. }
定义实体类

在需要合并单元格的属性上设置 @ExcelMerge 注解,二级表头通过设置 @ExcelProperty 注解中 value 值为数组形式来实现该效果:

  1. /**
  2. * @author
  3. */
  4. @Data
  5. public class OrderBO {
  6. @ExcelProperty(value = "订单主键")
  7. @ColumnWidth(16)
  8. @ExcelMerge(merge = true, isPrimaryKey = true)
  9. private String id;
  10. @ExcelProperty(value = "订单编号")
  11. @ColumnWidth(20)
  12. @ExcelMerge(merge = true)
  13. private String orderId;
  14. @ExcelProperty(value = "收货地址")
  15. @ExcelMerge(merge = true)
  16. @ColumnWidth(20)
  17. private String address;
  18. @ExcelProperty(value = "创建时间")
  19. @ColumnWidth(20)
  20. @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
  21. @ExcelMerge(merge = true)
  22. private Date createTime;
  23. @ExcelProperty(value = {"商品信息", "商品编号"})
  24. @ColumnWidth(20)
  25. private String productId;
  26. @ExcelProperty(value = {"商品信息", "商品名称"})
  27. @ColumnWidth(20)
  28. private String name;
  29. @ExcelProperty(value = {"商品信息", "商品标题"})
  30. @ColumnWidth(30)
  31. private String subtitle;
  32. @ExcelProperty(value = {"商品信息", "品牌名称"})
  33. @ColumnWidth(20)
  34. private String brandName;
  35. @ExcelProperty(value = {"商品信息", "商品价格"})
  36. @ColumnWidth(20)
  37. private BigDecimal price;
  38. @ExcelProperty(value = {"商品信息", "商品数量"})
  39. @ColumnWidth(20)
  40. private Integer count;
  41. }
数据映射与平铺

导出之前,需要对数据进行处理,将订单数据进行平铺,orderList为平铺前格式,exportData为平铺后格式:

自定义单元格合并策略

当 Excel 中两列主键相同时,合并被标记需要合并的列:

  1. /**
  2. * 自定义单元格合并策略
  3. *
  4. * @author yun
  5. */
  6. public class ExcelMergeStrategy implements RowWriteHandler {
  7. /**
  8. * 主键下标
  9. */
  10. private Integer primaryKeyIndex;
  11. /**
  12. * 需要合并的列的下标集合
  13. */
  14. private final List<Integer> mergeColumnIndexList = new ArrayList<>();
  15. /**
  16. * 数据类型
  17. */
  18. private final Class<?> elementType;
  19. public ExcelMergeStrategy(Class<?> elementType) {
  20. this.elementType = elementType;
  21. }
  22. @Override
  23. public void afterRowDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Integer relativeRowIndex, Boolean isHead) {
  24. // 判断是否为标题
  25. if (isHead) {
  26. return;
  27. }
  28. // 获取当前工作表
  29. Sheet sheet = writeSheetHolder.getSheet();
  30. // 初始化主键下标和需要合并字段的下标
  31. if (primaryKeyIndex == null) {
  32. this.initPrimaryIndexAndMergeIndex(writeSheetHolder);
  33. }
  34. // 判断是否需要和上一行进行合并
  35. // 不能和标题合并,只能数据行之间合并
  36. if (row.getRowNum() <= 1) {
  37. return;
  38. }
  39. // 获取上一行数据
  40. Row lastRow = sheet.getRow(row.getRowNum() - 1);
  41. // 将本行和上一行是同一类型的数据(通过主键字段进行判断),则需要合并
  42. if (lastRow.getCell(primaryKeyIndex).getStringCellValue().equalsIgnoreCase(row.getCell(primaryKeyIndex).getStringCellValue())) {
  43. for (Integer mergeIndex : mergeColumnIndexList) {
  44. CellRangeAddress cellRangeAddress = new CellRangeAddress(row.getRowNum() - 1, row.getRowNum(), mergeIndex, mergeIndex);
  45. sheet.addMergedRegionUnsafe(cellRangeAddress);
  46. }
  47. }
  48. }
  49. /**
  50. * 初始化主键下标和需要合并字段的下标
  51. *
  52. * @param writeSheetHolder WriteSheetHolder
  53. */
  54. private void initPrimaryIndexAndMergeIndex(WriteSheetHolder writeSheetHolder) {
  55. // 获取当前工作表
  56. Sheet sheet = writeSheetHolder.getSheet();
  57. // 获取标题行
  58. Row titleRow = sheet.getRow(0);
  59. // 获取所有属性字段
  60. Field[] fields = this.elementType.getDeclaredFields();
  61. // 遍历所有字段
  62. for (Field field : fields) {
  63. // 获取@ExcelProperty注解,用于获取该字段对应列的下标
  64. ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
  65. // 判断是否为空
  66. if (null == excelProperty) {
  67. continue;
  68. }
  69. // 获取自定义注解,用于合并单元格
  70. ExcelMerge excelMerge = field.getAnnotation(ExcelMerge.class);
  71. // 判断是否需要合并
  72. if (null == excelMerge) {
  73. continue;
  74. }
  75. for (int i = 0; i < fields.length; i++) {
  76. Cell cell = titleRow.getCell(i);
  77. if (null == cell) {
  78. continue;
  79. }
  80. // 将字段和表头匹配上
  81. if (excelProperty.value()[0].equalsIgnoreCase(cell.getStringCellValue())) {
  82. if (excelMerge.isPrimaryKey()) {
  83. primaryKeyIndex = i;
  84. }
  85. if (excelMerge.merge()) {
  86. mergeColumnIndexList.add(i);
  87. }
  88. }
  89. }
  90. }
  91. // 没有指定主键,则异常
  92. if (null == this.primaryKeyIndex) {
  93. throw new IllegalStateException("使用@ExcelMerge注解必须指定主键");
  94. }
  95. }
  96. }
定义接口

将自定义合并策略 ExcelMergeStrategy 通过 registerWriteHandler 注册上去:

  1. /**
  2. * EasyExcel导入导出
  3. *
  4. * @author yun
  5. */
  6. @RestController
  7. @RequestMapping("/excel")
  8. public class ExcelController {
  9. @GetMapping("/export/order")
  10. public void exportOrderExcel(HttpServletResponse response) {
  11. try {
  12. this.setExcelResponseProp(response, "订单列表");
  13. List<OrderDO> orderList = this.getOrderList();
  14. List<OrderBO> exportData = this.convert(orderList);
  15. EasyExcel.write(response.getOutputStream())
  16. .head(OrderBO.class)
  17. .registerWriteHandler(new ExcelMergeStrategy(OrderBO.class))
  18. .excelType(ExcelTypeEnum.XLSX)
  19. .sheet("订单列表")
  20. .doWrite(exportData);
  21. } catch (IOException e) {
  22. throw new RuntimeException(e);
  23. }
  24. }
  25. /**
  26. * 设置响应结果
  27. *
  28. * @param response 响应结果对象
  29. * @param rawFileName 文件名
  30. * @throws UnsupportedEncodingException 不支持编码异常
  31. */
  32. private void setExcelResponseProp(HttpServletResponse response, String rawFileName) throws UnsupportedEncodingException {
  33. response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
  34. response.setCharacterEncoding("utf-8");
  35. String fileName = URLEncoder.encode(rawFileName, "UTF-8").replaceAll("\+", "%20");
  36. response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
  37. }
  38. }
测试接口

运行项目,通过 Postman 或者 Apifox 工具来进行接口测试

注意:在 Apifox 中访问接口后无法直接下载,需要点击返回结果中的下载图标才行,点击之后方可对Excel文件进行保存。

接口地址:http://localhost:8080/excel/export/order

一对多导出

  1. //需要合并的列
  2. int[] mergeColumeIndex = {0,1,2,3,4,5,8,9,11};
  3. // 从那一列开始合并
  4. int mergeRowIndex = 0;
  5. ExcelWriter excelWriter = EasyExcel.write(outputStream)
  6. .sheet("SheetName")
  7. //设置合并单元格策略
  8. .registerWriteHandler(new ExcelFillCellMergeStrategy(mergeRowIndex, mergeColumeIndex))
  9. .doWrite(exportVoList);
自定义单元格合并策略
  1. public class ExcelFillCellMergeStrategy implements CellWriteHandler {
  2. private int[] mergeColumnIndex;
  3. private int mergeRowIndex;
  4. public ExcelFillCellMergeStrategy() {
  5. }
  6. public ExcelFillCellMergeStrategy(int mergeRowIndex, int[] mergeColumnIndex) {
  7. this.mergeRowIndex = mergeRowIndex;
  8. this.mergeColumnIndex = mergeColumnIndex;
  9. }
  10. @Override
  11. public void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Head head, Integer columnIndex, Integer relativeRowIndex, Boolean isHead) {
  12. }
  13. @Override
  14. public void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
  15. }
  16. @Override
  17. public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, List<CellData> list, Cell cell, Head head, Integer integer, Boolean aBoolean) {
  18. int curRowIndex = cell.getRowIndex();
  19. int curColIndex = cell.getColumnIndex();
  20. if (curRowIndex > mergeRowIndex) {
  21. for (int i = 0; i < mergeColumnIndex.length; i++) {
  22. if (curColIndex == mergeColumnIndex[i]) {
  23. mergeWithPrevRow(writeSheetHolder, cell, curRowIndex, curColIndex);
  24. break;
  25. }
  26. }
  27. }
  28. }
  29. /**
  30. * 当前单元格向上合并
  31. *
  32. * @param writeSheetHolder
  33. * @param cell 当前单元格
  34. * @param curRowIndex 当前行
  35. * @param curColIndex 当前列
  36. */
  37. private void mergeWithPrevRow(WriteSheetHolder writeSheetHolder, Cell cell, int curRowIndex, int curColIndex) {
  38. Object curData = cell.getCellTypeEnum() == CellType.STRING ? cell.getStringCellValue() : cell.getNumericCellValue();
  39. Cell preCell = cell.getSheet().getRow(curRowIndex - 1).getCell(curColIndex);
  40. Object preData = preCell.getCellTypeEnum() == CellType.STRING ? preCell.getStringCellValue() : preCell.getNumericCellValue();
  41. // 将当前单元格数据与上一个单元格数据比较
  42. Boolean dataBool = preData.equals(curData);
  43. //此处需要注意:因为我是按照序号确定是否需要合并的,所以获取每一行第一列数据和上一行第一列数据进行比较,如果相等合并
  44. Boolean bool = cell.getRow().getCell(0).getNumericCellValue() == cell.getSheet().getRow(curRowIndex - 1).getCell(0).getNumericCellValue();
  45. if (dataBool && bool) {
  46. Sheet sheet = writeSheetHolder.getSheet();
  47. List<CellRangeAddress> mergeRegions = sheet.getMergedRegions();
  48. boolean isMerged = false;
  49. for (int i = 0; i < mergeRegions.size() && !isMerged; i++) {
  50. CellRangeAddress cellRangeAddr = mergeRegions.get(i);
  51. // 若上一个单元格已经被合并,则先移出原有的合并单元,再重新添加合并单元
  52. if (cellRangeAddr.isInRange(curRowIndex - 1, curColIndex)) {
  53. sheet.removeMergedRegion(i);
  54. cellRangeAddr.setLastRow(curRowIndex);
  55. sheet.addMergedRegion(cellRangeAddr);
  56. isMerged = true;
  57. }
  58. }
  59. // 若上一个单元格未被合并,则新增合并单元
  60. if (!isMerged) {
  61. CellRangeAddress cellRangeAddress = new CellRangeAddress(curRowIndex - 1, curRowIndex, curColIndex, curColIndex);
  62. sheet.addMergedRegion(cellRangeAddress);
  63. }
  64. }
  65. }
  66. }
测试数据

exportVoList 导出的数据。

注意:如果需要合并三个单元格需要产生3条数据 例如:

1,李华,英语,80分

1,李华,语文,70分

1,李华,数学,60分

2,李四,数学,80分

导出效果如下:

简单导入

以导入用户信息为例,接下来手把手教大家如何使用EasyExcel实现导入功能!

定义接口
  1. /**
  2. * EasyExcel导入导出
  3. *
  4. * @author yun
  5. */
  6. @RestController
  7. @RequestMapping("/excel")
  8. @Api(tags = "EasyExcel")
  9. public class ExcelController {
  10. @PostMapping("/import/user")
  11. public ResponseVO importUserExcel(@RequestPart(value = "file") MultipartFile file) {
  12. try {
  13. List<UserDO> userList = EasyExcel.read(file.getInputStream())
  14. .head(UserDO.class)
  15. .sheet()
  16. .doReadSync();
  17. return ResponseVO.success(userList);
  18. } catch (IOException e) {
  19. return ResponseVO.error();
  20. }
  21. }
  22. }
测试接口 

参考资料

  • 项目地址:https://github.com/alibaba/easyexcel

  • 官方文档:https://www.yuque.com/easyexcel/doc/easyexcel

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/神奇cpp/article/detail/769119
推荐阅读
相关标签
  

闽ICP备14008679号