SAX(用于处理XML事件驱动的推模型),全称Simple API for XML,既是一种接口,也是一种软件包。它是一种XML解析的替代方法。SAX不同于DOM解析,它逐行扫描文档,一边扫描一边解析。由于应用程序只是在读取数据时检查数据,因此不需要将数据存储在内存中,这对于大型文档的解析是个巨大优势。
easyexcel框架的使用
二、程序实例
2.1、添加依赖包
<!--Excel导入导出依赖--> <dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>2.2.8</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-nop</artifactId> <version>1.7.2</version> </dependency>
|
2.2、导出Excel
easyExcel的导出支持两种方式,一种是通过实体类注解方式生成文件,另一种是通过动态参数化生成文件。
2.2.1、实体类注解方式生成文件
实体类注解方式生成文件,操作非常简单,只需要在对应的属性字段上添加@ExcelProperty注解,然后填写列名,配置就完成了,示例代码如下:
实体类:
public class UserEntity { @ExcelProperty(value = "姓名") private String name;
@ExcelProperty(value = "年龄") private int age;
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @ExcelProperty(value = "操作时间") private Date time;
//get、set封装字段... }
|
main方法:
public class Text { public static void main(String[] args) throws FileNotFoundException { List<UserEntity> dataList=new ArrayList<>(); for (int i=0;i<10;i++){ UserEntity user=new UserEntity(); user.setName("张三"+i); user.setAge(20+i); user.setTime(new Date(System.currentTimeMillis()+i)); dataList.add(user); } //定义文件输出的位置 FileOutputStream ops=new FileOutputStream(new File("D:/easyexcel.xlsx")); EasyExcel.write(ops,UserEntity.class).sheet("用户信息").doWrite(dataList); } }
|
运行程序,打开文件.显示内容:

2.2.2、动态参数化生成文件
动态参数化生成文件,这种方式用的较多,基于它,可以封装一个公共的导出工具类:
pom.xml配置:
<dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>2.2.8</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-nop</artifactId> <version>1.7.2</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> <!-- https://mvnrepository.com/artifact/com.google.common/google-collect --> <dependency><!--Lists工具类--> <groupId>com.google.common</groupId> <artifactId>google-collect</artifactId> <version>0.5</version> </dependency>
|
main方法:
//动态参数化生成文件 public class ParameterText { public static void main(String[] args) throws FileNotFoundException { //定义表头 List<List<String>> headList=new ArrayList<>(); headList.add(Lists.newArrayList("姓名")); headList.add(Lists.newArrayList("年龄")); headList.add(Lists.newArrayList("操作时间"));
//定义数据体 List<List<Object>> dataList=new ArrayList<>(); for (int i = 0; i < 10; i++) { List<Object> data=new ArrayList<>(); data.add("张三"+i); data.add(20+i); data.add(new Date(System.currentTimeMillis()+i)); dataList.add(data); } //定义文件输出路径 FileOutputStream fops=new FileOutputStream("D:/easyexcel1.xlsx"); EasyExcel.write(fops).head(headList).sheet("信息").doWrite(dataList); } }
|
运行结果,与上面一致:

2.2.3、生成复杂的表头

2.2.3.1、实体类添加注解的方法:
public class UserHeader { @ExcelProperty(value = "班级") private String className;
@ExcelProperty({"学生信息","姓名"}) private String name;
@ExcelProperty({"学生信息","年龄"}) private int age;
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @ExcelProperty({"学生信息","入学时间"}) private Date time; //get和set封装字段... }
|
【其中{"学生信息","姓名"}表示:在当前列,插入多行数据,第一行是学生信息,第二行是姓名,这样就形成多级表头。】
main方法:
//复杂表头//实体注解方法 public class ComplexHeaderNote { public static void main(String[] args) throws FileNotFoundException { List<UserHeader> userList=new ArrayList<>(); for (int i = 0; i < 10; i++) { UserHeader user=new UserHeader(); user.setClassName("一年级~"+i+"班"); user.setName("李四"+i); user.setAge(5+i); user.setTime(new Date(System.currentTimeMillis()+i)); userList.add(user); } //定义文件输出的位置 FileOutputStream ops=new FileOutputStream(new File("D:/easyexcel.xlsx")); EasyExcel.write(ops, UserHeader.class).sheet("复杂表头信息").doWrite(userList); } }
|
2.2.3.2、动态参数化生成复杂表头
代码:
//动态参数化生成复杂表头 public class ComplexHeaderParameter { public static void main(String[] args) throws FileNotFoundException { //定义多级表头 List<List<String>> headList=new ArrayList<>(); headList.add(Lists.newArrayList("班级")); headList.add(Lists.newArrayList("学生信息","姓名")); headList.add(Lists.newArrayList("学生信息","年龄")); headList.add(Lists.newArrayList("学生信息","入学时间")); //定义数据体 List<List<Object>> dataList=new ArrayList<>(); for (int i = 0; i < 10; i++) { List<Object> data=new ArrayList<>(); data.add("一年级~"+i+"班"); data.add("李四"+i); data.add(5+i); data.add(new Date(System.currentTimeMillis()+i)); dataList.add(data); } //定义文件输出位置 FileOutputStream fops=new FileOutputStream(new File("D:/easyexcel2.xlsx")); EasyExcel.write(fops).head(headList).sheet("用户信息").doWrite(dataList); } }
|
2.2.4、自定义样式

1.实体注解方法:
在2.2.3生成复杂表头的基础上进行
编写一个自定义样式的方法,然后在写入的时候注入进去。
customStyle()方法:(放到main方法下面)
/** * 自定义样式 * @return */ private static HorizontalCellStyleStrategy customStyle(){ //表头的策略 WriteCellStyle headWriteCellStyle=new WriteCellStyle(); //背景颜色设置 headWriteCellStyle.setFillForegroundColor(IndexedColors.RED.getIndex());//红色 WriteFont headWriteFont=new WriteFont(); headWriteFont.setFontHeightInPoints((short)20); headWriteCellStyle.setWriteFont(headWriteFont); //内容策略 WriteCellStyle contentWriteCellStyle=new WriteCellStyle(); //需要制定FillPatternType为FillPatternType.SOLID_FOREGROUND,不然无法显示背景颜色 // 表头默认为FillPatternType,所以可以不指定 contentWriteCellStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND); //背景颜色设置 contentWriteCellStyle.setFillForegroundColor(IndexedColors.YELLOW.getIndex());//黄色 WriteFont contentWF=new WriteFont(); //字体大小 contentWF.setFontHeightInPoints((short)20); contentWriteCellStyle.setWriteFont(contentWF);
//水平单元风格策略,表头的样式,内容的样式 HorizontalCellStyleStrategy hcss=new HorizontalCellStyleStrategy(headWriteCellStyle,contentWriteCellStyle); System.out.println(hcss); return hcss; }
|
main()方法中修改:
//定义文件输出的位置
FileOutputStream fops=new FileOutputStream(new File("D:/easyexcel.xlsx"));
//EasyExcel.write(fops, UserHeader.class).sheet("复杂表头信息").doWrite(userList);
//通过registerWriterHandle方法,将自定义的样式类注入进去
EasyExcel.write(fops,UserHeader.class).registerWriteHandler(customStyle()).sheet("用户信息").doWrite(userList);
2.动态参数化方法
编写一个自定义样式的方法:customStyle()方法:(放到main方法下面)
main()方法中修改:
//定义文件输出位置
FileOutputStream fops=new FileOutputStream(new File("D:/easyexcel2.xlsx"));
//EasyExcel.write(fops).head(headList).sheet("用户信息").doWrite(dataList);
//通过registerWriterHandle方法,将自定义的样式类注入进去
EasyExcel.write(fops).registerWriteHandler(customStyle()).head(headList).sheet("用户信息").doWrite(dataList);
2.3、导入Excel
easyexcel 的导入同样也支持两种方式:一种是通过实体类注解方式来读取文件,另一种是通过动态监听器读取文件。
2.3.1、实体类注解方法来读取文件(普通表头读取)
读取的Excel表头需要与实体类一一对应:

实体类:UserReadEntity
通过注解方式来读取,既可以指定列的下表,也可以通过列名来映射,但是两者智能取一个。
//读取实体类 public class UserReadEntity { @ExcelProperty(value = "姓名") private String name;
//强制读取第三个,这里不建议index和name同时用,要么一个对象只用index,要么一个对象只用name去匹配
@ExcelProperty(index = 1) private int age;
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @ExcelProperty(value = "操作时间") private Date time; //get与set方法封装... }
|
pom.xml导入:fastjson
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.76</version> </dependency>
|
main方法:
//读取Excel文件
public class ReadNote {
public static void main(String[] args) throws FileNotFoundException {
//同步读取文件内容
FileInputStream fis=new FileInputStream(new File("D:/easyexcel1.xlsx"));
List<UserReadEntity> list= EasyExcel.read(fis).head(UserReadEntity.class).sheet().doReadSync();
System.out.println(JSON.toJSONString(list));
}
}
运行结果:
[{"age":20,"name":"张三0","time":1618988915000},{"age":21,"name":"张三1","time":1618988915000},{"age":22,"name":"张三2","time":1618988915000},{"age":23,"name":"张三3","time":1618988915000},{"age":24,"name":"张三4","time":1618988915000},{"age":25,"name":"张三5","time":1618988915000},{"age":26,"name":"张三6","time":1618988915000},{"age":27,"name":"张三7","time":1618988915000},{"age":28,"name":"张三8","time":1618988915000},{"age":29,"name":"张三9","time":1618988915000}]
2.3.2、动态监听器读取文件
动态监听器读取文件,与注解的方式有一个明显的区别是重新写一个实现类,来监听easyexcel一行一行解析出来的数据,然后将数据封装出来,基于此,可以编写一套动态的导入工具类,示例代码:
Listener监听器实现类:UserDataListener
//创建监听器,继承自AnalysisEventListener public class UserDataListener extends AnalysisEventListener<Map<Integer,String>> { //日志记录器 private static final Logger log= LoggerFactory.getLogger(UserDataListener.class); //表头数据 private List<Map<Integer,String>> headList=new ArrayList<>(); //数据体 private List<Map<Integer,String>> dataList=new ArrayList<>(); //这里会一行一行的返回头 @Override public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) { log.info("解析到一条头数据{}", JSON.toJSONString(headList)); System.out.println("解析到头数据!"); headList.add(headMap); } //这个每条数据解析都会来调用 @Override public void invoke(Map<Integer, String> data, AnalysisContext context) { log.info("解析到一条数据:{}"+JSON.toJSONString(data)); System.out.println("解析到一条数据:"+ JSON.toJSONString(data)); dataList.add(data); } //所有数据解析完成了,都会来调用 @Override public void doAfterAllAnalysed(AnalysisContext analysisContext) { log.info("所有数据解析完成!"); System.out.println("所有数据解析完成"); } public List<Map<Integer,String>> getHeadList(){ return headList; } public List<Map<Integer,String>> getDataList(){ return dataList; } }
|
main方法:
public class ReadListener {
public static void main(String[] args) throws FileNotFoundException {
FileInputStream inputStream=new FileInputStream(new File("D:/easyexcel1.xlsx"));
//初始化监听器
UserDataListener listener=new UserDataListener();
//读取文件数据
EasyExcel.read(inputStream,listener).sheet().doRead();
System.out.println("表头:"+ JSONArray.toJSONString(listener.getHeadList()));
System.out.println("数据体:"+JSONArray.toJSONString(listener.getDataList()));
}
}
运行输出的结果:

表头:[{0:"姓名",1:"年龄",2:"操作时间"}]
数据体:[{0:"张三0",1:"20",2:"2021-04-21 15:08:35"},{0:"张三1",1:"21",2:"2021-04-21 15:08:35"},{0:"张三2",1:"22",2:"2021-04-21 15:08:35"},{0:"张三3",1:"23",2:"2021-04-21 15:08:35"},{0:"张三4",1:"24",2:"2021-04-21 15:08:35"},{0:"张三5",1:"25",2:"2021-04-21 15:08:35"},{0:"张三6",1:"26",2:"2021-04-21 15:08:35"},{0:"张三7",1:"27",2:"2021-04-21 15:08:35"},{0:"张三8",1:"28",2:"2021-04-21 15:08:35"},{0:"张三9",1:"29",2:"2021-04-21 15:08:35"}]
2.3.3、复杂表头读取

1.如果采用注解的方式导出文件,同样也可以通过注解方式来读取,例如上面是用实体类生成的文件,也可以通过这个类读取文件!
注解实体类:

main方法:
//读取复杂表头的文件 public class ReadComplexHead { public static void main(String[] args) { List<UserHeader> list= EasyExcel.read("D:/easyexcel2.xlsx").head(UserHeader.class).sheet().doReadSync(); System.out.println(JSONArray.toJSONString(list)); } }
|
运行结果:
[{"age":5,"className":"一年级~0班","name":"李四0","time":1619423603000},{"age":6,"className":"一年级~1班","name":"李四1","time":1619423603000},{"age":7,"className":"一年级~2班","name":"李四2","time":1619423603000},{"age":8,"className":"一年级~3班","name":"李四3","time":1619423603000},{"age":9,"className":"一年级~4班","name":"李四4","time":1619423603000},{"age":10,"className":"一年级~5班","name":"李四5","time":1619423603000},{"age":11,"className":"一年级~6班","name":"李四6","time":1619423603000},{"age":12,"className":"一年级~7班","name":"李四7","time":1619423603000},{"age":13,"className":"一年级~8班","name":"李四8","time":1619423603000},{"age":14,"className":"一年级~9班","name":"李四9","time":1619423603000}]
2.如果使用动态参数化生成文件,可以采用动态监听器的方法读取文件,在读取时需要指定所在行,实例代码:
//读取复杂表头文件//动态参数化方法 public class ReadComplexHeadListener { public static void main(String[] args) throws FileNotFoundException { FileInputStream inputStream=new FileInputStream(new File("D:/easyexcel2.xlsx")); //初始化监听器 UserDataListener listener=new UserDataListener(); EasyExcel.read(inputStream,listener).sheet().headRowNumber(2).doRead(); System.out.println("表头:"+ JSONArray.toJSONString(listener.getHeadList())); System.out.println("数据体:"+JSONArray.toJSONString(listener.getDataList())); } }
|
运行结果:

三、动态导出导入工具类封装
在实际开发中,不可能每来一个Excel导入导出需求就编写一个方法,而且很多业务需求都是动态导入导出,没办法基于实体类注解的方法来读取文件或者写入文件。
基于动态参数化生成文件和动态监听器读取文件方法,可以单独封装一套动态导出导入工具类,省的每次重新编写。
/** * 动态导出工具类 */ public class DynamicEasyExcelExportUtils { private static final Logger log= LoggerFactory.getLogger(DynamicEasyExcelExportUtils.class); private static final String DEFAULT_SHEET_NAME="sheet1";
/** * 动态生成导出模板(单表头) * @param headColumns 列名称 * @return excel文件流 */ public static byte[] exportTemplateExcelFile(List<String> headColumns) { List<List<String>> excelHead= Lists.newArrayList(); headColumns.forEach(columnName ->{ excelHead.add(Lists.newArrayList(columnName));}); byte[] stream=createExcelFile(excelHead,new ArrayList<>()); return stream; }
/** * 动态生成模板(复杂表头) * @param excelHead 列名称 * @return */ public static byte[] exportTemplateExcelFileCustomHead(List<List<String>> excelHead){ byte[] stream=createExcelFile(excelHead,new ArrayList<>()); return stream; }
/** * 动态导出文件 * @param headColumnMap 有序列头部 * @param dataList 数据体 * @return */ public static byte[] exportExcelFile(LinkedHashMap<String,String> headColumnMap, List<Map<String,Object>> dataList){ //获取列名称 List<List<String>> excelHead=new ArrayList<>();//获取列名称 if (MapUtils.isNotEmpty(headColumnMap)){ //key为匹配符,value为列名,如果多级列名 headColumnMap.entrySet().forEach(entry -> { excelHead.add(Lists.newArrayList(entry.getValue().split(","))); }); } List<List<Object>> excelRows=new ArrayList<>(); if (MapUtils.isNotEmpty(headColumnMap) && CollectionUtils.isNotEmpty(dataList)){ for (Map<String,Object> dataMap : dataList){ List<Object> rows=new ArrayList<>(); headColumnMap.entrySet().forEach(headColumnEntry ->{ if (dataMap.containsKey(headColumnEntry.getKey())){ Object data=dataMap.get(headColumnEntry.getKey()); rows.add(data); } }); excelRows.add(rows); } } byte[] stream=createExcelFile(excelHead,excelRows); return stream; }
/** * 生成文件 * @param excelHead * @param excelRows * @return */ private static byte[] createExcelFile(List<List<String>> excelHead,List<List<Object>> excelRows){ try{ if (CollectionUtils.isNotEmpty(excelHead)){ ByteArrayOutputStream outputStream=new ByteArrayOutputStream(); EasyExcel.write(outputStream).registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) .head(excelHead) .sheet(DEFAULT_SHEET_NAME) .doWrite(excelRows); return outputStream.toByteArray(); } }catch (Exception e){ log.error("动态生成Excel文件失败,headColumns:"+ JSONArray.toJSONString(excelHead)+",excelRows:"+JSONArray.toJSONString(excelRows),e);
} return null; }
/** * 导出文件测试 * @param args * @throws IOException */ private static void main(String[] args) throws IOException { //导出包含数据内容的文件 LinkedHashMap<String,String> headColumnMap= Maps.newLinkedHashMap(); headColumnMap.put("class","班级"); headColumnMap.put("name","学生信息,姓名"); headColumnMap.put("sex","学生信息,性别"); List<Map<String,Object>> dataList=new ArrayList<>(); for (int i=0;i<5;i++){ Map<String,Object> dataMap=Maps.newHashMap(); dataMap.put("class","一年级"); dataMap.put("name","张三"+i); dataMap.put("sex","男"); dataList.add(dataMap); } byte[] stream=exportExcelFile(headColumnMap,dataList); FileOutputStream outputStream=new FileOutputStream(new File("D:/easyexcel1.xlsx")); outputStream.write(stream); outputStream.close(); }
}
|
/** * 创建一个监听器 * 动态导入工具类 */ public class DynamicEasyExcelListener extends AnalysisEventListener<Map<Integer,String>> { private static final Logger log= LoggerFactory.getLogger(UserDataListener.class); //表头数据(存储所有的表头数据) private List<Map<Integer,String>> headList=new ArrayList<>(); //数据体 private List<Map<Integer,String>> dataList=new ArrayList<>();
public List<Map<Integer,String>> getHeadList(){ return headList; }
public List<Map<Integer,String>> getDataList(){ return dataList; }
/** * 一行行的返回头 * @param headMap * @param context */ @Override public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) { log.info("解析到一条头部数据:{}"+JSON.toJSONString(headMap)); //存储全部表头数据 headList.add(headMap); }
/** * 每一条数据解析都会调用 * @param data * @param context */ @Override public void invoke(Map<Integer, String> data, AnalysisContext context) { log.info("解析到一条数据:{}",JSON.toJSONString(data)); dataList.add(data); }
/** * 所有数据解析完成后都会调用 * @param analysisContext */ @Override public void doAfterAllAnalysed(AnalysisContext analysisContext) { //这里也要保存数据,确保最后遗留的数据也存储到数据库 log.info("所有数据解析完成!"); } }
|
/** * 导入工具类 */ public class DynamicEasyExcelImportUtils { /** * 动态获取全部列和数据体,默认从第一行开始解析数据 * @param stream * @return */ public static List<Map<String,String>> parseExcelToView(byte[] stream){ return parseExcelToView(stream,1); }
/** * 动态获取全部列和数据体 * @param stream Excel文件流 * @param parseRowNumber 指定读取行 * @return */ public static List<Map<String, String>> parseExcelToView(byte[] stream, Integer parseRowNumber) { DynamicEasyExcelListener readListener=new DynamicEasyExcelListener(); EasyExcelFactory.read(new ByteArrayInputStream(stream)).registerReadListener(readListener).headRowNumber(parseRowNumber).sheet(0).doRead(); List<Map<Integer,String>> headList=readListener.getHeadList(); if (CollectionUtils.isEmpty(headList)){ throw new RuntimeException("Excel未包含表头"); } List<Map<Integer,String>> dataList=readListener.getDataList(); if (CollectionUtils.isEmpty(dataList)){ System.out.println("Excel未包含数据!!"); } //获取头部,取最后一次解析的列头数据 Map<Integer,String> excelHeadIdxNameMap=headList.get(headList.size()-1); //封装数据体 List<Map<String,String>> excelDataList= Lists.newArrayList(); for (Map<Integer,String> dataRow : dataList) { Map<String,String> rowData=new LinkedHashMap<>(); excelHeadIdxNameMap.entrySet().forEach(columnHead ->{ rowData.put(columnHead.getValue(),dataRow.get(columnHead.getKey())); }); excelDataList.add(rowData); }
return excelDataList; }
/** * 文件导入测试 * @param args * @throws IOException */ public static void main(String[] args) throws IOException { FileInputStream inputStream=new FileInputStream(new File("D:/easyexcel1.xlsx")); byte[] stream= IOUtils.toByteArray(inputStream); List<Map<String,String>> dataList=parseExcelToView(stream,2); System.out.println(JSONArray.toJSONString(dataList)); inputStream.close(); } }
|
四、总结
以上是对easyexcel的简单使用,对动态导出导入基于业务的需要,做了一个公共的工具类,方便后续的使用!
easyexcel的功能还有很多,还有基于模板进行Excel填充,具体可以参考官方的文档:
https://www.yuque.com/easyexcel/doc/read#1bfaf593