前言

在平时的业务系统开发中,少不了需要用到导出、导入excel功能。Apache POI不做过多介绍
主要学习下,EasyPoi和EasyExcel的使用和区别对比。

官网
https://www.yuque.com/easyexcel

EasyPoi和EasyExcel都是基于Apache POI进行二次开发的。
不同点在于:

  1. EasyPoi 在读写数据的时候,优先是先将数据写入内存,优点是读写性能非常高,但是当数据量很大的时候,会出现OOM,当然它也提供了 sax 模式的读写方式,需要调用特定的方法实现。它主要的特点就是将更多重复的工作,全部简单化,避免编写重复的代码!
  2. EasyExcel 基于sax模式一行一行解析进行读写数据,不会出现OOM情况,在并发量很大的情况下,程序在经过高并发场景的验证下,依然能稳定运行!相对于 EasyPoi 来说,读写性能稍慢!

SAX(simple API for XML) 是一种XML解析的替代方法。相比于DOM,SAX是一种速度更快,更有效的方法。它逐行扫描文档,一边扫描一边解析。而且相比于DOM,SAX可以在解析文档的任意时刻停止解析,但任何事物都有其相反的一面,对于SAX来说就是操作复杂。

EasyPoi 对定制化的导出支持非常的丰富,如果当前的项目需求,并发量不大、数据量也不大,但是需要导出 excel 的文件样式千差万别,那么我推荐你用 EasyPoi;反之,使用 EasyExcel !

本篇学习EasyExcel的使用 👇

简单使用测试

pom.xml添加依赖

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

对于简单的导入导出封装了一个XlxsTool工具类

  1. package com.wg.easyexcel;
  2. import com.alibaba.excel.EasyExcel;
  3. import com.alibaba.excel.ExcelWriter;
  4. import com.alibaba.excel.context.AnalysisContext;
  5. import com.alibaba.excel.event.AnalysisEventListener;
  6. import com.alibaba.excel.read.metadata.holder.ReadSheetHolder;
  7. import com.alibaba.excel.write.metadata.WriteSheet;
  8. import org.slf4j.Logger;
  9. import org.slf4j.LoggerFactory;
  10. import java.io.FileInputStream;
  11. import java.io.InputStream;
  12. import java.util.*;
  13. import java.util.concurrent.atomic.AtomicInteger;
  14. public class XlxsTool {
  15. private static final Logger log = LoggerFactory.getLogger(XlxsTool.class);
  16. public static <T> List<T> loadExcel(String excelPath, Class<T> clazz) {
  17. try (FileInputStream fis = new FileInputStream(excelPath)) {
  18. return loadExcel(fis, clazz);
  19. } catch (Exception xe) {
  20. throw new RuntimeException(xe);
  21. }
  22. }
  23. public static <T> List<T> loadExcel(InputStream inputStream, Class<T> clazz) {
  24. ExcelDataListener<T> excelDataListener = new ExcelDataListener<>();
  25. EasyExcel.read(inputStream, clazz, excelDataListener).sheet().doRead();
  26. return excelDataListener.getResult();
  27. }
  28. public static <T> Map<String, List<T>> loadAllExcel(String excelPath, Class<T> clazz) {
  29. try (FileInputStream fis = new FileInputStream(excelPath)) {
  30. return loadAllExcel(fis, clazz);
  31. } catch (Exception xe) {
  32. throw new RuntimeException(xe);
  33. }
  34. }
  35. public static <T> Map<String, List<T>> loadAllExcel(InputStream inputStream, Class<T> clazz) {
  36. ExcelDataAllSheetListener<T> excelDataListener = new ExcelDataAllSheetListener<>();
  37. EasyExcel.read(inputStream, clazz, excelDataListener).doReadAll();
  38. return excelDataListener.getResult();
  39. }
  40. public static <T> void export(String filePath, Class<T> type, List<T> mainExcelData) {
  41. EasyExcel.write(filePath, type).sheet().doWrite(mainExcelData);
  42. }
  43. public static <T> void export(String filePath, Class<T> type, Map<String, List<T>> mainExcelData) {
  44. ExcelWriter excelWriter = EasyExcel.write(filePath, type).build();
  45. AtomicInteger i = new AtomicInteger();
  46. mainExcelData.forEach((key, value) -> {
  47. WriteSheet writeSheet = EasyExcel.writerSheet(i.getAndIncrement(), key).build();
  48. excelWriter.write(value, writeSheet);
  49. });
  50. excelWriter.finish();
  51. }
  52. public static class ExcelDataListener<T> extends AnalysisEventListener<T> {
  53. private final List<T> result = new ArrayList<>();
  54. @Override
  55. public void invoke(T data, AnalysisContext context) {
  56. result.add(data);
  57. }
  58. @Override
  59. public void doAfterAllAnalysed(AnalysisContext context) {
  60. }
  61. public List<T> getResult() {
  62. return result;
  63. }
  64. }
  65. public static class ExcelDataAllSheetListener<T> extends AnalysisEventListener<T> {
  66. private final Map<String, List<T>> result = new LinkedHashMap<>();
  67. @Override
  68. public void invoke(T data, AnalysisContext context) {
  69. ReadSheetHolder readSheetHolder = context.readSheetHolder();
  70. String sheetName = readSheetHolder.getSheetName();
  71. List<T> ts = result.computeIfAbsent(sheetName, k -> new ArrayList<>());
  72. ts.add(data);
  73. }
  74. @Override
  75. public void doAfterAllAnalysed(AnalysisContext context) {
  76. }
  77. public Map<String, List<T>> getResult() {
  78. return result;
  79. }
  80. }
  81. }

创建实体类

  1. package com.wg.easyexcel.entity;
  2. import com.alibaba.excel.annotation.ExcelIgnore;
  3. import com.alibaba.excel.annotation.ExcelProperty;
  4. import com.alibaba.excel.annotation.format.DateTimeFormat;
  5. import com.alibaba.excel.annotation.write.style.ColumnWidth;
  6. import com.wg.easyexcel.util.DefineConverter;
  7. import lombok.Data;
  8. import lombok.EqualsAndHashCode;
  9. import java.util.Date;
  10. @Data
  11. @EqualsAndHashCode(callSuper = false)
  12. public class Member {
  13. @ExcelProperty(value = "ID", index = 0)
  14. @ColumnWidth(10)
  15. private Long id;
  16. @ExcelProperty(value = "用户名", index = 1)
  17. @ColumnWidth(20)
  18. private String username;
  19. @ExcelIgnore
  20. private String password;
  21. @ExcelProperty(value = "昵称", index = 2)
  22. @ColumnWidth(20)
  23. private String nickname;
  24. @ExcelProperty(value = "出生日期", index = 3)
  25. @ColumnWidth(20)
  26. @DateTimeFormat("yyyy-MM-dd")
  27. private Date birthday;
  28. @ExcelProperty(value = "手机号", index = 4)
  29. @ColumnWidth(20)
  30. private String phone;
  31. @ExcelIgnore
  32. private String icon;
  33. @ExcelProperty(value = "性别", index = 5, converter = DefineConverter.GenderConverter.class)
  34. @ColumnWidth(10)
  35. private Integer gender;
  36. }

类注解
@Data与@EqualsAndHashCode的使用

用到的easyexcel中的注解,一般常用的也就下面这几个

  • @ExcelProperty(value = "ID", index = 0):index为excel中列的索引顺序
  • @ColumnWidth(10):列的宽度
  • @DateTimeFormat("yyyy-MM-dd"):时间格式
  • @ExcelIgnore:忽略项

    其他注解参考 https://www.cnblogs.com/bluekang/p/13438666.html

:::info @ExcelProperty 是最重要的一个注解,有三个参数value,index,converter。分别代表列名,列序号,数据转换方式

value和index只能二选一,通常不用设置converter

  1. value :通过标题文本对应
  2. index :通过文本行号对应
  3. converter :通常入库和出库转换使用,如性别,入库0和1,出库男和女 :::

创建自定义转换器

  1. package com.wg.easyexcel.util;
  2. import com.alibaba.excel.converters.Converter;
  3. import com.alibaba.excel.converters.ReadConverterContext;
  4. import com.alibaba.excel.converters.WriteConverterContext;
  5. import com.alibaba.excel.enums.CellDataTypeEnum;
  6. import com.alibaba.excel.metadata.data.WriteCellData;
  7. import com.alibaba.excel.util.StringUtils;
  8. /**
  9. * 自定义转换器
  10. */
  11. public class DefineConverter {
  12. // excel性别转换器
  13. // String与Integer转换
  14. public static class GenderConverter implements Converter<Integer> {
  15. @Override
  16. public Class<?> supportJavaTypeKey() {
  17. // 对象类型属性
  18. return Integer.class;
  19. }
  20. @Override
  21. public CellDataTypeEnum supportExcelTypeKey() {
  22. // CellData属性类型
  23. return CellDataTypeEnum.STRING;
  24. }
  25. @Override
  26. public Integer convertToJavaData(ReadConverterContext<?> context) throws Exception {
  27. // CellData转对象属性
  28. String cellStr = context.getReadCellData().getStringValue();
  29. if (StringUtils.isEmpty(cellStr)){
  30. return null;
  31. }
  32. if ("男".equals(cellStr)){
  33. return 0;
  34. }else if ("女".equals(cellStr)){
  35. return 1;
  36. }else {
  37. return null;
  38. }
  39. }
  40. @Override
  41. public WriteCellData<?> convertToExcelData(WriteConverterContext<Integer> context) throws Exception {
  42. // 对象属性转CellData
  43. Integer cellValue = context.getValue();
  44. if (cellValue == null){
  45. return new WriteCellData<>("");
  46. }
  47. if (cellValue == 0){
  48. return new WriteCellData<>("男");
  49. }else if (cellValue == 1) {
  50. return new WriteCellData<>("女");
  51. } else {
  52. return new WriteCellData<>("");
  53. }
  54. }
  55. }
  56. }

pom.xml中添加juint的依赖,使用junit测试

  1. package com.wg.easyexcel;
  2. import com.wg.easyexcel.entity.Member;
  3. import org.junit.Test;
  4. import org.slf4j.Logger;
  5. import org.slf4j.LoggerFactory;
  6. import java.util.*;
  7. // 简单导入导出
  8. public class simpleDemo {
  9. private static final Logger log = LoggerFactory.getLogger(simpleDemo.class);
  10. private static final String readExcelPath = "/Users/weigang/IdeaProjects/study/easyexcel/src/main/resources/file/simple/simple-excel.xlsx";
  11. private static final String outputExcelPath = "/Users/weigang/IdeaProjects/study/easyexcel/src/main/resources/file/simple/simple-excel-output.xlsx";
  12. @Test
  13. public void test(){
  14. // 单 sheet 读取
  15. List<Member> dataList = XlxsTool.loadExcel(readExcelPath, Member.class);
  16. // 多sheet 读取
  17. // key为sheet页的名字,value为对应sheet页的数据
  18. // Map<String ,List<Member>> dataMap = XlxsTool.loadAllExcel(readExcelPath, Member.class);
  19. // 导出
  20. XlxsTool.export(outputExcelPath, Member.class, dataList);
  21. }
  22. }

效果
image.png

实例:mybatis+mysql+easyexcel

案例一:简单导入导出Member表

Mysql数据库
member建表语句

  1. SET NAMES utf8mb4;
  2. SET FOREIGN_KEY_CHECKS = 0;
  3. -- ----------------------------
  4. -- Table structure for member
  5. -- ----------------------------
  6. DROP TABLE IF EXISTS `member`;
  7. CREATE TABLE `member` (
  8. `id` bigint NOT NULL,
  9. `username` varchar(255) DEFAULT NULL,
  10. `password` varchar(255) DEFAULT NULL,
  11. `nickname` varchar(255) DEFAULT NULL,
  12. `birthday` varchar(255) DEFAULT NULL,
  13. `phone` varchar(255) DEFAULT NULL,
  14. `gender` int DEFAULT NULL,
  15. PRIMARY KEY (`id`)
  16. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  17. -- ----------------------------
  18. -- Records of member
  19. -- ----------------------------
  20. BEGIN;
  21. INSERT INTO `member` VALUES (1, 'weig', '1234', 'gangzi', '1995-05-27', '15900330611', 0);
  22. INSERT INTO `member` VALUES (2, 'lijr', '1234', 'ruru', '1995-07-01', '13920803165', 1);
  23. INSERT INTO `member` VALUES (3, 'weip', '1234', 'erdan', '1997-04-25', '15900330611', 0);
  24. COMMIT;
  25. SET FOREIGN_KEY_CHECKS = 1;

定义对应实体类 Member

  1. @Data
  2. @EqualsAndHashCode(callSuper = false)
  3. public class Member {
  4. @ExcelProperty(value = "ID", index = 0)
  5. @ColumnWidth(10)
  6. private Long id;
  7. @ExcelProperty(value = "用户名", index = 1)
  8. @ColumnWidth(20)
  9. private String username;
  10. @ExcelIgnore
  11. private String password;
  12. @ExcelProperty(value = "昵称", index = 2)
  13. @ColumnWidth(20)
  14. private String nickname;
  15. @ExcelProperty(value = "出生日期", index = 3)
  16. @ColumnWidth(20)
  17. @DateTimeFormat("yyyy-MM-dd")
  18. private Date birthday;
  19. @ExcelProperty(value = "手机号", index = 4)
  20. @ColumnWidth(20)
  21. private String phone;
  22. @ExcelIgnore
  23. private String icon;
  24. @ExcelProperty(value = "性别", index = 5, converter = DefineConverter.GenderConverter.class)
  25. @ColumnWidth(10)
  26. private Integer gender;
  27. }

还是需要定义转换器,采用如上就可以
定义对应Mapper接口 MemberMapper

  1. @Mapper
  2. public interface MemberMapper {
  3. @Select("SELECT * FROM member")
  4. @Results({
  5. @Result(property = "id", column = "id"),
  6. @Result(property = "username", column = "username"),
  7. @Result(property = "password", column = "password"),
  8. @Result(property = "nickname", column = "nickname"),
  9. @Result(property = "birthday", column = "birthday"),
  10. @Result(property = "phone", column = "phone"),
  11. @Result(property = "gender", column = "gender"),
  12. })
  13. List<Member> getMember();
  14. }

定义对应service层 MemberServiceImpl

  1. @Service
  2. public class MemberServiceImpl {
  3. @Autowired
  4. MemberMapper mapper;
  5. public List<Member> getMember(){
  6. return mapper.getMember();
  7. }
  8. }

定义controller层 easyexcelController

  1. @RestController
  2. @RequestMapping("/easyexcel")
  3. public class easyexcelController {
  4. @Autowired
  5. MemberMapper memberService;
  6. // 简单导入
  7. @SneakyThrows
  8. @RequestMapping(value = "/importMemberList", method = RequestMethod.POST)
  9. @ResponseBody
  10. public CommonResult importMemberList(@RequestPart("file") MultipartFile file) {
  11. List<Member> memberList = EasyExcel.read(file.getInputStream())
  12. .head(Member.class)
  13. .sheet()
  14. .doReadSync();
  15. return CommonResult.success(memberList);
  16. }
  17. // 设置excel下载响应头属性
  18. private void setExcelRespProp(HttpServletResponse response, String rawFileName) throws UnsupportedEncodingException {
  19. response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
  20. response.setCharacterEncoding("utf-8");
  21. String fileName = URLEncoder.encode(rawFileName, "UTF-8").replaceAll("\\+" ,"%20");
  22. response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
  23. }
  24. // 简单导出
  25. @SneakyThrows(IOException.class)
  26. @RequestMapping(value = "/exportMemberList", method = RequestMethod.GET)
  27. public void exportMemberList(HttpServletResponse response) {
  28. setExcelRespProp(response, "会员列表");
  29. List<Member> memberList = memberService.getMember();
  30. EasyExcel.write(response.getOutputStream())
  31. .head(Member.class)
  32. .excelType(ExcelTypeEnum.XLSX)
  33. .sheet("会员列表")
  34. .doWrite(memberList);
  35. }
  36. }

补充:
对于导入返回的结果获取,封装了一个通用对象CommonResult
首先创建一个封装API的错误码的接口IErrorCode

  1. /**
  2. * 封装API的错误码
  3. * Created by macro on 2019/4/19.
  4. */
  5. public interface IErrorCode {
  6. long getCode();
  7. String getMessage();
  8. }

定义一个枚举类型ResultCode用来返回一些错误码

  1. package com.wg.easyexcel.common.api;
  2. public enum ResultCode implements IErrorCode{
  3. SUCCESS(200, "操作成功"),
  4. FAILED(500, "操作失败"),
  5. VALIDATE_FAILED(404, "参数检验失败"),
  6. UNAUTHORIZED(401, "暂未登录或token已经过期"),
  7. FORBIDDEN(403, "没有相关权限");
  8. private long code;
  9. private String message;
  10. private ResultCode(long code, String message) {
  11. this.code = code;
  12. this.message = message;
  13. }
  14. public long getCode() {
  15. return code;
  16. }
  17. public String getMessage() {
  18. return message;
  19. }
  20. }

通过泛型定义一个通用返回对象 CommonResult

  1. package com.wg.easyexcel.common.api;
  2. import org.apache.poi.ss.formula.functions.T;
  3. /**
  4. * 通用返回对象
  5. * Created by macro on 2019/4/19.
  6. */
  7. public class CommonResult<T> {
  8. private long code;
  9. private String message;
  10. private T data;
  11. protected CommonResult() {
  12. }
  13. protected CommonResult(long code, String message, T data) {
  14. this.code = code;
  15. this.message = message;
  16. this.data = data;
  17. }
  18. /**
  19. * 成功返回结果
  20. *
  21. * @param data 获取的数据
  22. */
  23. public static <T> CommonResult<T> success(T data) {
  24. return new CommonResult<T>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data);
  25. }
  26. /**
  27. * 成功返回结果
  28. *
  29. * @param data 获取的数据
  30. * @param message 提示信息
  31. */
  32. public static <T> CommonResult<T> success(T data, String message) {
  33. return new CommonResult<T>(ResultCode.SUCCESS.getCode(), message, data);
  34. }
  35. /**
  36. * 失败返回结果
  37. * @param errorCode 错误码
  38. */
  39. public static <T> CommonResult<T> failed(IErrorCode errorCode) {
  40. return new CommonResult<T>(errorCode.getCode(), errorCode.getMessage(), null);
  41. }
  42. /**
  43. * 失败返回结果
  44. * @param message 提示信息
  45. */
  46. public static <T> CommonResult<T> failed(String message) {
  47. return new CommonResult<T>(ResultCode.FAILED.getCode(), message, null);
  48. }
  49. /**
  50. * 失败返回结果
  51. */
  52. public static <T> CommonResult<T> failed() {
  53. return failed(ResultCode.FAILED);
  54. }
  55. /**
  56. * 参数验证失败返回结果
  57. */
  58. public static <T> CommonResult<T> validateFailed() {
  59. return failed(ResultCode.VALIDATE_FAILED);
  60. }
  61. /**
  62. * 参数验证失败返回结果
  63. * @param message 提示信息
  64. */
  65. public static <T> CommonResult<T> validateFailed(String message) {
  66. return new CommonResult<T>(ResultCode.VALIDATE_FAILED.getCode(), message, null);
  67. }
  68. /**
  69. * 未登录返回结果
  70. */
  71. public static <T> CommonResult<T> unauthorized(T data) {
  72. return new CommonResult<T>(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage(), data);
  73. }
  74. /**
  75. * 未授权返回结果
  76. */
  77. public static <T> CommonResult<T> forbidden(T data) {
  78. return new CommonResult<T>(ResultCode.FORBIDDEN.getCode(), ResultCode.FORBIDDEN.getMessage(), data);
  79. }
  80. public long getCode() {
  81. return code;
  82. }
  83. public void setCode(long code) {
  84. this.code = code;
  85. }
  86. public String getMessage() {
  87. return message;
  88. }
  89. public void setMessage(String message) {
  90. this.message = message;
  91. }
  92. public T getData() {
  93. return data;
  94. }
  95. public void setData(T data) {
  96. this.data = data;
  97. }
  98. }

通过ApiPost接口测试
在导入时,设置为Post请求。测试时需注意,同时需要在如图位置,设置上传参数,选择上传的文件
实时响应为解析出的excel内容
image.png
在导出设置为Get请求,在测试时需注意,测试成功后,点击右下方的下载,查看输出excel内容
image.png
image.png

案例二:复杂导出order,实现自定义单元格合并

业务:
一个订单 order,一个顾客 member,一个商品 product
每个订单 order 有

  1. 订单本身信息
  2. 对应的顾客 member
  3. 对应的若干商品 List

order
—————————————————————————
订单ID,订单编号,创建时间,收货地址,member,List
—————————————————————————

效果
普通订单表
image.png
实现自定义合并单元格策略的订单表
image.png

sql文件

  1. SET NAMES utf8mb4;
  2. SET FOREIGN_KEY_CHECKS = 0;
  3. -- ----------------------------
  4. -- Table structure for member
  5. -- ----------------------------
  6. DROP TABLE IF EXISTS `member`;
  7. CREATE TABLE `member` (
  8. `id` bigint NOT NULL,
  9. `username` varchar(255) DEFAULT NULL,
  10. `password` varchar(255) DEFAULT NULL,
  11. `nickname` varchar(255) DEFAULT NULL,
  12. `birthday` varchar(255) DEFAULT NULL,
  13. `phone` varchar(255) DEFAULT NULL,
  14. `gender` int DEFAULT NULL,
  15. PRIMARY KEY (`id`)
  16. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  17. -- ----------------------------
  18. -- Records of member
  19. -- ----------------------------
  20. BEGIN;
  21. INSERT INTO `member` VALUES (1, 'weig', '1234', 'gangzi', '1995-05-27', '15900330611', 0);
  22. INSERT INTO `member` VALUES (2, 'lijr', '1234', 'ruru', '1995-07-01', '13920803165', 1);
  23. INSERT INTO `member` VALUES (3, 'weip', '1234', 'erdan', '1997-04-25', '15900330611', 0);
  24. COMMIT;
  25. -- ----------------------------
  26. -- Table structure for order
  27. -- ----------------------------
  28. DROP TABLE IF EXISTS `order`;
  29. CREATE TABLE `order` (
  30. `id` bigint NOT NULL,
  31. `orderSn` varchar(255) DEFAULT NULL,
  32. `createTime` date DEFAULT NULL,
  33. `receiverAddress` varchar(255) DEFAULT NULL,
  34. PRIMARY KEY (`id`)
  35. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  36. -- ----------------------------
  37. -- Records of order
  38. -- ----------------------------
  39. BEGIN;
  40. INSERT INTO `order` VALUES (1, '20220115010100001', '2022-01-15', '天津市北辰区');
  41. INSERT INTO `order` VALUES (2, '20220115010100002', '2022-01-16', '天津市和平区');
  42. INSERT INTO `order` VALUES (3, '20220115010100003', '2022-01-17', '天津市西青区');
  43. COMMIT;
  44. -- ----------------------------
  45. -- Table structure for product
  46. -- ----------------------------
  47. DROP TABLE IF EXISTS `product`;
  48. CREATE TABLE `product` (
  49. `id` bigint NOT NULL,
  50. `productSn` varchar(255) DEFAULT NULL,
  51. `name` varchar(255) DEFAULT NULL,
  52. `subTitle` varchar(255) DEFAULT NULL,
  53. `brandName` varchar(255) DEFAULT NULL,
  54. `price` decimal(10,2) DEFAULT NULL,
  55. `count` int DEFAULT NULL,
  56. PRIMARY KEY (`id`)
  57. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  58. -- ----------------------------
  59. -- Records of product
  60. -- ----------------------------
  61. BEGIN;
  62. INSERT INTO `product` VALUES (1, '7437788', '小米10s', '小米10s的标题', '小米', 1.00, 100);
  63. INSERT INTO `product` VALUES (2, '7437789', '红米', '红米的标题', '小米', 2.00, 200);
  64. INSERT INTO `product` VALUES (3, '7437799', 'Appale12', 'Appale12的标题', '苹果', 3.00, 300);
  65. COMMIT;
  66. SET FOREIGN_KEY_CHECKS = 1;

在如上项目进行改进(Member之前定义过了,后面不再介绍)
定义对应的实体类 Member,Order,Product

  1. @Data
  2. @EqualsAndHashCode(callSuper = false)
  3. public class Order {
  4. private Long id;
  5. private String orderSn;
  6. private Date createTime;
  7. private String receiverAddress;
  8. private Member member;
  9. private List<Product> productList;
  10. }
  11. @Data
  12. @EqualsAndHashCode(callSuper = false)
  13. public class Product {
  14. private Long id;
  15. private String productSn;
  16. private String name;
  17. private String subTitle;
  18. private String brandName;
  19. private BigDecimal price;
  20. private Integer count;
  21. }

对于自定义单元格合并策略,需要将order表的数据先平铺,通过另一实体类OrderData接收

  1. @Data
  2. @EqualsAndHashCode(callSuper = false)
  3. public class OrderData {
  4. @ExcelProperty(value = "订单ID")
  5. @ColumnWidth(10)
  6. @CustomMerge(needMerge = true, isPk = true)
  7. private String id;
  8. @ExcelProperty(value = "订单编码")
  9. @ColumnWidth(20)
  10. @CustomMerge(needMerge = true)
  11. private String orderSn;
  12. @ExcelProperty(value = "创建时间")
  13. @ColumnWidth(20)
  14. @DateTimeFormat("yyyy-MM-dd")
  15. @CustomMerge(needMerge = true)
  16. private Date createTime;
  17. @ExcelProperty(value = "收货地址")
  18. @CustomMerge(needMerge = true)
  19. @ColumnWidth(20)
  20. private String receiverAddress;
  21. @ExcelProperty(value = "顾客用户名")
  22. @ColumnWidth(20)
  23. @CustomMerge(needMerge = true)
  24. private String username;
  25. @ExcelProperty(value = "顾客昵称")
  26. @ColumnWidth(30)
  27. @CustomMerge(needMerge = true)
  28. private String nickname;
  29. // 如果为二级表头,value设置为数组形式
  30. @ExcelProperty(value = {"商品信息", "商品编码"})
  31. @ColumnWidth(20)
  32. private String productSn;
  33. @ExcelProperty(value = {"商品信息", "商品名称"})
  34. @ColumnWidth(20)
  35. private String name;
  36. @ExcelProperty(value = {"商品信息", "商品标题"})
  37. @ColumnWidth(30)
  38. private String subTitle;
  39. @ExcelProperty(value = {"商品信息", "品牌名称"})
  40. @ColumnWidth(20)
  41. private String brandName;
  42. @ExcelProperty(value = {"商品信息", "商品价格"})
  43. @ColumnWidth(20)
  44. private BigDecimal price;
  45. @ExcelProperty(value = {"商品信息", "商品数量"})
  46. @ColumnWidth(20)
  47. private Integer count;
  48. }

其中对于部分需要合并的单元格,自定义了一个注解@CustomMerge来标识是否需要合并以及合并的主键

  1. /**
  2. * 自定义注解,用于判断是否需要合并以及合并的主键
  3. */
  4. @Target({ElementType.FIELD})
  5. @Retention(RetentionPolicy.RUNTIME)
  6. @Inherited
  7. public @interface CustomMerge {
  8. /**
  9. * 是否需要合并单元格
  10. */
  11. boolean needMerge() default false;
  12. /**
  13. * 是否是主键,即该字段相同的行合并
  14. */
  15. boolean isPk() default false;
  16. }

接下来的重头戏,就是自定义单元格的合并策略 CustomMergeStrategy,最后在导出接口上需要将其注册上去

  1. /**
  2. * 自定义单元格合并策略
  3. */
  4. public class CustomMergeStrategy implements RowWriteHandler {
  5. /**
  6. * 主键下标
  7. */
  8. private Integer pkIndex;
  9. /**
  10. * 需要合并的列的下标集合
  11. */
  12. private List<Integer> needMergeColumnIndex = new ArrayList<>();
  13. /**
  14. * DTO数据类型
  15. */
  16. private Class<?> elementType;
  17. public CustomMergeStrategy(Class<?> elementType) {
  18. this.elementType = elementType;
  19. }
  20. @Override
  21. public void afterRowDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Integer relativeRowIndex, Boolean isHead) {
  22. // 如果是标题,则直接返回
  23. if (isHead) {
  24. return;
  25. }
  26. // 获取当前sheet
  27. Sheet sheet = writeSheetHolder.getSheet();
  28. // 获取标题行
  29. Row titleRow = sheet.getRow(0);
  30. if (null == pkIndex) {
  31. this.lazyInit(writeSheetHolder);
  32. }
  33. // 判断是否需要和上一行进行合并
  34. // 不能和标题合并,只能数据行之间合并
  35. if (row.getRowNum() <= 1) {
  36. return;
  37. }
  38. // 获取上一行数据
  39. Row lastRow = sheet.getRow(row.getRowNum() - 1);
  40. // 将本行和上一行是同一类型的数据(通过主键字段进行判断),则需要合并
  41. if (lastRow.getCell(pkIndex).getStringCellValue().equalsIgnoreCase(row.getCell(pkIndex).getStringCellValue())) {
  42. for (Integer needMerIndex : needMergeColumnIndex) {
  43. CellRangeAddress cellRangeAddress = new CellRangeAddress(row.getRowNum() - 1, row.getRowNum(),
  44. needMerIndex, needMerIndex);
  45. sheet.addMergedRegionUnsafe(cellRangeAddress);
  46. }
  47. }
  48. }
  49. /**
  50. * 初始化主键下标和需要合并字段的下标
  51. */
  52. private void lazyInit(WriteSheetHolder writeSheetHolder) {
  53. // 获取当前sheet
  54. Sheet sheet = writeSheetHolder.getSheet();
  55. // 获取标题行
  56. Row titleRow = sheet.getRow(0);
  57. // 获取DTO的类型
  58. Class<?> eleType = this.elementType;
  59. // 获取DTO所有的属性
  60. Field[] fields = eleType.getDeclaredFields();
  61. // 遍历所有的字段,因为是基于DTO的字段来构建excel,所以字段数 >= excel的列数
  62. for (Field theField : fields) {
  63. // 获取@ExcelProperty注解,用于获取该字段对应在excel中的列的下标
  64. ExcelProperty easyExcelAnno = theField.getAnnotation(ExcelProperty.class);
  65. // 为空,则表示该字段不需要导入到excel,直接处理下一个字段
  66. if (null == easyExcelAnno) {
  67. continue;
  68. }
  69. // 获取自定义的注解,用于合并单元格
  70. CustomMerge customMerge = theField.getAnnotation(CustomMerge.class);
  71. // 没有@CustomMerge注解的默认不合并
  72. if (null == customMerge) {
  73. continue;
  74. }
  75. for (int index = 0; index < fields.length; index++) {
  76. Cell theCell = titleRow.getCell(index);
  77. // 当配置为不需要导出时,返回的为null,这里作一下判断,防止NPE
  78. if (null == theCell) {
  79. continue;
  80. }
  81. // 将字段和excel的表头匹配上
  82. if (easyExcelAnno.value()[0].equalsIgnoreCase(theCell.getStringCellValue())) {
  83. if (customMerge.isPk()) {
  84. pkIndex = index;
  85. }
  86. if (customMerge.needMerge()) {
  87. needMergeColumnIndex.add(index);
  88. }
  89. }
  90. }
  91. }
  92. // 没有指定主键,则异常
  93. if (null == this.pkIndex) {
  94. throw new IllegalStateException("使用@CustomMerge注解必须指定主键");
  95. }
  96. }
  97. }

定义对应Mapper接口 OrderMapper,ProductMapper

  1. @Mapper
  2. public interface OrderMapper {
  3. @Select("SELECT * FROM `order`")
  4. @Results({
  5. @Result(property = "id", column = "id"),
  6. @Result(property = "orderSn", column = "orderSn"),
  7. @Result(property = "createTime", column = "createTime"),
  8. @Result(property = "receiverAddress", column = "receiverAddress"),
  9. })
  10. List<Order> getOrder();
  11. }
  12. @Mapper
  13. public interface ProductMapper {
  14. @Select("SELECT * FROM product")
  15. @Results({
  16. @Result(property = "id", column = "id"),
  17. @Result(property = "productSn", column = "productSn"),
  18. @Result(property = "name", column = "name"),
  19. @Result(property = "subTitle", column = "subTitle"),
  20. @Result(property = "brandName", column = "brandName"),
  21. @Result(property = "price", column = "price"),
  22. @Result(property = "count", column = "count"),
  23. })
  24. List<Product> getProduct();
  25. }

定义对应service层 OrderServiceImpl,ProductServiceImpl

  1. @Service
  2. public class OrderServiceImpl {
  3. @Autowired
  4. OrderMapper mapper;
  5. public List<Order> getOrder(){
  6. return mapper.getOrder();
  7. }
  8. }
  9. @Service
  10. public class ProductServiceImpl {
  11. @Autowired
  12. ProductMapper mapper;
  13. public List<Product> getProduct(){
  14. return mapper.getProduct();
  15. }
  16. }

�easyexcelController中

  1. @RestController
  2. @RequestMapping("/easyexcel")
  3. public class easyexcelController {
  4. @Autowired
  5. OrderServiceImpl orderService;
  6. @Autowired
  7. ProductMapper productService;
  8. @Autowired
  9. MemberMapper memberService;
  10. // 简单导入
  11. @SneakyThrows
  12. @RequestMapping(value = "/importMemberList", method = RequestMethod.POST)
  13. @ResponseBody
  14. public CommonResult importMemberList(@RequestPart("file") MultipartFile file) {
  15. List<Member> memberList = EasyExcel.read(file.getInputStream())
  16. .head(Member.class)
  17. .sheet()
  18. .doReadSync();
  19. return CommonResult.success(memberList);
  20. }
  21. // 设置excel下载响应头属性
  22. private void setExcelRespProp(HttpServletResponse response, String rawFileName) throws UnsupportedEncodingException {
  23. response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
  24. response.setCharacterEncoding("utf-8");
  25. String fileName = URLEncoder.encode(rawFileName, "UTF-8").replaceAll("\\+" ,"%20");
  26. response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
  27. }
  28. // 简单导出
  29. @SneakyThrows(IOException.class)
  30. @RequestMapping(value = "/exportMemberList", method = RequestMethod.GET)
  31. public void exportMemberList(HttpServletResponse response) {
  32. setExcelRespProp(response, "会员列表");
  33. List<Member> memberList = memberService.getMember();
  34. EasyExcel.write(response.getOutputStream())
  35. .head(Member.class)
  36. .excelType(ExcelTypeEnum.XLSX)
  37. .sheet("会员列表")
  38. .doWrite(memberList);
  39. }
  40. // 复杂导出
  41. @SneakyThrows
  42. @RequestMapping(value = "/exportOrderList", method = RequestMethod.GET)
  43. public void exportOrderList(HttpServletResponse response) {
  44. List<Order> orderList = getOrderList();
  45. List<OrderData> orderDataList = convert(orderList);
  46. setExcelRespProp(response, "订单列表");
  47. EasyExcel.write(response.getOutputStream())
  48. .head(OrderData.class)
  49. // 注册自定义合并策略
  50. .registerWriteHandler(new CustomMergeStrategy(OrderData.class))
  51. .excelType(ExcelTypeEnum.XLSX)
  52. .sheet("订单列表")
  53. .doWrite(orderDataList);
  54. }
  55. // 平铺到OrderData上
  56. private List<OrderData> convert(List<Order> orderList) {
  57. List<OrderData> result = new ArrayList<>();
  58. for (Order order : orderList) {
  59. List<Product> productList = order.getProductList();
  60. Member member = order.getMember();
  61. for (Product product : productList) {
  62. OrderData orderData = new OrderData();
  63. BeanUtil.copyProperties(product,orderData);
  64. BeanUtil.copyProperties(order,orderData);
  65. BeanUtil.copyProperties(member,orderData);
  66. result.add(orderData);
  67. }
  68. }
  69. return result;
  70. }
  71. private List<Order> getOrderList() {
  72. List<Order> orderList = orderService.getOrder();
  73. List<Product> productList = productService.getProduct();
  74. List<Member> memberList = memberService.getMember();
  75. for (int i = 0; i < orderList.size(); i++) {
  76. Order order = orderList.get(i);
  77. order.setMember(memberList.get(i));
  78. order.setProductList(productList);
  79. }
  80. return orderList;
  81. }
  82. }

over搞定🤝

以上参考借鉴 https://juejin.cn/post/7051751438715715620

EasyPoi与EasyExcel性能对比

https://www.jianshu.com/p/4e56a1a1db3f

总结

EasyExcel以使用简单、节省内存,性能高效著称。EasyExcel 能大大减少占用内存的主要原因是在解析 Excel 时没有将文件数据一次性全部加载到内存中, 而是从磁盘上一行行读取数据,逐个解析。
但是对于复杂点的导出,需要自己实现。这点EasyPoi做的较好,使用简单,功能强大,但是性能低于easyexcel

  • 如果Excel导出数据量不大的话,可以使用EasyPoi
  • 如果数据量大,比较在意性能的话,使用EasyExcel。

公众号:程序员WeiG
个人博客:wggz.top