基础学习

准备excel文件如下:
image.png

根据文件准备实体类

SimpleData

  1. @Data
  2. public class SimpleData {
  3. private String strLabel;
  4. private String dateLabel;
  5. private int intLabel;
  6. private double doubleLabel;
  7. }

监听器

为该文件创建一个监听类

  1. // 有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
  2. public class DemoDataListener extends AnalysisEventListener<DemoData> {
  3. private static final Logger LOGGER = LoggerFactory.getLogger(DemoDataListener.class);
  4. /**
  5. * 每隔5条存储数据库,实际使用中可以3000条,然后清理list ,方便内存回收
  6. */
  7. private static final int BATCH_COUNT = 5;
  8. List<DemoData> list = new ArrayList<>();
  9. /**
  10. * 假设这个是一个DAO,当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。
  11. */
  12. private DemoDAO demoDAO;
  13. public DemoDataListener() {
  14. // 这里是demo,所以随便new一个。实际使用如果到了spring,请使用下面的有参构造函数
  15. demoDAO = new DemoDAO();
  16. }
  17. /**
  18. * 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来
  19. *
  20. * @param demoDAO
  21. */
  22. public DemoDataListener(DemoDAO demoDAO) {
  23. this.demoDAO = demoDAO;
  24. }
  25. /**
  26. * 这个每一条数据解析都会来调用
  27. *
  28. * @param data one row value. Is is same as {@link AnalysisContext#readRowHolder()}
  29. * @param context
  30. */
  31. @Override
  32. public void invoke(DemoData data, AnalysisContext context) {
  33. LOGGER.info("解析到一条数据:{}", JSON.toJSONString(data));
  34. list.add(data);
  35. // 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
  36. if (list.size() >= BATCH_COUNT) {
  37. saveData();
  38. // 存储完成清理 list
  39. list.clear();
  40. }
  41. }
  42. /**
  43. * 所有数据解析完成了 都会来调用
  44. *
  45. * @param context
  46. */
  47. @Override
  48. public void doAfterAllAnalysed(AnalysisContext context) {
  49. // 这里也要保存数据,确保最后遗留的数据也存储到数据库
  50. saveData();
  51. LOGGER.info("所有数据解析完成!");
  52. }
  53. /**
  54. * 加上存储数据库
  55. */
  56. private void saveData() {
  57. LOGGER.info("{}条数据,开始存储数据库!", list.size());
  58. demoDAO.save(list);
  59. LOGGER.info("存储数据库成功!");
  60. }
  61. }

业务代码

  1. public class DemoDAO {
  2. public void save(List<DemoData> list) {
  3. System.out.println("存储成功");
  4. }
  5. }

测试代码

  1. public class SimpleRead {
  2. public static void main(String[] args) throws IOException {
  3. ClassPathResource resource = new ClassPathResource("demo/demo.xlsx");
  4. String absolutePath = resource.getFile().getAbsolutePath();
  5. ExcelReader excelReader = null;
  6. try {
  7. excelReader = EasyExcel.read(absolutePath, DemoData.class, new DemoDataListener()).build();
  8. ReadSheet readSheet = EasyExcel.readSheet(0).build();
  9. excelReader.read(readSheet);
  10. } finally {
  11. assert excelReader != null;
  12. excelReader.finish();
  13. }
  14. }
  15. }

运行结果

image.png
这种方式一定严格遵循excel表中列的数据类型进行设计实体类的数据类型,因为程序会进行自上而下的获取列,如果顺序错误,可能会导致数据类型转换异常

指定列名查询或者下标查询

IndexOrNameData

  1. @Data
  2. public class IndexOrNameData {
  3. @ExcelProperty("字符串")
  4. private String strLabel;
  5. @ExcelProperty("日期")
  6. private String dateLabel;
  7. @ExcelProperty("数字")
  8. private int intLabel;
  9. @ExcelProperty("小数")
  10. private double doubleLabel;
  11. }
  12. //或者
  13. @Data
  14. public class IndexOrNameData {
  15. @ExcelProperty(index = 0)
  16. private String strLabel;
  17. @ExcelProperty(index = 2)
  18. private String dateLabel;
  19. @ExcelProperty(index = 3)
  20. private int intLabel;
  21. @ExcelProperty(index = 1)
  22. private double doubleLabel;
  23. }

注意对于@ExcelProperty注解,要么使用index进行标记列的位置,下标从0 开始,要么使用name值进行匹配列
一般推荐使用name进行匹配,不推荐两个混合使用,
如果name重复,后面的列会覆盖前面的列,导致只能获取到一列的值

使用指定之后只要指定正确即可,不用太在意实体类属性的声明顺序
image.png

读取多个Sheet

读取一张表中的多个sheet,默认读取第一个

核心业务代码

  1. public static void readMoreSheets(String path) {
  2. ExcelReader excelReader = null;
  3. try {
  4. //读取全部sheet
  5. EasyExcel.read(path, SimpleData.class, new SimpleDataListener()).doReadAll();
  6. //读取部分sheet
  7. excelReader = EasyExcel.read(path).build();
  8. //不同的sheet必须使用不同的listener
  9. ReadSheet readSheet1 = EasyExcel.readSheet(0).head(SimpleData.class).registerReadListener(new SimpleDataListener()).build();
  10. ReadSheet readSheet2 = EasyExcel.readSheet(1).head(IndexOrNameData.class).registerReadListener(new IndexOrNameDataListener()).build();
  11. //必须将多个sheet传入
  12. excelReader.read(readSheet1,readSheet2);
  13. } finally {
  14. assert excelReader != null;
  15. excelReader.finish();
  16. }
  17. }

核心步骤如下:

  1. 生成ExcelReader,先读取全部的sheets
  2. 单独读取每一个sheet,并为之分配对应的实体类和监听器
  3. 将多个读取的sheet传入ExcelReader

image.png

但是读取sheet会时如果是03版本的excel会重复读取多次

日期、数字或者自定义格式转换

全局注册转换器

在进行读取或者写入操作 的时候进行注册

  1. public static void convertExcel(String path) {
  2. EasyExcel.read(path, ConvertData.class, new ConvertDataListener())
  3. //可以注册转换器,但是这种注册方式是全局的,可以对所有列进行转换
  4. //如果只需要转换一列的话,使用@ExcelProperty(converter="转换器.class")
  5. .registerConverter(new CustomStringConvert())
  6. .sheet()
  7. .doRead();
  8. }

局部使用转换器

在对应的实体类中进行声明
实体

  1. @Data
  2. public class ConvertData {
  3. @ExcelProperty(index = 0, converter = CustomStringConvert.class)
  4. private String strLabel;
  5. @ExcelProperty(index = 2)
  6. @DateTimeFormat("yy-MM-dd HH:mm:ss")
  7. private String dateLabel;
  8. @ExcelProperty(index = 3)
  9. private int intLabel;
  10. @ExcelProperty(index = 1)
  11. @NumberFormat("#.##%")
  12. private double doubleLabel;
  13. }

测试代码

  1. public static void convertExcel(String path) {
  2. EasyExcel.read(path, ConvertData.class, new ConvertDataListener())
  3. //可以注册转换器,但是这种注册方式是全局的,可以对所有列进行转换
  4. //如果只需要转换一列的话,使用@ExcelProperty(converter="转换器.class")
  5. //.registerConverter(new CustomStringConvert())
  6. .sheet()
  7. .doRead();
  8. }

超链接、批注、合并单元格读取

在相对应的监听器中重写extra方法即可

为额外的数据读取创建实体,或者使用map

  1. @Data
  2. public class ExtraData {
  3. private String row1;
  4. private String row2;
  5. private String row3;
  6. }

监听器重写方法

  1. public class ExtraDataListener extends AnalysisEventListener<ExtraData> {
  2. private static final Logger LOGGER = LoggerFactory.getLogger(ExtraDataListener.class);
  3. /**
  4. * 每隔5条存储数据库,实际使用中可以3000条,然后清理list ,方便内存回收
  5. */
  6. private static final int BATCH_COUNT = 5;
  7. /**
  8. * 假设这个是一个DAO,当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。
  9. */
  10. private final DemoDAO demoDAO;
  11. List<ExtraData> list = new ArrayList<>();
  12. public ExtraDataListener() {
  13. // 这里是demo,所以随便new一个。实际使用如果到了spring,请使用下面的有参构造函数
  14. demoDAO = new DemoDAO();
  15. }
  16. /**
  17. * 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来
  18. *
  19. * @param demoDAO
  20. */
  21. public ExtraDataListener(DemoDAO demoDAO) {
  22. this.demoDAO = demoDAO;
  23. }
  24. /**
  25. * 这个每一条数据解析都会来调用
  26. *
  27. * @param data one row value. It is same as {@link AnalysisContext#readRowHolder()}
  28. * @param context
  29. */
  30. @Override
  31. public void invoke(ExtraData data, AnalysisContext context) {
  32. LOGGER.info("解析到一条数据:{}", JSON.toJSONString(data));
  33. list.add(data);
  34. // 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
  35. if(list.size() >= BATCH_COUNT) {
  36. saveData();
  37. // 存储完成清理 list
  38. list.clear();
  39. }
  40. }
  41. @Override
  42. public void extra(CellExtra extra, AnalysisContext context) {
  43. LOGGER.info("读取到了一条额外信息:{}", JSON.toJSONString(extra));
  44. switch(extra.getType()) {
  45. case COMMENT:
  46. LOGGER.info("额外信息是批注,在rowIndex:{},columnIndex;{},内容是:{}", extra.getRowIndex(), extra.getColumnIndex()
  47. , extra.getText());
  48. break;
  49. case HYPERLINK:
  50. if("Sheet1!A1".equals(extra.getText())) {
  51. LOGGER.info("额外信息是超链接,在rowIndex:{},columnIndex;{},内容是:{}", extra.getRowIndex(),
  52. extra.getColumnIndex(), extra.getText());
  53. } else if("Sheet2!A1".equals(extra.getText())) {
  54. LOGGER.info("额外信息是超链接,而且覆盖了一个区间,在firstRowIndex:{},firstColumnIndex;{},lastRowIndex:{}," +
  55. "lastColumnIndex:{}," + "内容是:{}", extra.getFirstRowIndex(),
  56. extra.getFirstColumnIndex(), extra.getLastRowIndex(), extra.getLastColumnIndex(),
  57. extra.getText());
  58. } else {
  59. LOGGER.error("Unknown hyperlink!");
  60. }
  61. break;
  62. case MERGE:
  63. LOGGER.info("额外信息是超链接,而且覆盖了一个区间,在firstRowIndex:{},firstColumnIndex;{},lastRowIndex:{}," +
  64. "lastColumnIndex:{}", extra.getFirstRowIndex(), extra.getFirstColumnIndex(),
  65. extra.getLastRowIndex(), extra.getLastColumnIndex());
  66. break;
  67. default:
  68. }
  69. }
  70. /**
  71. * 所有数据解析完成了 都会来调用
  72. *
  73. * @param context
  74. */
  75. @Override
  76. public void doAfterAllAnalysed(AnalysisContext context) {
  77. // 这里也要保存数据,确保最后遗留的数据也存储到数据库
  78. saveData();
  79. LOGGER.info("所有数据解析完成!");
  80. }
  81. /**
  82. * 加上存储数据库
  83. */
  84. private void saveData() {
  85. LOGGER.info("{}条数据,开始存储数据库!", list.size());
  86. demoDAO.save(list);
  87. LOGGER.info("存储数据库成功!");
  88. }
  89. }

读取操作的时候需要进行显示的指定想要读取哪些额外的内容

  1. /**
  2. * 读取excel其他的额外内容,批注,合并单元格,超链接等等
  3. * 监听器中重写extra方法即可
  4. */
  5. private static void extraRead(String path) {
  6. EasyExcel.read(path, ExtraData.class, new ExtraDataListener())
  7. //需要显示指定读取的额外内容
  8. //批注
  9. .extraRead(CellExtraTypeEnum.COMMENT)
  10. //单元格信息
  11. .extraRead(CellExtraTypeEnum.MERGE)
  12. //超链接
  13. .extraRead(CellExtraTypeEnum.HYPERLINK).doReadAll();
  14. }

读取单元格类型和公式

可能会用到公式进行计算产生结果

使用CellData进行接收数据,指定数据类型进行接收接口

核心代码

  1. @Data
  2. public class CellReadData {
  3. private CellData<String> strLabel;
  4. //虽然制定了Date类型,但是实际上excel存储的是number类型的值
  5. private CellData<Date> dateLabel;
  6. private CellData<Double> doubleData;
  7. private CellData<String> formulaValue;
  8. }

测试代码

  1. /**
  2. * 读取单元格类型和公式
  3. *
  4. * @param path
  5. */
  6. private static void cellReadDataRead(String path) {
  7. EasyExcel.read(path, CellReadData.class, new CellReadDataListener()).sheet(1).doRead();
  8. }

数据读取,数据转换异常处理

当我们使用了错误的数据类型进行接收excel的列时,就会导致异常的出现,我们可以指定异常处理,跳过该列,继续读取剩下的列数据

核心代码

重写监听器中的onException方法,在里面执行相关的业务逻辑

  1. /**
  2. * @author xupu
  3. * @Description 数据监听器
  4. * @Date 2021-08-27 11:43
  5. */
  6. // 有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
  7. public class ExceptionDataListener extends AnalysisEventListener<ExceptionData> {
  8. private static final Logger LOGGER = LoggerFactory.getLogger(ExceptionDataListener.class);
  9. /**
  10. * 每隔5条存储数据库,实际使用中可以3000条,然后清理list ,方便内存回收
  11. */
  12. private static final int BATCH_COUNT = 5;
  13. /**
  14. * 假设这个是一个DAO,当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。
  15. */
  16. private final DemoDAO demoDAO;
  17. List<ExceptionData> list = new ArrayList<>();
  18. public ExceptionDataListener() {
  19. // 这里是demo,所以随便new一个。实际使用如果到了spring,请使用下面的有参构造函数
  20. demoDAO = new DemoDAO();
  21. }
  22. /**
  23. * 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来
  24. *
  25. * @param demoDAO
  26. */
  27. public ExceptionDataListener(DemoDAO demoDAO) {
  28. this.demoDAO = demoDAO;
  29. }
  30. /**
  31. * 这个每一条数据解析都会来调用
  32. *
  33. * @param data one row value. It is same as {@link AnalysisContext#readRowHolder()}
  34. * @param context
  35. */
  36. @Override
  37. public void invoke(ExceptionData data, AnalysisContext context) {
  38. LOGGER.info("解析到一条数据:{}", JSON.toJSONString(data));
  39. list.add(data);
  40. // 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
  41. if(list.size() >= BATCH_COUNT) {
  42. saveData();
  43. // 存储完成清理 list
  44. list.clear();
  45. }
  46. }
  47. /**
  48. * 在转换异常 获取其他异常下会调用本接口。抛出异常则停止读取。如果这里不抛出异常则 继续读取下一行。
  49. *
  50. * @param exception
  51. * @param context
  52. *
  53. * @throws Exception
  54. */
  55. @Override
  56. public void onException(Exception exception, AnalysisContext context) {
  57. LOGGER.error("解析失败,但是继续解析下一行:{}", exception.getMessage());
  58. // 如果是某一个单元格的转换异常 能获取到具体行号
  59. // 如果要获取头的信息 配合invokeHeadMap使用
  60. if(exception instanceof ExcelDataConvertException) {
  61. ExcelDataConvertException excelDataConvertException = (ExcelDataConvertException) exception;
  62. LOGGER.error("第{}行,第{}列解析异常", excelDataConvertException.getRowIndex(),
  63. excelDataConvertException.getColumnIndex());
  64. }
  65. }
  66. /**
  67. * 所有数据解析完成了 都会来调用
  68. *
  69. * @param context
  70. */
  71. @Override
  72. public void doAfterAllAnalysed(AnalysisContext context) {
  73. // 这里也要保存数据,确保最后遗留的数据也存储到数据库
  74. saveData();
  75. LOGGER.info("doAfterAllAnalysed!");
  76. LOGGER.info("所有数据解析完成!");
  77. }
  78. /**
  79. * 加上存储数据库
  80. */
  81. private void saveData() {
  82. LOGGER.info("{}条数据,开始存储数据库!", list.size());
  83. demoDAO.save(list);
  84. LOGGER.info("存储数据库成功!");
  85. }
  86. }

读取表头信息(读取列名)

核心代码
重写监听器中的invokeHeadMap方法,读取表格中的头部数据

  1. /**
  2. * 读取多行表头
  3. * 监听器中重写invokeHeadMap方法
  4. *
  5. * @param path
  6. */
  7. public static void headerRead(String path) {
  8. //指定已知实体类读取列名
  9. //EasyExcel.read(path, SimpleData.class, new SimpleDataListener()).sheet().doRead();
  10. //未知实体,可以直接读取
  11. EasyExcel.read(path, new NoModelDataListener()).sheet().doRead();
  12. }

监听器

  1. public class NoModelDataListener extends AnalysisEventListener<Map<Integer, String>> {
  2. private static final Logger LOGGER = LoggerFactory.getLogger(NoModelDataListener.class);
  3. /**
  4. * 每隔5条存储数据库,实际使用中可以3000条,然后清理list ,方便内存回收
  5. */
  6. private static final int BATCH_COUNT = 5;
  7. List<Map<Integer, String>> list = new ArrayList<Map<Integer, String>>();
  8. @Override
  9. public void invoke(Map<Integer, String> data, AnalysisContext context) {
  10. LOGGER.info("解析到一条数据:{}", JSON.toJSONString(data));
  11. list.add(data);
  12. if(list.size() >= BATCH_COUNT) {
  13. saveData();
  14. list.clear();
  15. }
  16. }
  17. @Override
  18. public void doAfterAllAnalysed(AnalysisContext context) {
  19. saveData();
  20. LOGGER.info("所有数据解析完成!");
  21. }
  22. @Override
  23. public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
  24. LOGGER.info("解析到一条头数据:{}", JSON.toJSONString(headMap));
  25. }

不创建对象进行读取(实用)

在一些通用的情况下,我们可能不会清楚的知道excel的每一个列是什么,这个时候我们可以使用Map来读取excel的列数据

核心代码

  1. @Slf4j
  2. public class NoModelDataListener extends AnalysisEventListener<Map<Integer, String>> {
  3. /**
  4. * 每隔5条存储数据库,实际使用中可以3000条,然后清理list ,方便内存回收
  5. */
  6. private static final int BATCH_COUNT = 3000;
  7. List<Map<Integer, String>> list = new ArrayList<>();
  8. @Override
  9. public void invoke(Map<Integer, String> data, AnalysisContext context) {
  10. log.info("解析到一条数据:{}", JSON.toJSONString(data));
  11. list.add(data);
  12. if (list.size() >= BATCH_COUNT) {
  13. saveData();
  14. list.clear();
  15. }
  16. }
  17. @Override
  18. public void doAfterAllAnalysed(AnalysisContext context) {
  19. saveData();
  20. log.info("所有数据解析完成!");
  21. }
  22. @Override
  23. public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
  24. log.info("解析到一条头数据:{}", JSON.toJSONString(headMap));
  25. }
  26. /**
  27. * 加上存储数据库
  28. */
  29. private void saveData() {
  30. log.info("{}条数据,开始存储数据库!", list.size());
  31. log.info("存储数据库成功!");
  32. }
  33. }

测试代码

  1. /**
  2. * 通用读取,不创建对象进行读取,
  3. * 将监听器中的监听具体对象改为Map
  4. *
  5. * @param path
  6. */
  7. private static void noModelDataRead(String path) {
  8. //不再要求声明相对应的实体
  9. EasyExcel.read(path, new NoModelDataListener()).sheet().doRead();
  10. }

可以搭配读取表头信息方法进行获取每一列的列名

读取web接口上传的文件

当我们需要使用到上传文件的读取方法时,可以直接传递上传的文件的字节码进行解析获取

  1. /**
  2. * 读取通过web接口上传的文件
  3. *
  4. * @param file
  5. */
  6. private static void webDataRead(MultipartFile file) {
  7. try {
  8. EasyExcel.read(file.getInputStream(), UploadData.class, new UploadDataListener()).sheet().doRead();
  9. } catch(IOException e) {
  10. e.printStackTrace();
  11. }
  12. }

可以结合不创建对象进行读取一起使用