背景
后台需要批量处理excel数据,常见的就是导入导出了。故自己写了一个小工具。
依赖
xml
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.4</version>
</dependency>
工具类
java
package org.test.exceldemo.excel;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.metadata.data.CellData;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.read.listener.PageReadListener;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.style.column.AbstractColumnWidthStyleStrategy;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Sheet;
import org.springframework.util.CollectionUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
/**
* Excel 导入导出工具类
* 包含只读写、只写、流式读、流式写
*/
public class EasyExcelPipelineUtil {
/**
* ⭐⭐⭐ 同步读取(小文件用)
*/
public static <T> List<T> readSync(InputStream inputStream, Class<T> clazz) {
return EasyExcel.read(inputStream)
.head(clazz)
.sheet()
.doReadSync();
}
/**
* ⭐⭐⭐ 分页读取(推荐)
*/
public static <T> void readByPage(InputStream inputStream,
Class<T> clazz,
Consumer<List<T>> consumer,
int pageSize) {
EasyExcel.read(inputStream, clazz,
new PageReadListener<T>(consumer, pageSize))
.sheet()
.doRead();
}
/**
* ⭐⭐⭐ MultipartFile 分页读取
*/
public static <T> void readByMultipart(MultipartFile file,
Class<T> clazz,
Consumer<List<T>> consumer,
int pageSize) throws IOException {
readByPage(file.getInputStream(), clazz, consumer, pageSize);
}
/**
* ⭐⭐⭐ URL 分页读取
*/
public static <T> void readByUrl(String url,
Class<T> clazz,
Consumer<List<T>> consumer,
int pageSize) throws Exception {
try (InputStream in = new URL(url).openStream()) {
readByPage(in, clazz, consumer, pageSize);
}
}
/**
* ====================================
* ⭐⭐⭐⭐⭐ 直接写 Excel(最常用导出)
* ====================================
*/
public static <T> void write(String filePath,
Class<T> clazz,
List<T> data, String sheetName) {
EasyExcel.write(filePath, clazz)
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
.registerWriteHandler(new ExcelCellWriteWidthConfig())
.sheet(sheetName)
.doWrite(data);
}
/**
* ⭐ 写到 OutputStream(浏览器下载用)
*/
public static <T> void write(OutputStream outputStream,
Class<T> clazz,
List<T> data, String sheetName) {
EasyExcel.write(outputStream, clazz)
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
.registerWriteHandler(new ExcelCellWriteWidthConfig())
.sheet(sheetName)
.doWrite(data);
}
/**
* ⭐⭐⭐⭐ 超大数据流式写(百万行推荐)
*/
public static <T> void writeStream(String filePath,
Class<T> clazz,
Consumer<ExcelWriter> writerConsumer) {
ExcelWriter writer = EasyExcel.write(filePath, clazz)
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
.registerWriteHandler(new ExcelCellWriteWidthConfig())
.build();
try {
writerConsumer.accept(writer);
} finally {
writer.finish();
}
}
// 调用样例
// EasyExcelUtil.writeStream(
// "D:/big.xlsx",
// UserExportVO.class,
// writer -> {
//
// for (int i = 0; i < 100; i++) {
//
// List<UserExportVO> page =
// userService.queryPage(i);
//
// EasyExcelUtil.writeBatchSheet(
// writer,
// page,
// 0,
// "用户表"
// );
// }
// }
//);
/**
* ⭐ 分批写 sheet
*/
public static <T> void writeBatchSheet(ExcelWriter writer,
List<T> data,
int sheetNo,
String sheetName) {
WriteSheet sheet = EasyExcel.writerSheet(sheetNo, sheetName)
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
.registerWriteHandler(new ExcelCellWriteWidthConfig())
.build();
writer.write(data, sheet);
}
/**
* ⭐⭐⭐ 核心流式分页处理(真正生产级)
* ⭐⭐⭐ 不关心读写,只需要处理数据即可,包含excel文件的读写
*/
private static <IN, OUT> void processStream(
InputStream inputStream,
OutputStream outputStream,
Class<IN> inClass,
Class<OUT> outClass,
Function<List<IN>, List<OUT>> mapper,
int pageSize) {
ExcelWriter writer = EasyExcel.write(outputStream, outClass)
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
.registerWriteHandler(new ExcelCellWriteWidthConfig())
.build();
WriteSheet sheet = EasyExcel.writerSheet("result").build();
EasyExcel.read(inputStream, inClass,
new PageReadListener<IN>(pageData -> {
List<OUT> result = mapper.apply(pageData);
writer.write(result, sheet);
}, pageSize))
.sheet()
.doRead();
writer.finish();
}
/**
* =========================
* ⭐ 本地文件路径
* =========================
*/
public static <IN, OUT> void processByFilePath(
String inputFile,
String outputFile,
Class<IN> inClass,
Class<OUT> outClass,
Function<List<IN>, List<OUT>> mapper,
int pageSize) throws Exception {
try (InputStream in = new FileInputStream(inputFile);
OutputStream out = new FileOutputStream(outputFile)) {
processStream(in, out, inClass, outClass, mapper, pageSize);
}
}
/**
* =========================
* ⭐ URL 网络文件
* =========================
*/
public static <IN, OUT> void processByUrl(
String fileUrl,
String outputFile,
Class<IN> inClass,
Class<OUT> outClass,
Function<List<IN>, List<OUT>> mapper,
int pageSize) throws Exception {
try (InputStream in = new URL(fileUrl).openStream();
OutputStream out = new FileOutputStream(outputFile)) {
processStream(in, out, inClass, outClass, mapper, pageSize);
}
}
/**
* =========================
* ⭐ MultipartFile(Web上传)
* =========================
*/
public static <IN, OUT> void processByMultipart(
MultipartFile file,
String outputFile,
Class<IN> inClass,
Class<OUT> outClass,
Function<List<IN>, List<OUT>> mapper,
int pageSize) throws Exception {
try (InputStream in = file.getInputStream();
OutputStream out = new FileOutputStream(outputFile)) {
processStream(in, out, inClass, outClass, mapper, pageSize);
}
}
/**
* =========================
* ⭐ InputStream 通用入口
* =========================
*/
public static <IN, OUT> void processByStream(
InputStream inputStream,
OutputStream outputStream,
Class<IN> inClass,
Class<OUT> outClass,
Function<List<IN>, List<OUT>> mapper,
int pageSize) {
processStream(inputStream, outputStream, inClass, outClass, mapper, pageSize);
}
/**
* 自动计算列宽
*
*/
private static class ExcelCellWriteWidthConfig extends AbstractColumnWidthStyleStrategy {
private final Map<Integer, Map<Integer, Integer>> CACHE = new HashMap<>();
@Override
protected void setColumnWidth(WriteSheetHolder writeSheetHolder, List<WriteCellData<?>> cellDataList, Cell cell, Head head, Integer integer, Boolean isHead) {
boolean needSetWidth = isHead || !CollectionUtils.isEmpty(cellDataList);
if (needSetWidth) {
Map<Integer, Integer> maxColumnWidthMap = CACHE.computeIfAbsent(writeSheetHolder.getSheetNo(), k -> new HashMap<>());
Integer columnWidth = this.dataLength(cellDataList, cell, isHead);
// 单元格文本长度大于60换行
if (columnWidth >= 0) {
if (columnWidth > 60) {
columnWidth = 60;
}
Integer maxColumnWidth = maxColumnWidthMap.get(cell.getColumnIndex());
if (maxColumnWidth == null || columnWidth > maxColumnWidth) {
maxColumnWidthMap.put(cell.getColumnIndex(), columnWidth);
Sheet sheet = writeSheetHolder.getSheet();
sheet.setColumnWidth(cell.getColumnIndex(), columnWidth * 2 * 256);
}
}
}
}
/**
* 计算长度
*/
private Integer dataLength(List<WriteCellData<?>> cellDataList, Cell cell, Boolean isHead) {
if (isHead) {
return cell.getStringCellValue().getBytes().length;
} else {
CellData<?> cellData = cellDataList.get(0);
CellDataTypeEnum type = cellData.getType();
if (type == null) {
return -1;
} else {
switch (type) {
case STRING:
// 换行符(数据需要提前解析好)
int index = cellData.getStringValue().indexOf("\n");
return index != -1 ?
cellData.getStringValue().substring(0, index).getBytes().length + 1 : cellData.getStringValue().getBytes().length + 1;
case BOOLEAN:
return cellData.getBooleanValue().toString().getBytes().length;
case NUMBER:
return cellData.getNumberValue().toString().getBytes().length;
default:
return -1;
}
}
}
}
}
}
举例
定义exel文件数据类,定义数据处理函数即可。
导入类
java
package org.test.exceldemo.excel;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
/**
* 导入数据类,对应excel表格字段
*/
@Data
public class UserImportVO {
@ExcelProperty("User Name")
private String name;
@ExcelProperty("User Age")
private Integer age;
}
导出类
java
package org.test.exceldemo.excel;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
/**
* 导入结果类
*/
@Data
public class UserImportResultVO {
@ExcelProperty("User Name")
private String name;
@ExcelProperty("User Age")
private Integer age;
@ExcelProperty("Import Result")
private String result;
}
数据处理函数类
java
package org.test.exceldemo.excel;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
*
*/
@Service
public class TestExcelDemoService {
/**
* excel数据处理类
* @param list excel数据
* @param globalUnique 全局唯一
* @return 处理结果
*/
public List<UserImportResultVO> convertPage(List<UserImportVO> list, Set<String> globalUnique) {
List<UserImportResultVO> result = new ArrayList<>();
for (UserImportVO in : list) {
UserImportResultVO out = new UserImportResultVO();
BeanUtils.copyProperties(in, out);
// 判断数据重复
if (!globalUnique.add(in.getName())){
out.setResult("重复数据");
result.add(out);
continue;
}
// 数据处理逻辑
// 处理结果
out.setResult("success");
result.add(out);
}
return result;
}
}
模拟请求处理类
java
@RestController
@RequestMapping("/")
public class TestExcelController {
@Resource
private TestExcelDemoService testExcelDemoService;
@PostMapping(path = "upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public String uploadFile(@RequestParam("file") MultipartFile file) throws Exception {
//通过文件后缀名或文件魔数进行更可靠的校验
String originalFilename = file.getOriginalFilename();
// 只处理excel文件
if (originalFilename != null && !originalFilename.toLowerCase().endsWith(".xlsx")) {
return "仅支持 EXCEL 文件";
}
Set<String> globalUnique = new HashSet<>();
EasyExcelPipelineUtil.processByMultipart(file,
"D:/output.xlsx",
UserImportVO.class,
UserImportResultVO.class, list -> testExcelDemoService.convertPage(list, globalUnique), 1000);
return "上传成功";
}
}
测试数据
模拟请求

请求文件数据

生成导出结果文件数据
