基础学习
准备excel文件如下:
SimpleData
@Datapublic class SimpleData {private String strLabel;private String dateLabel;private int intLabel;private double doubleLabel;}
监听器
为该文件创建一个监听类
// 有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去public class DemoDataListener extends AnalysisEventListener<DemoData> {private static final Logger LOGGER = LoggerFactory.getLogger(DemoDataListener.class);/*** 每隔5条存储数据库,实际使用中可以3000条,然后清理list ,方便内存回收*/private static final int BATCH_COUNT = 5;List<DemoData> list = new ArrayList<>();/*** 假设这个是一个DAO,当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。*/private DemoDAO demoDAO;public DemoDataListener() {// 这里是demo,所以随便new一个。实际使用如果到了spring,请使用下面的有参构造函数demoDAO = new DemoDAO();}/*** 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来** @param demoDAO*/public DemoDataListener(DemoDAO demoDAO) {this.demoDAO = demoDAO;}/*** 这个每一条数据解析都会来调用** @param data one row value. Is is same as {@link AnalysisContext#readRowHolder()}* @param context*/@Overridepublic void invoke(DemoData data, AnalysisContext context) {LOGGER.info("解析到一条数据:{}", JSON.toJSONString(data));list.add(data);// 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOMif (list.size() >= BATCH_COUNT) {saveData();// 存储完成清理 listlist.clear();}}/*** 所有数据解析完成了 都会来调用** @param context*/@Overridepublic void doAfterAllAnalysed(AnalysisContext context) {// 这里也要保存数据,确保最后遗留的数据也存储到数据库saveData();LOGGER.info("所有数据解析完成!");}/*** 加上存储数据库*/private void saveData() {LOGGER.info("{}条数据,开始存储数据库!", list.size());demoDAO.save(list);LOGGER.info("存储数据库成功!");}}
业务代码
public class DemoDAO {public void save(List<DemoData> list) {System.out.println("存储成功");}}
测试代码
public class SimpleRead {public static void main(String[] args) throws IOException {ClassPathResource resource = new ClassPathResource("demo/demo.xlsx");String absolutePath = resource.getFile().getAbsolutePath();ExcelReader excelReader = null;try {excelReader = EasyExcel.read(absolutePath, DemoData.class, new DemoDataListener()).build();ReadSheet readSheet = EasyExcel.readSheet(0).build();excelReader.read(readSheet);} finally {assert excelReader != null;excelReader.finish();}}}
运行结果

这种方式一定严格遵循excel表中列的数据类型进行设计实体类的数据类型,因为程序会进行自上而下的获取列,如果顺序错误,可能会导致数据类型转换异常
指定列名查询或者下标查询
IndexOrNameData
@Datapublic class IndexOrNameData {@ExcelProperty("字符串")private String strLabel;@ExcelProperty("日期")private String dateLabel;@ExcelProperty("数字")private int intLabel;@ExcelProperty("小数")private double doubleLabel;}//或者@Datapublic class IndexOrNameData {@ExcelProperty(index = 0)private String strLabel;@ExcelProperty(index = 2)private String dateLabel;@ExcelProperty(index = 3)private int intLabel;@ExcelProperty(index = 1)private double doubleLabel;}
注意对于@ExcelProperty注解,要么使用index进行标记列的位置,下标从0 开始,要么使用name值进行匹配列
一般推荐使用name进行匹配,不推荐两个混合使用,
如果name重复,后面的列会覆盖前面的列,导致只能获取到一列的值
使用指定之后只要指定正确即可,不用太在意实体类属性的声明顺序
读取多个Sheet
核心业务代码
public static void readMoreSheets(String path) {ExcelReader excelReader = null;try {//读取全部sheetEasyExcel.read(path, SimpleData.class, new SimpleDataListener()).doReadAll();//读取部分sheetexcelReader = EasyExcel.read(path).build();//不同的sheet必须使用不同的listenerReadSheet readSheet1 = EasyExcel.readSheet(0).head(SimpleData.class).registerReadListener(new SimpleDataListener()).build();ReadSheet readSheet2 = EasyExcel.readSheet(1).head(IndexOrNameData.class).registerReadListener(new IndexOrNameDataListener()).build();//必须将多个sheet传入excelReader.read(readSheet1,readSheet2);} finally {assert excelReader != null;excelReader.finish();}}
核心步骤如下:
- 生成ExcelReader,先读取全部的sheets
- 单独读取每一个sheet,并为之分配对应的实体类和监听器
- 将多个读取的sheet传入ExcelReader

但是读取sheet会时如果是03版本的excel会重复读取多次
日期、数字或者自定义格式转换
全局注册转换器
在进行读取或者写入操作 的时候进行注册
public static void convertExcel(String path) {EasyExcel.read(path, ConvertData.class, new ConvertDataListener())//可以注册转换器,但是这种注册方式是全局的,可以对所有列进行转换//如果只需要转换一列的话,使用@ExcelProperty(converter="转换器.class").registerConverter(new CustomStringConvert()).sheet().doRead();}
局部使用转换器
在对应的实体类中进行声明
实体
@Datapublic class ConvertData {@ExcelProperty(index = 0, converter = CustomStringConvert.class)private String strLabel;@ExcelProperty(index = 2)@DateTimeFormat("yy-MM-dd HH:mm:ss")private String dateLabel;@ExcelProperty(index = 3)private int intLabel;@ExcelProperty(index = 1)@NumberFormat("#.##%")private double doubleLabel;}
测试代码
public static void convertExcel(String path) {EasyExcel.read(path, ConvertData.class, new ConvertDataListener())//可以注册转换器,但是这种注册方式是全局的,可以对所有列进行转换//如果只需要转换一列的话,使用@ExcelProperty(converter="转换器.class")//.registerConverter(new CustomStringConvert()).sheet().doRead();}
超链接、批注、合并单元格读取
在相对应的监听器中重写extra方法即可
为额外的数据读取创建实体,或者使用map
@Datapublic class ExtraData {private String row1;private String row2;private String row3;}
监听器重写方法
public class ExtraDataListener extends AnalysisEventListener<ExtraData> {private static final Logger LOGGER = LoggerFactory.getLogger(ExtraDataListener.class);/*** 每隔5条存储数据库,实际使用中可以3000条,然后清理list ,方便内存回收*/private static final int BATCH_COUNT = 5;/*** 假设这个是一个DAO,当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。*/private final DemoDAO demoDAO;List<ExtraData> list = new ArrayList<>();public ExtraDataListener() {// 这里是demo,所以随便new一个。实际使用如果到了spring,请使用下面的有参构造函数demoDAO = new DemoDAO();}/*** 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来** @param demoDAO*/public ExtraDataListener(DemoDAO demoDAO) {this.demoDAO = demoDAO;}/*** 这个每一条数据解析都会来调用** @param data one row value. It is same as {@link AnalysisContext#readRowHolder()}* @param context*/@Overridepublic void invoke(ExtraData data, AnalysisContext context) {LOGGER.info("解析到一条数据:{}", JSON.toJSONString(data));list.add(data);// 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOMif(list.size() >= BATCH_COUNT) {saveData();// 存储完成清理 listlist.clear();}}@Overridepublic void extra(CellExtra extra, AnalysisContext context) {LOGGER.info("读取到了一条额外信息:{}", JSON.toJSONString(extra));switch(extra.getType()) {case COMMENT:LOGGER.info("额外信息是批注,在rowIndex:{},columnIndex;{},内容是:{}", extra.getRowIndex(), extra.getColumnIndex(), extra.getText());break;case HYPERLINK:if("Sheet1!A1".equals(extra.getText())) {LOGGER.info("额外信息是超链接,在rowIndex:{},columnIndex;{},内容是:{}", extra.getRowIndex(),extra.getColumnIndex(), extra.getText());} else if("Sheet2!A1".equals(extra.getText())) {LOGGER.info("额外信息是超链接,而且覆盖了一个区间,在firstRowIndex:{},firstColumnIndex;{},lastRowIndex:{}," +"lastColumnIndex:{}," + "内容是:{}", extra.getFirstRowIndex(),extra.getFirstColumnIndex(), extra.getLastRowIndex(), extra.getLastColumnIndex(),extra.getText());} else {LOGGER.error("Unknown hyperlink!");}break;case MERGE:LOGGER.info("额外信息是超链接,而且覆盖了一个区间,在firstRowIndex:{},firstColumnIndex;{},lastRowIndex:{}," +"lastColumnIndex:{}", extra.getFirstRowIndex(), extra.getFirstColumnIndex(),extra.getLastRowIndex(), extra.getLastColumnIndex());break;default:}}/*** 所有数据解析完成了 都会来调用** @param context*/@Overridepublic void doAfterAllAnalysed(AnalysisContext context) {// 这里也要保存数据,确保最后遗留的数据也存储到数据库saveData();LOGGER.info("所有数据解析完成!");}/*** 加上存储数据库*/private void saveData() {LOGGER.info("{}条数据,开始存储数据库!", list.size());demoDAO.save(list);LOGGER.info("存储数据库成功!");}}
读取操作的时候需要进行显示的指定想要读取哪些额外的内容
/*** 读取excel其他的额外内容,批注,合并单元格,超链接等等* 监听器中重写extra方法即可*/private static void extraRead(String path) {EasyExcel.read(path, ExtraData.class, new ExtraDataListener())//需要显示指定读取的额外内容//批注.extraRead(CellExtraTypeEnum.COMMENT)//单元格信息.extraRead(CellExtraTypeEnum.MERGE)//超链接.extraRead(CellExtraTypeEnum.HYPERLINK).doReadAll();}
读取单元格类型和公式
可能会用到公式进行计算产生结果
核心代码
@Datapublic class CellReadData {private CellData<String> strLabel;//虽然制定了Date类型,但是实际上excel存储的是number类型的值private CellData<Date> dateLabel;private CellData<Double> doubleData;private CellData<String> formulaValue;}
测试代码
/*** 读取单元格类型和公式** @param path*/private static void cellReadDataRead(String path) {EasyExcel.read(path, CellReadData.class, new CellReadDataListener()).sheet(1).doRead();}
数据读取,数据转换异常处理
当我们使用了错误的数据类型进行接收excel的列时,就会导致异常的出现,我们可以指定异常处理,跳过该列,继续读取剩下的列数据
核心代码
重写监听器中的onException方法,在里面执行相关的业务逻辑
/*** @author xupu* @Description 数据监听器* @Date 2021-08-27 11:43*/// 有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去public class ExceptionDataListener extends AnalysisEventListener<ExceptionData> {private static final Logger LOGGER = LoggerFactory.getLogger(ExceptionDataListener.class);/*** 每隔5条存储数据库,实际使用中可以3000条,然后清理list ,方便内存回收*/private static final int BATCH_COUNT = 5;/*** 假设这个是一个DAO,当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。*/private final DemoDAO demoDAO;List<ExceptionData> list = new ArrayList<>();public ExceptionDataListener() {// 这里是demo,所以随便new一个。实际使用如果到了spring,请使用下面的有参构造函数demoDAO = new DemoDAO();}/*** 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来** @param demoDAO*/public ExceptionDataListener(DemoDAO demoDAO) {this.demoDAO = demoDAO;}/*** 这个每一条数据解析都会来调用** @param data one row value. It is same as {@link AnalysisContext#readRowHolder()}* @param context*/@Overridepublic void invoke(ExceptionData data, AnalysisContext context) {LOGGER.info("解析到一条数据:{}", JSON.toJSONString(data));list.add(data);// 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOMif(list.size() >= BATCH_COUNT) {saveData();// 存储完成清理 listlist.clear();}}/*** 在转换异常 获取其他异常下会调用本接口。抛出异常则停止读取。如果这里不抛出异常则 继续读取下一行。** @param exception* @param context** @throws Exception*/@Overridepublic void onException(Exception exception, AnalysisContext context) {LOGGER.error("解析失败,但是继续解析下一行:{}", exception.getMessage());// 如果是某一个单元格的转换异常 能获取到具体行号// 如果要获取头的信息 配合invokeHeadMap使用if(exception instanceof ExcelDataConvertException) {ExcelDataConvertException excelDataConvertException = (ExcelDataConvertException) exception;LOGGER.error("第{}行,第{}列解析异常", excelDataConvertException.getRowIndex(),excelDataConvertException.getColumnIndex());}}/*** 所有数据解析完成了 都会来调用** @param context*/@Overridepublic void doAfterAllAnalysed(AnalysisContext context) {// 这里也要保存数据,确保最后遗留的数据也存储到数据库saveData();LOGGER.info("doAfterAllAnalysed!");LOGGER.info("所有数据解析完成!");}/*** 加上存储数据库*/private void saveData() {LOGGER.info("{}条数据,开始存储数据库!", list.size());demoDAO.save(list);LOGGER.info("存储数据库成功!");}}
读取表头信息(读取列名)
核心代码
重写监听器中的invokeHeadMap方法,读取表格中的头部数据
/*** 读取多行表头* 监听器中重写invokeHeadMap方法** @param path*/public static void headerRead(String path) {//指定已知实体类读取列名//EasyExcel.read(path, SimpleData.class, new SimpleDataListener()).sheet().doRead();//未知实体,可以直接读取EasyExcel.read(path, new NoModelDataListener()).sheet().doRead();}
监听器
public class NoModelDataListener extends AnalysisEventListener<Map<Integer, String>> {private static final Logger LOGGER = LoggerFactory.getLogger(NoModelDataListener.class);/*** 每隔5条存储数据库,实际使用中可以3000条,然后清理list ,方便内存回收*/private static final int BATCH_COUNT = 5;List<Map<Integer, String>> list = new ArrayList<Map<Integer, String>>();@Overridepublic void invoke(Map<Integer, String> data, AnalysisContext context) {LOGGER.info("解析到一条数据:{}", JSON.toJSONString(data));list.add(data);if(list.size() >= BATCH_COUNT) {saveData();list.clear();}}@Overridepublic void doAfterAllAnalysed(AnalysisContext context) {saveData();LOGGER.info("所有数据解析完成!");}@Overridepublic void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {LOGGER.info("解析到一条头数据:{}", JSON.toJSONString(headMap));}
不创建对象进行读取(实用)
在一些通用的情况下,我们可能不会清楚的知道excel的每一个列是什么,这个时候我们可以使用Map来读取excel的列数据
核心代码
@Slf4jpublic class NoModelDataListener extends AnalysisEventListener<Map<Integer, String>> {/*** 每隔5条存储数据库,实际使用中可以3000条,然后清理list ,方便内存回收*/private static final int BATCH_COUNT = 3000;List<Map<Integer, String>> list = new ArrayList<>();@Overridepublic void invoke(Map<Integer, String> data, AnalysisContext context) {log.info("解析到一条数据:{}", JSON.toJSONString(data));list.add(data);if (list.size() >= BATCH_COUNT) {saveData();list.clear();}}@Overridepublic void doAfterAllAnalysed(AnalysisContext context) {saveData();log.info("所有数据解析完成!");}@Overridepublic void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {log.info("解析到一条头数据:{}", JSON.toJSONString(headMap));}/*** 加上存储数据库*/private void saveData() {log.info("{}条数据,开始存储数据库!", list.size());log.info("存储数据库成功!");}}
测试代码
/*** 通用读取,不创建对象进行读取,* 将监听器中的监听具体对象改为Map** @param path*/private static void noModelDataRead(String path) {//不再要求声明相对应的实体EasyExcel.read(path, new NoModelDataListener()).sheet().doRead();}
可以搭配读取表头信息方法进行获取每一列的列名
读取web接口上传的文件
当我们需要使用到上传文件的读取方法时,可以直接传递上传的文件的字节码进行解析获取
/*** 读取通过web接口上传的文件** @param file*/private static void webDataRead(MultipartFile file) {try {EasyExcel.read(file.getInputStream(), UploadData.class, new UploadDataListener()).sheet().doRead();} catch(IOException e) {e.printStackTrace();}}
可以结合不创建对象进行读取一起使用
