EasyExcel特点
- Java领域解析、生成Excel比较有名的框架有Apache poi、jxl等。但他们都存在一个严重的问题就是非常的耗内存。如果你的系统并发量不大的话可能还行,但是一旦并发上来后一定会OOM或者JVM频繁的full gc。
- EasyExcel是阿里巴巴开源的一个excel处理框架,以使用简单、节省内存著称。EasyExcel能大大减少占用内存的主要原因是在解析Excel时没有将文件数据一次性全部加载到内存中,而是从磁盘上一行行读取数据,逐个解析。
- EasyExcel采用一行一行的解析模式,并将一行的解析结果以观察者的模式通知处理(AnalysisEventListener)。
参考:原先地址:https://alibaba-easyexcel.github.io/
新地址:https://www.yuque.com/easyexcel/doc/easyexcel
pom中引入xml相关依赖
<!--阿里巴巴EasyExcel依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.0.5</version>
</dependency>
一.导出(写)数据(日志导出)
1.创建导出数据实体类RespLogInfo.java
package com.xiaogui.log.vo.resp;
import cn.hutool.core.date.DateTime;
import com.alibaba.excel.annotation.ExcelIgnore;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.format.DateTimeFormat;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.alibaba.excel.annotation.write.style.ContentRowHeight;
import com.alibaba.excel.annotation.write.style.HeadRowHeight;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* 导出实体响应类
*/
@Data
@ContentRowHeight(20)
@ColumnWidth(12)
@HeadRowHeight(20)
public class RespLogInfo {
//@ExcelProperty(value = { "用户信息","用户账号" })
@ExcelProperty(value = {"用户账号" })
@ApiModelProperty(value="用户账号")
private String userId;
//@ExcelProperty(value = {"用户信息","用户名称"})
@ExcelProperty(value = {"用户名称"})
@ApiModelProperty(value="用户名称")
private String userName;
//@ExcelProperty(value = {"其他信息","ip地址"})
@ExcelProperty(value = {"ip地址"})
@ApiModelProperty(value="ip地址")
private String ipAddress;
//@ExcelProperty(value = {"其他信息","操作类型"})
@ExcelProperty(value = {"操作类型"})
@ApiModelProperty(value="操作类型")
private String operationType;
//@ExcelProperty(value = {"内容","内容"})
@ExcelProperty(value = {"内容"})
@ApiModelProperty(value="内容")
private String content;
@ExcelIgnore
@ApiModelProperty(value="操作时间")
private DateTime operationTime;
/**
* 宽度为16,覆盖上面的宽度12
*/
@ColumnWidth(16)
@ExcelProperty(value = "操作时间")
@DateTimeFormat("yyyy-MM-dd")
@ApiModelProperty(value="操作时间")
private String createTime;
}
2.创建合并单元格配置类 BizMergeStrategy.java
package com.xiaogui.log.utils;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.write.handler.CellWriteHandler;
import com.alibaba.excel.write.merge.AbstractMergeStrategy;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;
import java.util.ArrayList;
import java.util.List;
/**
* @author 廾匸
* @version 1.0.0
* @ClassName BizMergeStrategy
* @Description 业务合并单元格逻辑
* @createTime 2022年01月08日
*/
public class BizMergeStrategy extends AbstractMergeStrategy implements CellWriteHandler {
/**
* 合并坐标集合
*/
private List<CellRangeAddress> cellRangeAddress;
private Sheet sheet;
public BizMergeStrategy(List<CellRangeAddress> cellRangeAddress) {
this.cellRangeAddress = cellRangeAddress;
}
public BizMergeStrategy() {
List<CellRangeAddress> listRange = new ArrayList<>();
//合并 单元格坐标
CellRangeAddress item2 = new CellRangeAddress(2, 2, 2, 5);
CellRangeAddress item3 = new CellRangeAddress(3, 3, 0, 1);
listRange.add(item2);
listRange.add(item3);
this.cellRangeAddress = listRange;
}
@Override
protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {
this.sheet = sheet;
if (cellRangeAddress.size() > 0) {
if (cell.getRowIndex() == 1 && cell.getColumnIndex() == 0) {
/**
* 保证每个cell被合并一次,如果不加上面的判断,因为是一个cell一个cell操作的,
* 例如合并A2:A3,当cell为A2时,合并A2,A3,但是当cell为A3时,又是合并A2,A3,
* 但此时A2,A3已经是合并的单元格了
*/
for (CellRangeAddress item : cellRangeAddress) {
// 添加合并请求
sheet.addMergedRegionUnsafe(item);
}
}
}
}
}
3.创建导出类 LogInfoExportController.java
package com.xiaogui.log.controller;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelReader;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.converters.ReadConverterContext;
import com.alibaba.excel.read.metadata.ReadSheet;
import com.alibaba.excel.write.builder.ExcelWriterBuilder;
import com.alibaba.excel.write.builder.ExcelWriterSheetBuilder;
import com.alibaba.excel.write.merge.AbstractMergeStrategy;
import com.alibaba.excel.write.merge.LoopMergeStrategy;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.metadata.style.WriteFont;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
import com.xiaogui.log.service.LogService;
import com.xiaogui.log.utils.BizMergeStrategy;
import com.xiaogui.log.utils.ExcelDataListener;
import com.xiaogui.log.vo.req.ReqLogInfo;
import com.xiaogui.log.vo.resp.RespLogInfo;
import com.xiaogui.log.vo.resp.RespPageResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import static org.apache.poi.ss.usermodel.BorderStyle.THIN;
/**
* @author PC
*/
@Slf4j
@RestController
@RequestMapping("/logExport")
@Api(value = "用户操作日志",tags = "用户操作日志")
public class LogInfoExportController {
@Autowired
private LogService logService;
@Autowired
private ExcelDataListener excelDataListener;
@PostMapping("detailExport")
@ApiOperation(value = "用户操作日志导出", notes = "用户操作日志导出")
public void detailExport(@RequestBody ReqLogInfo req, HttpServletResponse resp) throws IOException {
/*resp.setContentType("application/vnd.ms-excel;charset=UTF-8");
//设置头
resp.setHeader("Pragma", "No-cache");
resp.setHeader("Cache-Control", "no-cache");
resp.setHeader("Content-disposition", "attachment; filename = logInfo.xlsx");*/
HorizontalCellStyleStrategy horizontalCellStyleStrategy = exportExcel(resp, "logInfo");
// 数据库所查询到的数据
RespPageResult result = logService.queryLogInfo(req);
List<RespLogInfo> list = (List<RespLogInfo>)result.getList();
/**
* 多个sheet导出
*/
/*ExcelWriter excelWriter = null;
try {
excelWriter = EasyExcel.write(resp.getOutputStream(), RespLogInfo.class).build();
WriteSheet sheet = EasyExcel.writerSheet(0,"用户操作日志")
.registerWriteHandler(new LoopMergeStrategy(2, 1))
.registerWriteHandler(horizontalCellStyleStrategy)
.build();
// 写入第一个
excelWriter.write(list,sheet);
sheet = EasyExcel.writerSheet(1,"用户操作日志1")
//.registerWriteHandler(new LoopMergeStrategy(2, 0))
.registerWriteHandler(new BizMergeStrategy())
.build();
// 写入第二个
excelWriter.write(list,sheet);
excelWriter.finish();
} catch (IOException e) {
e.printStackTrace();
}*/
/**
* 单独sheet导出
*/
try {
// 每两行一合并
LoopMergeStrategy loopMergeStrategy = new LoopMergeStrategy(2, 1);
LoopMergeStrategy loopMergeStrategies = new LoopMergeStrategy(2, 0);
EasyExcel.write(resp.getOutputStream(), RespLogInfo.class).
registerWriteHandler(loopMergeStrategy)
.registerWriteHandler(loopMergeStrategies)
.registerWriteHandler(horizontalCellStyleStrategy)
.sheet(0,"用户操作日志")
.doWrite(list);
} catch (IOException e) {
e.printStackTrace();
}
/**
* 样式设置
*/
private static HorizontalCellStyleStrategy exportExcel(HttpServletResponse response, String tableName){
response.setContentType("application/vnd.ms-excel;charset=UTF-8");
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
// 设置表名
String fileName = URLEncoder.encode(tableName, StandardCharsets.UTF_8);
response.setHeader( "Content-disposition", "attachment;filename=" + fileName + ".xlsx");
// 头部标题设置
WriteCellStyle headStyle = new WriteCellStyle();
// 设置背景色
headStyle.setFillForegroundColor(IndexedColors.PALE_BLUE.getIndex());
// 设置表头字体
WriteFont headFont = new WriteFont();
headFont.setFontHeightInPoints((short) 13);
headFont.setBold(true);
// 赋值字体
headStyle.setWriteFont(headFont);
// 设置表头水平居中
headStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
// 内容设置
WriteCellStyle contentStyle = new WriteCellStyle();
// 水平居中
contentStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
// 垂直居中
contentStyle.setVerticalAlignment(VerticalAlignment.CENTER);
// 边框样式
contentStyle.setBorderLeft(THIN);
contentStyle.setBorderTop(THIN);
contentStyle.setBorderRight(THIN);
contentStyle.setBorderBottom(THIN);
return new HorizontalCellStyleStrategy(headStyle,contentStyle);
}
}
导出数据 多组模板填充
/**
* 多组模板填充 (将导出类的方法里的替换掉且要准备好导出模板)
*/
String template = "src/main/resources/logInfo_template.xlsx";
// 方案1 一下子全部放到内存里面 并填充
EasyExcel.write(resp.getOutputStream(),RespLogInfo.class).withTemplate(template).sheet().doFill(list);
// 方案2 分多次 填充 会使用文件缓存(省内存)
ExcelWriter excelWriter = EasyExcel.write(resp.getOutputStream(),RespLogInfo.class).withTemplate(template).build();
WriteSheet writeSheet = EasyExcel.writerSheet().build();
excelWriter.fill(list, writeSheet);
excelWriter.fill(list, writeSheet);
// 千万别忘记关闭流
excelWriter.finish();
二.导入(读)数据
1.编写一个监听器 ExcelDataListener.java继承AnalysisEventListener类
重写invoke方法
package com.xiaogui.log.utils;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.metadata.Cell;
import com.alibaba.excel.read.metadata.holder.ReadRowHolder;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.alibaba.fastjson.JSON;
import com.xiaogui.log.mapper.LogMapper;
import com.xiaogui.log.vo.resp.RespLogInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.*;
/**
* @author 廾匸
* @version 1.0.0
* @ClassName ExcelDataListener
* @Description 读取数据监听器
* @createTime 2022年01月09日
*/
@Component
@Slf4j
public class ExcelDataListener extends AnalysisEventListener<RespLogInfo> {
/**
* 每隔10条存储数据库,实际使用中可以3000条,然后清理list ,方便内存回收
*/
private static final int BATCH_COUNT = 10;
List<RespLogInfo> list = new ArrayList<>();
/**
* 假设这个是一个DAO,当然有业务逻辑这个也可以是一个service。
* 当然如果不用存储这个对象没用。
*/
private final LogMapper logMapper;
/**
* 如果使用了spring,请使用这个构造方法。
* 每次创建Listener的时候需要把spring管理的类传进来
*/
public ExcelDataListener(LogMapper logMapper) {
this.logMapper = logMapper;
}
/**
* 这个每一条数据解析都会来调用
* one row value. Is is same as {@link AnalysisContext#readRowHolder()}
*/
@Override
public void invoke(RespLogInfo data, AnalysisContext context) {
log.info("解析到一条数据:{}", JSON.toJSONString(data));
list.add(data);
// 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
if (list.size() >= BATCH_COUNT) {
for (RespLogInfo respLogInfo : list) {
logMapper.insertSelective(respLogInfo);
}
// 存储完成清理 list
list.clear();
}
}
/**
* 所有数据解析完成了 都会来调用
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
log.info("所有数据解析完成!");
}
}
导出类中使用read读取,并且引入(可以注入也可以new一个ExcelDataListener)
/**
* 读取数据
*/
// 写法1:
String fileName = "src/main/resources/logInfo.xlsx";
// 这里 需要指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭
EasyExcel.read(fileName, RespLogInfo.class, excelDataListener).sheet().doRead();
// 写法2:
ExcelReader excelReader = EasyExcel.read(fileName, RespLogInfo.class,excelDataListener).build();
ReadSheet readSheet = EasyExcel.readSheet(0).build();
excelReader.read(readSheet);
// 这里千万别忘记关闭,读的时候会创建临时文件,到时磁盘会崩的
excelReader.finish();