前言

在我们的项目需求中,经常会遇到导出的需求,其中excel的导出最为常见。生成Excel比较有名的框架有Apache poi,jxl等,但他们都存在一个严重的问题就是非常的耗内存,如果你的系统并发量不大的话可能还行,但是一旦并发上来后一定会OOM或者JVM频繁的full gc.

一、EasyExcel特点

EasyExcel是阿里巴巴开源的一个excel处理框架,以使用简单,节省内存著称,
64M内存1分钟内读取75M(46W行25列)的Excel(当然还有急速模式能更快,但是内存占用会在100M多一点)。
EasyExcel能大大减少占用内存的主要原因是在解析Excel时没有将文件数据一次性全部加载到内存中,而是从磁盘上一行行读取数据,逐个解析。

不支持的功能

1、单个文件的并发写入、
2、读取读取图片
3、宏
4、csv读取(这个后续可能会考虑)

三、常见问题

1、读取文件务必使用2.0.5+(现在项目中用的是2.2.10)
2、读写反射对象用到了Cglib动态代理,所以成员变量必须符合驼峰规范,而且使用@Data不能使用@Accessors(chain = true)。后续会考虑支持非驼峰。
3、出现 NoSuchMethodException, ClassNotFoundException, NoClassDefFoundError。极大概率是jar冲突,建议clean项目,或者统一poi 的版本,理论上来说easyexcel兼容poi的3.17,4.0.1,4.1.0所有较新版本
4、用String去接收数字,出现小数点等情况这个是BUG,但是很难修复,后续版本会修复这个问题。目前请使用@NumberFormat注解,里面的参数就是调用了java自带的NumberFormat.format方法,不知道怎么入参的可以自己网上查询。
easyExcel的官方文档地址:https://alibaba-easyexcel.github.io/index.html

四、常用注解

4-1、读

ExcelProperty 指定当前字段对应excel中的那一列。可以根据名字或者Index去匹配。当然也可以不写,默认第一个字段就是index=0,以此类推。千万注意,要么全部不写,要么全部用index,要么全部用名字去匹配。千万别三个混着用,除非你非常了解源代码中三个混着用怎么去排序的。

@Getter@Setter@EqualsAndHashCode public class IndexOrNameData{//强制读取第三个这里不建议 index 和 name 同时用,要么一个对象只用index,要么一个对象只用name去山配*@ExcelProperty(index = 2)private Double doubleData/*用名字去匹配,这里需要注意,如果名字重复会导致只有一个字殷读取到数据@ExcelProperty ("字符串标题”) */private Stringstring;@ExcelProperty ("日期标题") private Date date;}

ExcelIgnore 默认所有字段都会和excel去匹配,加了这个注解会忽略该字段

//强制读取第三个这里不建议 index 和 name 同时用,要么一个对象只用index,要么一个对象只用name去山配*@ExcelIgnoreprivate Double doubleData

DateTimeFormat 日期转换,用String去接收excel日期格式的数据会调用这个注解。里面的value参照java.text.SimpleDateFormat。

@DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒")private String date;

NumberFormat 数字转换,用String去接收excel数字格式的数据会调用这个注解。里面的value参照java.text.DecimalFormat。

@NumberFormat("#.##%") private String doubleData; //接收百比的数字

4-2、写

ExcelProperty index 指定写到第几列,默认根据成员变量排序。value指定写入的名称,默认成员变量的名字,多个value可以参照快速开始中的复杂头
ExcelIgnore 默认所有字段都会写入excel,这个注解会忽略这个字段
DateTimeFormat 日期转换,将Date写到excel会调用这个注解。里面的value参照java.text.SimpleDateFormat
NumberFormat 数字转换,用Number写excel会调用这个注解。里面的value参照java.text.DecimalFormat
ExcelIgnoreUnannotated 默认不加ExcelProperty 的注解的都会参与读写,加了不会参与

五、EasyExcel的使用

1、依赖

<dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>2.2.10</version></dependency>

2、读excel

2.1最简单的

对象

@Datapublic class DemoData {private String string;private Date date;private Double doubleData;}

controller类

@PostMapping("/outstoragcExce1")@Apioperation("读取出库excel表")public DeviceResponse storageservice(@RequestBody MultipartFile file) {try{storageservice.storageservice(file);} catch (Exception e){return new DeviceResponse(Constant.FAIL CODE,"出库导失败");}return new DeviceResponse(Constant.SUCCESS CODE,"出库导入成功");}

service实现类

@Autowiredprivate StorageService storageServicel@overridepublil void slorageservice(MulliparlFile file) {Tnnutstream is = null:try{ is=file.getInputstream();} catch (IDException e){ e.printstackTrace();}//1.进行读取数数据,slorageReLrieval是我的puju类,//2.new Soragelistenpr(storagpServire)这个是监听器,主要用来i取数据的,别急后面会讲//3.特别注意的是storageservice这个service,我上面有注入进去 @Autowired,切记不要new会报错EasyExcel.read(is,StorageRetrieval.class, new Soragelisterer(storageService))sheet().doRead();}

SorageListener监听器

@Componentpublic class SorageListener extends AnalysisEventListener<pojo类> {private static final Logger LOGGER = LoggerFactory.getLogger(SorageListener.class);//读取数据初始化值private static final int BATCH_COUNT = 50;List<pojo类> list = new ArrayList<pojo类>();private StorageService storageService;public SorageListener() {storageService=new StorageServiceImpl();} public SorageListener(StorageService storageService) {this.storageService = storageService;}/** * 这个每一条数据解析都会来调用,数据是一条一条进行解析的 * * @param dataone row value. Is is same as {@link AnalysisContext#readRowHolder()} * @param context */@Overridepublic void invoke(StorageRetrieval data, AnalysisContext context) {list.add(data);// 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOMif (list.size() >= BATCH_COUNT) {saveData();// 存储完成清理 listlist.clear();}}/** * 所有excel表中数据解析完成了 都会来调用这个 * 解释为什么要保存数据? *初始化读取数量为50,表中信息已经加载完毕,,假设excel表中最后只剩下30行遗留数据,所以为了防止存在遗留数据 尽量判断下集合是否为空,不为空在进行存储(这是我的逻辑需要判断,如果不需要也可进行不判断) * @param context */@Overridepublic void doAfterAllAnalysed(AnalysisContext context) {if(list.size()==0){ return;}saveData();LOGGER.info("所有数据解析完成!");}/** * 加上存储数据库 */public void saveData() {storageService.save(list); //代码实现类层保存数据LOGGER.info("存储数据库成功!");}}
2.2、指定列的下标或者列名
@Datapublic class IndexOrNameData {/** * 强制读取第三个 这里不建议 index 和 name 同时用,要么一个对象只用index,要么一个对象只用name去匹配 */@ExcelProperty(index = 2)private Double doubleData;/** * 用名字去匹配,这里需要注意,如果名字重复,会导致只有一个字段读取到数据 */@ExcelProperty("字符串标题")private String string;@ExcelProperty("日期标题")private Date date;}/** * 指定列的下标或者列名 * * 

1. 创建excel对应的实体对象,并使用{@link ExcelProperty}注解. 参照{@link IndexOrNameData} *

2. 由于默认一行行的读取excel,所以需要创建excel一行一行的回调监听器,参照{@link IndexOrNameDataListener} *

3. 直接读即可 */@Testpublic void indexOrNameRead() {String fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";// 这里默认读取第一个sheetEasyExcel.read(fileName, IndexOrNameData.class, new IndexOrNameDataListener()).sheet().doRead();}

3、写excel

3.1最简单的


实体对象

@Data@piModel(value = "年龄统计实体类”public class FToWAgeStatisticalVo implements Serializable {private static final long serialVersionUID = -7891558029837989473L;@ApiModelProperty("区间")@ExcelProperty(value = "区间")private String ageGap;@ApiModeLProperty("病例数”)@ExcelProperty(value ="病例数)private Integer casesNumber ;@ApiModeProperty("密接数”)@ExceProperty(value = "密接数”)private Integer closeNumber ;}

service实现

@Overridepublic void avestatisticalExcel(Httpservlethesponse resonse,FlowReionStatisticalParam flowReionStatisticalParam) throws Exception{//这里文件名如果涉及中文一定要使用URL编码,否则会乱码String fileName = URLEncoder.encode( s: "floWAgeStatistical.xlsx" StandardCharsets.UTF_8.toString());List<FloWAgeStatisticalVo> data = ageStatistical(flowRegionStatisticalParam);response.setContentType("application/force-download");response.setcharacterEncoding("utf-8");response.setHeader( s: "Content-Disposition", s1: "attachment;filename=" + fileName);EasyExcel.write(response.getoutputstream()FLoWAgeStatisticalVo.class).autoclosestream(true).exceType(ExcelTypeEnum.XLSX).sheet( sheetName: "年龄统计表").doWrite(data) ;}
3.2、列宽、行高
@Data@ContentRowHeight(10)@HeadRowHeight(20)@ColumnWidth(25)public class WidthAndHeightData {@ExcelProperty("字符串标题")private String string;@ExcelProperty("日期标题")private Date date;/** * 宽度为50 */@ColumnWidth(50)@ExcelProperty("数字标题")private Double doubleData;}
3.3、合并单元格
@Getter@Setter@EqualsAndHashCode// 将第6-7行的2-3列合并成一个单元格// @OnceAbsoluteMerge(firstRowIndex = 5, lastRowIndex = 6, firstColumnIndex = 1, lastColumnIndex = 2)public class DemoMergeData {// 这一列 每隔2行 合并单元格@ContentLoopMerge(eachRow = 2)@ExcelProperty("字符串标题")private String string;@ExcelProperty("日期标题")private Date date;@ExcelProperty("数字标题")private Double doubleData;}
/*** 合并单元格* 

1. 创建excel对应的实体对象 参照{@link DemoData}*

2. 创建一个merge策略 并注册*

3. 直接写即可*/ @Test public void mergeWrite() { String fileName = TestFileUtil.getPath() + "mergeWrite" + System.currentTimeMillis() + ".xlsx"; // 每隔2行会合并 把eachColumn 设置成 3 也就是我们数据的长度,所以就第一列会合并。当然其他合并策略也可以自己写 LoopMergeStrategy loopMergeStrategy = new LoopMergeStrategy(2, 0); // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭 EasyExcel.write(fileName, DemoData.class).registerWriteHandler(loopMergeStrategy).sheet("模板") .doWrite(data()); }

3.4、复杂头写入

@Data@ApiModel("学校学生缺勤信息")public class SchoolAnalyseVo {@ApiModelProperty("学校Id")@ExcelIgnore()private Long schoolId;@ExcelProperty("学校")private String schoolName;@ExcelProperty("学校类型")private String schoolType; ..........@ExcelProperty({"症状", "发热"})private String fever;@ExcelProperty({"症状", "咳嗽"})private String cough;@ExcelProperty({"症状", "头痛"})private String headache;.........@ExcelProperty({"疾病","普通感冒", "人数"})private String commonColdNumber ;@ExcelProperty({"疾病","普通感冒", "因病缺勤率"})private String commonColdRate ;@ExcelProperty({"疾病","流感", "人数"})private String influenzaNumber;.........}
3.5、日期、数字或者自定义格式转换

@Datapublic class ConverterData {/** * 我想所有的 字符串起前面加上"自定义:"三个字 */@ExcelProperty(value = "字符串标题", converter = CustomStringStringConverter.class)private String string;/** * 我想写到excel 用年月日的格式 */@DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒")@ExcelProperty("日期标题")private Date date;/** * 我想写到excel 用百分比表示 */@NumberFormat("#.##%")@ExcelProperty(value = "数字标题")private Double doubleData;}

自定义转换器

public class CustomStringStringConverter implements Converter<String> {@Overridepublic Class supportJavaTypeKey() {return String.class;}@Overridepublic CellDataTypeEnum supportExcelTypeKey() {return CellDataTypeEnum.STRING;}/** * 这里读的时候会调用 * * @param cellData * @param contentProperty * @param globalConfiguration * @return */@Overridepublic String convertToJavaData(CellData cellData, ExcelContentProperty contentProperty,GlobalConfiguration globalConfiguration) {return "自定义:" + cellData.getStringValue();}/** * 这里是写的时候会调用 不用管 * * @param value * @param contentProperty * @param globalConfiguration * @return */@Overridepublic CellData convertToExcelData(String value, ExcelContentProperty contentProperty,GlobalConfiguration globalConfiguration) {return new CellData(value);}}
3.6、指定写入列

@Getter@Setter@EqualsAndHashCodepublic class IndexData {@ExcelProperty(value = "字符串标题", index = 0)private String string;@ExcelProperty(value = "日期标题", index = 1)private Date date;/** * 这里设置3 会导致第二列空的 */@ExcelProperty(value = "数字标题", index = 3)private Double doubleData;}
3.7、其他读操作

https://www.yuque.com/easyexcel/doc/write

4、填充excel

4.1 最简单的填充


对象

@Getter@Setter@EqualsAndHashCodepublic class FillData{private string name;private double number;private Date date;}

代码

/*最简单的填充* @since 2.1.1*/@Testpublic void simpleFill() [// 模板注 用]来表示你要用的变量 如果本来就有””,”]”特殊字符 用””]"代替String templateFileName =TestFileUtil.getPath() + "demo" + File.separator + "fill" + File.separator + "simple.xlsx";// 方案1 根据对象填充String fileName = TestFileUtil,getPath() + "simpleFill" + System,currentTimeMillis() + ".xlsx",// 这里 会填充到第一个sheet, 然后文件流会自动关闭FillData fillData = new FillData();fillData.setName("张一");fillData.setNumber(5.2);EasyExcel.write(fileName).withTemplate(templateFileName).sheet().doFill(fillData);// 方案2 根据Map填充fileName = TestFileUtil.getPath() + "simpleFill" + System.currentTimeMillis() + ".xlsx"// 这里 会填充到第一个sheet, 然后文件流会自动关闭Map<string, Object> map = new HashMap<string, Object>();map.put("name”,"张二");map .put("number", 5.2) :EasyExcel.write(fileName).withTemplate(templateFileName).sheet().doFill(map);}
4.2、其他填充

填充列表、复杂的填充、数据量大的复杂填充、横向的填充、多列表组合填充填充
https://www.yuque.com/easyexcel/doc/fill

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。