封装
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.util.StrUtil;
import cn.it.learning.constant.ExportConstant;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
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.alibaba.excel.write.style.column.AbstractColumnWidthStyleStrategy;
import com.alibaba.excel.write.style.column.SimpleColumnWidthStyleStrategy;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Objects;
/**
* @Author it-learning
* @Description excel导出工具包
* @Date 2022/2/25 18:05
* @Version 1.0
*/
@Slf4j
@Component
public class ExcelExportUtil<T> {
/**
* excel文件导出(可以包含多个sheet页),固定表头(通过实体指定属性的方式)
*
* @param response
* @param fileName 导出文件名
* @param head 导出表头(多个sheet页就是多个集合元素)
* @param exportData 需要导出数据
* @param sheetNames sheet页的名称,为空则默认以:sheet + 数字规则命名
*/
public static <T> void exportFile(String fileName, List<T> head, List<List<T>> exportData, List<String> sheetNames, HttpServletResponse response) {
if (Objects.isNull(response) || StrUtil.isBlank(fileName) || CollUtil.isEmpty(head)) {
log.info("ExcelExportUtil exportFile required param can't be empty");
return;
}
ExcelWriter writer = null;
try {
response.setContentType(ExportConstant.EXCEL_CONTENT_TYPE);
response.setCharacterEncoding(ExportConstant.UTF_8);
response.setHeader(ExportConstant.CONTENT_DISPOSITION, ExportConstant.ATTACHMENT_FILENAME + fileName + ExportConstant.XLSX_SUFFIX);
// 设置导出的表格样式
HorizontalCellStyleStrategy horizontalCellStyleStrategy = getExportDefaultStyle();
writer = EasyExcel.write(response.getOutputStream()).registerWriteHandler(horizontalCellStyleStrategy).build();
for (int itemIndex = 0; itemIndex < exportData.size(); itemIndex++) {
// 表头数据
Object headData = head.get(itemIndex);
// sheet页的数据
List<T> list = exportData.get(itemIndex);
WriteSheet sheet = EasyExcel.writerSheet(itemIndex, CollUtil.isEmpty(sheetNames) ? ExportConstant.SHEET_NAME + itemIndex + 1 : sheetNames.get(itemIndex)).head(headData.getClass()).build();
writer.write(list, sheet);
}
} catch (Exception e) {
log.error("ExcelExportUtil exportFile in error:{}", e);
} finally {
if (null != writer) {
writer.finish();
}
}
}
/**
* 导出动态表头数据(支持多表单),表头列不固定,根据程序或者读取数据库生成
*
* @param fileName 导出文件名
* @param head 导出表头列
* @param exportData 需要导出的数据
* @param sheetNames sheet页名称
* @param response 响应流
*/
public static <T> void exportWithDynamicData(String fileName, List<List<List<String>>> head, List<List<List<T>>> exportData, List<String> sheetNames, HttpServletResponse response) throws IOException {
ExcelWriter writer = null;
try {
response.setContentType(ExportConstant.EXCEL_CONTENT_TYPE);
response.setCharacterEncoding(ExportConstant.UTF_8);
response.setHeader(ExportConstant.CONTENT_DISPOSITION, ExportConstant.ATTACHMENT_FILENAME + fileName + ExportConstant.XLSX_SUFFIX);
// 设置默认样式的excel表格对象
HorizontalCellStyleStrategy horizontalCellStyleStrategy = getExportDefaultStyle();
AbstractColumnWidthStyleStrategy columnWidthStyleStrategy = new SimpleColumnWidthStyleStrategy(ExportConstant.DEFAULT_CELL_LENGTH);
writer = EasyExcel.write(response.getOutputStream()).registerWriteHandler(horizontalCellStyleStrategy).registerWriteHandler(columnWidthStyleStrategy).build();
for (int i = 0; i < exportData.size(); i++) {
List<List<T>> tableData = exportData.get(i);
WriteSheet sheet = EasyExcel.writerSheet(i, CollectionUtil.isEmpty(sheetNames) ? ExportConstant.SHEET_NAME + i + 1 : sheetNames.get(i)).head(head.get(i)).build();
writer.write(tableData, sheet);
}
} finally {
if (Objects.nonNull(writer)) {
writer.finish();
}
}
}
/**
* 下载指定路径下的模板
*
* @param filePath 文件所在路径(包含模板名称):一般是放在项目的resources目录下的template
* @param fileName 下载时默认的文件名称
* @param response 响应流
*/
public static void downloadTemplate(String filePath, String fileName,String fileSuffix, HttpServletResponse response) {
try {
// 设置浏览器以附件形式读取响应流中的数据
response.setContentType(ExportConstant.EXCEL_CONTENT_TYPE);
response.setCharacterEncoding(ExportConstant.UTF_8);
response.setHeader(ExportConstant.CONTENT_DISPOSITION, ExportConstant.ATTACHMENT_FILENAME + fileName + fileSuffix);
byte[] bytes = ResourceUtil.readBytes(filePath);
response.getOutputStream().write(bytes);
} catch (Exception e) {
log.error("ExcelExportUtil downloadTemplate in error:{}", e);
}
}
/**
* 配置默认的excel表格样式对象
*
* @return
*/
private static HorizontalCellStyleStrategy getExportDefaultStyle() {
WriteCellStyle headWriteCellStyle = new WriteCellStyle();
//设置头字体
WriteFont headWriteFont = new WriteFont();
headWriteFont.setBold(true);
headWriteCellStyle.setWriteFont(headWriteFont);
//设置头居中
headWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
//内容策略
WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
//设置 水平居中
contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
HorizontalCellStyleStrategy horizontalCellStyleStrategy = new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);
return horizontalCellStyleStrategy;
}
}
import cn.it.learning.refactor.ThrowingConsumer;
import cn.it.learning.util.excel.ExcelImportCommonListener;
import com.alibaba.excel.EasyExcel;
import java.io.InputStream;
import java.util.List;
/**
* @Author it-learning-diary
* @Description 导入excel模板
* @Date 2022/4/24 14:09
* @Version 1.0
*/
public class ExcelImportUtil<T> {
/**
* 通用导入excel文件方法
*
* @param fileStream 导入的文件流
* @param rowDto 接收excel每行数据的实体
* @param rowAction 将接收到的实体进行自定义的业务处理逻辑方法
* @param <T> 实体类型
*/
public static <T> void importFile(InputStream fileStream, T rowDto, ThrowingConsumer<List<T>> rowAction) {
// 获取excel通用监听器
ExcelImportCommonListener<T> commonListener = new ExcelImportCommonListener<>(rowAction);
// 读取excel文件并导入
EasyExcel.read(fileStream, rowDto.getClass(), commonListener).sheet().doRead();
}
}
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.util.StrUtil;
import cn.it.learning.constant.ImportConstant;
import cn.it.learning.refactor.ThrowingConsumer;
import cn.it.learning.valid.ImportValid;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.exception.ExcelDataConvertException;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.excel.util.ConverterUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.utils.Lists;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
/**
* @Author it-learning-diary
* @Description 数据导入通用监听器
* @Date 2022/4/24 14:27
* @Version 1.0
*/
@Slf4j
public class ExcelImportCommonListener<T> implements ReadListener<T> {
/**
* 转换后插入数据表的实体
*/
private List<T> persistentDataList = Lists.newArrayList();
/**
* 具体数据落库的业务逻辑方法:此处的逻辑是将数据从excel中读取出来后,然后进行自己的业务处理,最后进行落库操作
* 不懂的可以参考:UserServiceImpl下的saveUserList方法案例
*/
private final ThrowingConsumer<List<T>> persistentActionMethod;
/**
* 异常日志记录(可用于记录解析excel数据时存储异常信息,用于业务的事务的回滚)
*/
private List<String> errorLogList = new ArrayList<>();
/**
* 用于测试异常变量
*/
private Long count = 1000L;
/**
* 构造函数(不包含异常信息)
*
* @param persistentActionMethod 从excel读取到的数据到落库的业务逻辑
*/
public ExcelImportCommonListener(ThrowingConsumer<List<T>> persistentActionMethod) {
this.persistentActionMethod = persistentActionMethod;
}
/**
* 构造函数(不包含异常信息)
*
* @param persistentActionMethod 从excel读取到的数据到落库的业务逻辑
*/
public ExcelImportCommonListener(ThrowingConsumer<List<T>> persistentActionMethod, List<String> errorLogLIst) {
this.persistentActionMethod = persistentActionMethod;
this.errorLogList = errorLogLIst;
}
/**
* 在转换异常 获取其他异常情况下会调用本接口。抛出异常则停止读取。如果这里不抛出异常则 继续读取下一行。
* 根据自己需要看是否抛出异常,如果没有特殊要求,则可以不修改。
*
* @param exception
* @param context
* @throws Exception
*/
@Override
public void onException(Exception exception, AnalysisContext context) throws Exception {
if (exception instanceof ExcelDataConvertException) {
ExcelDataConvertException excelDataConvertException = (ExcelDataConvertException) exception;
log.error("第{}行,第{}列解析异常,数据为:{}", excelDataConvertException.getRowIndex(),
excelDataConvertException.getColumnIndex(), excelDataConvertException.getCellData().getStringValue());
if (Objects.nonNull(errorLogList)) {
// 记录异常日志
String errorLog = "第" + excelDataConvertException.getRowIndex() + "行,第" + excelDataConvertException.getColumnIndex()
+ "列解析异常,数据为:" + excelDataConvertException.getCellData().getStringValue() + "";
errorLogList.add(errorLog);
}
}
}
/**
* 返回每个sheet页的表头,根据自己实际业务进行表头字段等校验逻辑,如果没有则保持不动
*
* @param headMap
* @param context
*/
@Override
public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {
Map<Integer, String> headMapping = ConverterUtils.convertToStringMap(headMap, context);
log.info("表头数据: " + StrUtil.toString(headMapping));
if (CollUtil.isEmpty(headMapping)) {
errorLogList.add("The header of file can't be empty!");
}
}
/**
* 解析excel表中每行数据
*
* @param t
* @param analysisContext
*/
@Override
public void invoke(T t, AnalysisContext analysisContext) {
try {
// 如果对实体需要设置其他额外属性,可以通过反射方式,如下面的relationId属性
Class<?> aClass = t.getClass();
Field relationIdField = aClass.getDeclaredField("relationId");
relationIdField.setAccessible(Boolean.TRUE);
relationIdField.set(t, count++);
} catch (Exception e) {
log.error("in error{}", e);
errorLogList.add("The Row Data inject relationId field in error");
}
// 校验导入字段
ImportValid.validRequireField(t,errorLogList);
if (Objects.isNull(errorLogList) || CollUtil.isEmpty(errorLogList)) {
persistentDataList.add(t);
// 当数据达到最大插入数量后则进行落库操作,防止大数量情况下OOM
if (persistentDataList.size() >= ImportConstant.MAX_INSERT_COUNT) {
// 进行业务数据插入
this.persistentDataToDb(persistentDataList);
// 清空集合
persistentDataList.clear();
}
}
}
/**
* 所有数据解析完后,回调
*
* @param analysisContext
*/
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
// 在此处调用落库操作,防止有剩余数据未落库
if (CollUtil.isNotEmpty(persistentDataList)) {
persistentDataToDb(persistentDataList);
}
}
/**
* 将数据持久化到数据库中
*
* @param data
*/
private void persistentDataToDb(List<T> data) {
// 对数据分组,批量插入
List<List<T>> dataList = ListUtil.split(data, ImportConstant.MAX_INSERT_COUNT);
dataList.stream().forEach(persistentActionMethod);
}
}
使用示例:
/**
* 导入用户数据案例
*
* @param file
*/
@Transactional(rollbackFor = Exception.class)
public void uploadUserListDemoWithExcel(MultipartFile file, String username) throws Exception {
// 此处先校验导入的文件类型是否为excel
String type = FileTypeUtil.getType(file.getInputStream());
if (StrUtil.isBlank(type) || type.contains(ImportConstant.XLS_TYPE) || type.contains(ImportConstant.XLSX_TYPE)) {
// 返回校验失败信息
return;
}
User user = new User();
user.setId(100);
user.setName("外层");
user.setPassword("外层");
userService.save(user);
// 调用统一导入方法
ExcelImportUtil.importFile(file.getInputStream(), new UserDto(), UserServiceImpl::saveUserList);
}
/**
* 导出案例
*
* @param response
*/
public void exportUserListDemoWithExcel(HttpServletResponse response) {
// 表头(使用excel中的注解定义,如果表头不固定,请使用进行导出)
List<UserExportVo> head = Stream.of(new UserExportVo()).collect(Collectors.toList());
// 数据(使用两层list为了兼容多个sheet页,如果是不同的sheet页则放在不同的List集合中)
List<List<UserExportVo>> exportDataList = new ArrayList<>();
List<UserExportVo> exportItem = new ArrayList<>();
// 查询数据
List<User> dbData = userService.list();
// 将数据转换成导出需要的实际数据格式,此处只是演示
for (User user : dbData) {
UserExportVo vo = new UserExportVo();
BeanUtil.copyProperties(user, vo);
exportItem.add(vo);
}
exportDataList.add(exportItem);
// sheet页名称-自定义,如果没有则传空
List<String> sheetNameList = Stream.of("sheet1").collect(Collectors.toList());
ExcelExportUtil.exportFile("user", head, exportDataList, sheetNameList, response);
}
参考文档:
it-wheels-king
并发编程实战:多线程+EasyExcel,20ms内极速导入百万级大数据报表
EasyExcel模板填充技术的魅力