EasyExcel是阿里巴巴开源插件之一,主要解决了poi框架使用复杂,sax解析模式不容易操作,数据量大起来容易OOM,解决了POI并发造成的报错。主要解决方式:通过解压文件的方式加载,一行一行地加载,并且抛弃样式字体等不重要的数据,降低内存的占用。
EasyExcel在SpringBoot的集成 引入对应的pom依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.2.11</version>
</dependency>
excel常用注解
@ExcelProperty
@ColumnWith 列宽
@ContentFontStyle 文本字体样式
@ContentLoopMerge 文本合并
@ContentRowHeight 文本行高度
@ContentStyle 文本样式
@HeadFontStyle 标题字体样式
@HeadRowHeight 标题高度
@HeadStyle 标题样式
@ExcelIgnore 忽略项
@ExcelIgnoreUnannotated 忽略未注解
@Data
public class Member {
/**
* EasyExcel使用:导出时忽略该字段
*/
@ExcelIgnore
private Integer id;
@ExcelProperty("用户名")
@ColumnWidth(20)
private String username;
/**
* EasyExcel使用:日期的格式化
*/
@ColumnWidth(20)
@ExcelProperty("出生日期")
@DateTimeFormat("yyyy-MM-dd")
private Date birthday;
/**
* EasyExcel使用:自定义转换器
*/
@ColumnWidth(10)
@ExcelProperty(value = "性别", converter = GenderConverter.class)
private Integer gender;
}
GenderConverter转换器的代码实现如下:
public class GenderConverter implements Converter<Integer> {
private static final String MAN = "男";
private static final String WOMAN = "女";
@Override
public Class<?> supportJavaTypeKey() {
// 实体类中对象属性类型
return Integer.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
// Excel中对应的CellData属性类型
return CellDataTypeEnum.STRING;
}
@Override
public Integer convertToJavaData(CellData cellData, ExcelContentProperty excelContentProperty,
GlobalConfiguration globalConfiguration) {
// 从Cell中读取数据
String gender = cellData.getStringValue();
// 判断Excel中的值,将其转换为预期的数值
if (MAN.equals(gender)) {
return 0;
} else if (WOMAN.equals(gender)) {
return 1;
}
return null;
}
@Override
public CellData<?> convertToExcelData(Integer integer, ExcelContentProperty excelContentProperty,
GlobalConfiguration globalConfiguration) {
// 判断实体类中获取的值,转换为Excel预期的值,并封装为CellData对象
if (integer == null) {
return new CellData<>("");
} else if (integer == 0) {
return new CellData<>(MAN);
} else if (integer == 1) {
return new CellData<>(WOMAN);
}
return new CellData<>("");
}
}
字段注解 类注解
@ColumnWith(列宽)
@ColumnWidth(全局列宽)
@ExcelProperty(字段配置)
@HeadFontStyle(头样式)
@HeadRowHeight(标题高度)
@ContentFontStyle(内容字体样式)
@ContentRowHeight(内容高度)
@ExcelProperty
必要的一个注解,注解中有三个参数value,index,converter分别代表列明,列序号,数据转换方式 value和index只能二选一,通常不用设置converter
1.value 通过标题文本对应
2.index 通过文本行号对应
3.converter 转换器,通常入库和出库转换使用,如性别入库0和1,出库男和女
public class ImeiEncrypt {
@ExcelProperty(value = "值")
private String valueField;
@ExcelProperty(value = 1,converter =IndustryIdConverter.class)
private String indexField;
@ExcelProperty(value = "值对应和转换器",converter =IndustryIdConverter.class)
private String valueAndConverterField;
}
@ColumnWith 设置列宽度,只有一个参数value,value的单位是字符长度,最大可以设置255个字符,因为一个excel单元格最大可以写入的字符个数就是255个字符。
public class ImeiEncrypt {
@ColumnWidth(value = 18)
private String imei;
}
@ContentFontStyle 用于设置单元格内容字体格式的注解
参数:
fontName 字体名称
fontHeightInPoints 字体高度
italic 是否斜体
strikeout 是否设置删除水平线
color 字体颜色
typeOffset 偏移量
underline 下划线
bold 是否加粗
charset 编码格式
@ContentLoopMerge 用于设置合并单元格的注解
参数:
eachRow
columnExtend
@ContentRowHeight 用于设置行高
参数:
value 行高,-1代表自动行高
@ContentStyle 设置内容格式注解
参数:
dataFormat 日期格式
hidden 设置单元格使用此样式隐藏
locked 设置单元格使用此样式锁定
quotePrefix 在单元格前面增加`符号,数字或公式将以字符串形式展示
horizontalAlignment 设置是否水平居中
wrapped 设置文本是否应换行。将此标志设置为true通过在多行上显示使单元格中的所有内容可见
verticalAlignment 设置是否垂直居中
rotation 设置单元格中文本旋转角度。03版本的Excel旋转角度区间为-90°90°,07版本的Excel旋转角度区间为0°180°
indent 设置单元格中缩进文本的空格数
borderLeft 设置左边框的样式
borderRight 设置右边框样式
borderTop 设置上边框样式
borderBottom 设置下边框样式
leftBorderColor 设置左边框颜色
rightBorderColor 设置右边框颜色
topBorderColor 设置上边框颜色
bottomBorderColor 设置下边框颜色
fillPatternType 设置填充类型
fillBackgroundColor 设置背景色
fillForegroundColor 设置前景色
shrinkToFit 设置自动单元格自动大小
@HeadFontStyle 用于定制标题字体格式
参数 含义
fontName 设置字体名称
fontHeightInPoints 设置字体高度
italic 设置字体是否斜体
strikeout 是否设置删除线
color 设置字体颜色
typeOffset 设置偏移量
underline 设置下划线
charset 设置字体编码
bold 设置字体是否加粗
@HeadRowHeight 设置标题行行高
参数 含义
value 设置行高,-1代表自动行高
@HeadStyle 设置标题样式
参数 含义
dataFormat 日期格式
hidden 设置单元格使用此样式隐藏
locked 设置单元格使用此样式锁定
quotePrefix 在单元格前面增加`符号,数字或公式将以字符串形式展示
horizontalAlignment 设置是否水平居中
wrapped 设置文本是否应换行。将此标志设置为true通过在多行上显示使单元格中的所有内容可见
verticalAlignment 设置是否垂直居中
rotation 设置单元格中文本旋转角度。03版本的Excel旋转角度区间为-90°90°,07版本的Excel旋转角度区间为0°180°
indent 设置单元格中缩进文本的空格数
borderLeft 设置左边框的样式
borderRight 设置右边框样式
borderTop 设置上边框样式
borderBottom 设置下边框样式
leftBorderColor 设置左边框颜色
rightBorderColor 设置右边框颜色
topBorderColor 设置上边框颜色
bottomBorderColor 设置下边框颜色
fillPatternType 设置填充类型
fillBackgroundColor 设置背景色
fillForegroundColor 设置前景色
shrinkToFit 设置自动单元格自动大小
@ExcelIgnore 不将该字段转换成Excel
@ExcelIgnoreUnannotated 没有注解的字段都不转换
基础综合示例
//行高全部设为40
@HeadRowHeight(value = 40)
//标题全部居中
@HeadStyle(horizontalAlignment = CENTER)
public class SupervisionDailyExportProcessDTO {
//"二、问题整改情况"是大标题,"序号"是大标题下面的子标题
@ExcelProperty({"二、问题整改情况", "序号"})
@ContentStyle(horizontalAlignment = CENTER)
private Integer id;
@ExcelProperty({"二、问题整改情况", "问题来源"})
//单独设置这一列列宽为40
@ColumnWidth(value = 40)
@ContentStyle(wrapped = true, horizontalAlignment = CENTER)
private String questionOrigin;
@ExcelProperty({"二、问题整改情况", "督察点位"})
@ColumnWidth(value = 40)
@ContentStyle(wrapped = true, horizontalAlignment = CENTER)
private String supervisionPoint;
@ExcelProperty({"二、问题整改情况", "问题内容"})
@ColumnWidth(value = 40)
@ContentStyle(wrapped = true, horizontalAlignment = CENTER)
private String questionContents;
@ExcelProperty({"二、问题整改情况", "责任单位"})
@ColumnWidth(value = 30)
@ContentStyle(horizontalAlignment = CENTER)
private String orgName;
@ExcelProperty({"二、问题整改情况", "整改情况"})
@ColumnWidth(value = 40)
@ContentStyle(wrapped = true, horizontalAlignment = CENTER)
private String processSituation;
}
补充颜色
NPOI Excel 单元格颜色对照表,在引用了 NPOI.dll 后可通过 ICellStyle 接口的 FillForegroundColor 属性实现 Excel 单元格的背景色设置,FillPattern 为单元格背景色的填充样式。
NPOI Excel 单元格背景颜色设置方法以及颜色对照表:
ICellStyle style = workbook.CreateCellStyle();
style.FillForegroundColor = NPOI.HSSF.Util.HSSFColor.Red.Index;
style.FillPattern = FillPattern.SolidForeground;
ICell cell = workbook.CreateSheet().CreateRow(0).CreateCell(0);
cell.CellStyle = style;
easyExcel使用
EasyExcelUtils 导出工具类
package com.sjasoft.framework.utils;
import cn.hutool.core.date.DateUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.metadata.style.WriteFont;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.springframework.lang.Nullable;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import javax.print.DocFlavor;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.util.Date;
import java.util.List;
/**
* 导出工具类
*
* @author zlf
*/
public class EasyExcelUtils {
/**
* 导出公共方法
*
* @param datas 要导出的数据集
* @param clazz 导出的实体
* @param fileName 导出文件名称
* @throws IOException
*/
public static void export(List datas, @Nullable Class<?> clazz, String fileName, String sheetName) throws IOException {
ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletResponse response = sra.getResponse();
response.setContentType("application/vnd.ms-excel;charset=utf-8");
// 设置文件名
String exportFileName = URLEncoder.encode(fileName, "UTF-8");
//String exportSheetName = URLEncoder.encode(sheetName, "UTF-8");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + exportFileName + ExcelTypeEnum.XLSX.getValue());
// 创建一个写出的单元格样式对象
WriteCellStyle headWriteCellStyle = new WriteCellStyle();
// 设置水平对齐方式
headWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
// 设置填充前景色
headWriteCellStyle.setFillForegroundColor(IndexedColors.PALE_BLUE.getIndex());
// 创建写出Excel的字体对象
WriteFont headWriteFont = new WriteFont();
//设置字体高度
//headWriteFont.setFontHeightInPoints((short) 10);
headWriteCellStyle.setWriteFont(headWriteFont);
headWriteCellStyle.setWrapped(true);
// 创建一个写出的单元格样式对象
WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
//设置内容靠中对齐
//contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
// 边框设置
/*contentWriteCellStyle.setBorderTop(BorderStyle.THIN); // 设置单元格上边框为细线
contentWriteCellStyle.setBorderBottom(BorderStyle.THICK); // 设置单元格下边框为粗线
contentWriteCellStyle.setBorderLeft(BorderStyle.MEDIUM); // 设置单元格左边框为中线
contentWriteCellStyle.setBorderRight(BorderStyle.MEDIUM_DASHED); // 设置单元格右边框为中虚线*/
// 设置行样式
HorizontalCellStyleStrategy horizontalCellStyleStrategy = new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);
// 设置自定义自适应列宽样式
ExcelWidthStyleStrategy excelWidthStyleStrategy = new ExcelWidthStyleStrategy();
// 如果不用模板的方式导出的话,是doWrite
EasyExcel.write(response.getOutputStream(), clazz)
.excelType(ExcelTypeEnum.XLSX)
.autoCloseStream(true)
.registerWriteHandler(horizontalCellStyleStrategy)
.registerWriteHandler(excelWidthStyleStrategy)
.sheet(sheetName)
.doWrite(datas);
}
/**
* 根据Excel模板,批量导入数据
*
* @param file 导入的Excel
* @param clazz 解析的类型
* @return 解析完成的数据
*/
public static List<?> importExcel(MultipartFile file, Class<?> clazz) {
if (file == null || file.isEmpty()) {
throw new RuntimeException("没有文件或者文件内容为空!");
}
List<?> dataList;
BufferedInputStream ipt;
try {
InputStream is = file.getInputStream();
// 用缓冲流对数据流进行包装
ipt = new BufferedInputStream(is);
// 数据解析监听器
ExcelListener<?> listener = new ExcelListener<>();
// 读取数据
EasyExcel.read(ipt, clazz, listener).sheet().doRead();
// 获取去读完成之后的数据
dataList = listener.getList();
} catch (Exception e) {
throw new RuntimeException("数据导入失败!" + e);
}
return dataList;
}
}
ExcelListener Excel数据解析监听器, 数据解析方法异步执行
package com.sjasoft.framework.utils;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Excel数据解析监听器, 数据解析方法异步执行
*
* @param <T> Excel中数据的类型
* @author zlf
*/
@Getter
@Setter
@NoArgsConstructor
public class ExcelListener<T> extends AnalysisEventListener<T> {
// 加入一个判断标签,判断数据是否已经读取完
private volatile boolean retryLock = false;
// 解析完成后的数据集合, 监听对象初始化之后,立即初始化集合对象
private final List<T> dataList = new ArrayList<>();
// 每次最多导入条数
private final int batchSize = 2000;
/**
* 获取解析后的数据集合, 如果数据还没有被解析完成,会对读取该集合的线程进行阻塞,直到数据读取完成之后,进行解锁。
* 如果一次导入数据超过batchSize条,则以抛异常的形式阻止导入数据
*
* @return 解析后的数据集合
*/
public List<T> getList() {
while (true) {
if (retryLock) {
if (dataList.size() > batchSize) {
// 手动清空数据内存数据,减少内存消耗
dataList.clear();
throw new RuntimeException("一次最多导入" + batchSize + "条数据");
} else {
return dataList;
}
}
}
}
/**
* Excel每解析一行数据,就会调用一次该方法
*
* @param data one row value. Is is same as {@link AnalysisContext#readRowHolder()}
* @param context analysis context
*/
@Override
public void invoke(T data, AnalysisContext context) {
dataList.add(data);
}
/**
* 读取表头内容
*
* @param headMap 表头部数据
* @param context 数据解析上下文
*/
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
// 在此处可以进行表头的校验,headMap对应的就是表头的数据
// 在此处有个很奇怪的地方,就是当用户导入的文件没有任何表头的时候,也就是表头这一行为空
// 表头检验则不会进行这个函数,且数据导入成功,所以这个地方要确认清楚
// 最后我是在拿到所有数据,包括表头,再取第一行为表头进行校验
//System.out.println("表头:" + headMap);
}
/**
* 流中的数据解析完成之后,就会调用此方法
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 数据解析完成,解锁
retryLock = true;
}
/**
* 解析过程如果发生异常,会调用此方法
*/
@Override
public void onException(Exception exception, AnalysisContext context) {
throw new RuntimeException("Excel数据异常,请检查或联系管理员!");
}
}
ExcelWidthStyleStrategy 自适应宽度工具类
package com.sjasoft.framework.utils;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.style.column.AbstractColumnWidthStyleStrategy;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.poi.ss.usermodel.Cell;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ExcelWidthStyleStrategy extends AbstractColumnWidthStyleStrategy {
// 单元格的最大宽度
private static final int MAX_COLUMN_WIDTH = 200;
// 缓存(第一个Map的键是sheet的index, 第二个Map的键是列的index, 值是数据长度)
private final Map<Integer, Map<Integer, Integer>> CACHE = new HashMap<>(8);
// 重写设置列宽的方法
@Override
protected void setColumnWidth(WriteSheetHolder writeSheetHolder,
List<WriteCellData<?>> cellDataList,
Cell cell,
Head head,
Integer relativeRowIndex,
Boolean isHead) {
boolean needSetWidth = isHead || !CollectionUtils.isEmpty(cellDataList);
// 当时表头或者单元格数据列表有数据时才进行处理
if (needSetWidth) {
Map<Integer, Integer> maxColumnWidthMap = CACHE.computeIfAbsent(writeSheetHolder.getSheetNo(), k -> new HashMap<>(16));
// 获取数据长度
Integer columnWidth = this.dataLength(cellDataList, cell, isHead);
if (columnWidth >= 0) {
if (columnWidth > MAX_COLUMN_WIDTH) {
columnWidth = MAX_COLUMN_WIDTH;
}
// 确保一个列的列宽以表头为主,如果表头已经设置了列宽,单元格将会跟随表头的列宽
Integer maxColumnWidth = maxColumnWidthMap.get(cell.getColumnIndex());
if (maxColumnWidth == null || columnWidth > maxColumnWidth) {
maxColumnWidthMap.put(cell.getColumnIndex(), columnWidth);
// 如果使用EasyExcel默认表头,那么使用columnWidth * 512
// 如果不使用EasyExcel默认表头,那么使用columnWidth * 256
// 如果是自己定义的字体大小,可以再去测试这个参数常量
writeSheetHolder
.getSheet()
.setColumnWidth(cell.getColumnIndex(), columnWidth * 512);
}
}
}
}
/**
* 获取当前单元格的数据长度
*/
private Integer dataLength(List<WriteCellData<?>> cellDataList,
Cell cell,
Boolean isHead) {
if (isHead) {
return cell.getStringCellValue().getBytes().length;
} else {
WriteCellData cellData = cellDataList.get(0);
CellDataTypeEnum type = cellData.getType();
if (type == null) {
return -1;
} else {
switch (type) {
case STRING:
return cellData.getStringValue().getBytes().length;
case BOOLEAN:
return cellData.getBooleanValue().toString().getBytes().length;
case NUMBER:
return cellData.getNumberValue().toString().getBytes().length;
default:
return -1;
}
}
}
}
}
调用
Controller
@ApiOperation("导出日志明细")
@PostMapping("/exportLogDetailInfo")
public void exportLogDetailInfo(@RequestBody LogDetailParam detailParam) throws IOException{
logService.exportLogDetailInfo(detailParam);
}
service
@SystemLog("导出日志明细")
public void exportLogDetailInfo(LogDetailParam detailParam) throws IOException {
List<LogDetailVo> logDetailList = tLogListDao.getLogDetailInfo(detailParam);
if(CollectionUtil.isEmpty(logDetailList)){
throw new RuntimeException("未查询到数据,请调整条件后重新导出");
}
//导出文件名称
String fileName = new String("导出日志明细" + DateUtil.format(new Date(),"yyyyMMddHHmmss"));
EasyExcelUtils.export(logDetailList
,LogDetailVo.class,fileName,"导出日志明细");
}