最近做了一个excel 导入相关的功能,因为导入的excel 格式比较复杂,并且还有图片,做的时候也是费了一点功夫,因此整理了这篇文章
推荐大家看官方文档,但是我在访问官方文档时报了权限不足
搜了一下好像是说截至2024年11月18日已经无法打开了,但我又找到了个优化版在语雀上,这里分享给大家
EasyPoi官方文档优化版: https://www.yuque.com/guomingde/easypoi
读前须知
本文采用的是Spring boot + EasyPoi ,同时使用了lombak 来简化实体类配置。使用Spring 或者其他框架,对应依赖需要修改,代码部分是一样的
EasyPoi 是什么?
简单说,EasyPoi 是一个基于 Apache POI 的封装库,它的核心哲学就是:让Excel导入导出变得跟呼吸一样简单!
核心优势:
- 注解驱动:一个注解搞定一个字段,告别繁琐的API。
- 功能强大:轻松处理一对多、图片、公式、下拉框等。
- 代码极简:比原生POI代码量减少90%!
使用步骤
导入表格示例:
第一步:Maven依赖
springboot整合EasyPoi
<!-- EasyPoi 核心依赖 -->
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-spring-boot-starter</artifactId>
<version>4.5.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
spring 下依赖包配置
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-base</artifactId>
<version>4.1.0</version>
</dependency>
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-web</artifactId>
<version>4.1.0</version>
</dependency>
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-annotation</artifactId>
<version>4.1.0</version>
</dependency>
第二步:创建实体类
这是最关键的一步!我们用注解来告诉EasyPoi数据结构。
1. 创建主实体(订单)
import cn.afterturn.easypoi.excel.annotation.Excel;
import cn.afterturn.easypoi.excel.annotation.ExcelCollection;
import cn.afterturn.easypoi.excel.annotation.ExcelTarget;
import java.util.List;
@Data
public class OrderDTO {
@Excel(name = "订单编号", width = 20)
private String orderNo;
@Excel(name = "客户名称")
private String customerName;
// 👇 核心注解!处理一对多关系
@ExcelCollection(name = "订单商品") // 这里的name在导入时没啥用,但导出时是表头
private List<OrderItemDTO> items;
}
划重点:
@ExcelCollection
: 就是处理一对多的关键配置! 它会自动将Excel中连续的多行数据,根据主实体的字段(如订单编号)进行分组,封装成一个List。
2. 创建子实体(商品项)
import cn.afterturn.easypoi.excel.annotation.Excel;
import cn.afterturn.easypoi.excel.annotation.ExcelTarget;
@Data
public class OrderItemDTO {
@Excel(name = "商品名称") // 👈 对应Excel列名
private String productName;
@Excel(name = "商品数量")
private Integer quantity;
// 👇 核心注解!处理图片导入
@Excel(name = "商品图片", type = 2, savePath = "uploads/images/")
private String imagePath; // 字段存储的是图片保存后的路径
}
划重点:
@ExcelTarget
: 标记这是一个Excel实体。@Excel(name = "...")
: 将字段与Excel列名绑定。@Excel(type = 2, savePath = "...")
:type = 2
表示这是一个图片类型。savePath
极其重要,它指定了图片上传后保存的服务器路径。
遇到的坑:
在导入图片时,一开始我在 Excel 中使用的是嵌入模式,在导入时一直读不到图片,后来改成浮动式,才能正常读到文件。官网上好像也没有特别说明,如果你也遇到这个问题,可以试着改成浮动试一下。
第三步:编写Controller
import cn.afterturn.easypoi.excel.ExcelImportUtil;
import cn.afterturn.easypoi.excel.entity.ImportParams;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
import java.util.Map;
@RestController
public class ExcelImportController {
@PostMapping("/import/order")
public String importOrder(MultipartFile file) throws Exception {
// 1. 设置导入参数
ImportParams params = new ImportParams();
params.setTitleRows(0); // 标题行,从第0行开始
params.setHeadRows(1); // 表头行,从第1行开始
params.setDataRows(0); // 数据行,从第0行开始(相对于表头)
// 👇 关键!开启一对多导入模式
params.setNeedVerify(true); // 开启验证
params.setGroupFieldIndex(0); // 👈 指定分组依据的列索引(订单编号在第0列)
// 2. 执行导入
// 注意:这里导入的是主实体 OrderDTO
List<OrderDTO> orderList = ExcelImportUtil.importExcel(file.getInputStream(), OrderDTO.class, params);
// 3. 处理结果
for (OrderDTO order : orderList) {
System.out.println("订单编号: " + order.getOrderNo());
System.out.println("客户名称: " + order.getCustomerName());
for (OrderItemDTO item : order.getItems()) {
System.out.println(" - 商品: " + item.getProductName() + ", 数量: " + item.getQuantity() + ", 图片路径: " + item.getImagePath());
}
// ... 在这里进行你的数据库保存操作 ...
}
return "导入成功!共导入 " + orderList.size() + " 个订单。";
}
}
代码解析:
ImportParams
是导入的配置中心,可以设置标题、表头位置等。params.setGroupFieldIndex(0);
这是处理一对多的灵魂! 它告诉EasyPoi:“请看第0列(订单编号),如果这列的值不变,就认为是同一个订单,把后面的数据塞到它的items
列表里!”ExcelImportUtil.importExcel(...)
一行代码,搞定所有解析和封装!图片会自动保存到你在DTO里设置的savePath
,路径赋值给imagePath
字段。
大量数据导入
如果导入的数据量很大的话,直接使用上面的代码,很可能导入后直接内存溢出!这时候就可以使用监听器模式进行优化
创建自定义监听器
/**
* 订单导入监听器
* 核心:实现 IExcelReadListener 接口
* 作用:流式处理每一行数据
*/
@Component
@Slf4j
public class OrderImportListener implements IExcelReadListener<OrderDTO> {
@Autowired
private OrderService orderService;
private List<OrderDTO> orderCache = new ArrayList<>();
private int batchSize = 1000; // 🎯 批处理大小
private int totalCount = 0;
/**
* 核心方法:处理每一行数据
*/
@Override
public void invoke(OrderDTO order, AnalysisContext context) {
totalCount++;
// 数据校验
if (isValidOrder(order)) {
orderCache.add(order);
// 批量保存
if (orderCache.size() >= batchSize) {
saveBatch();
orderCache.clear();
}
} else {
log.warn("数据校验失败,行号: {}", context.getCurrentRowNum());
}
// 进度反馈(每1000行日志一次)
if (totalCount % 1000 == 0) {
log.info(" 已处理: {} 行数据", totalCount);
}
}
/**
* 所有数据解析完成
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 处理最后一批数据
if (!orderCache.isEmpty()) {
saveBatch();
}
log.info("🎉 导入完成!总计处理: {} 行", totalCount);
}
/**
* 🎯批量保存到数据库
*/
private void saveBatch() {
try {
// 这里可以处理图片和一对多关系
orderService.batchSaveOrders(orderCache);
log.info("✅ 批量保存成功,数量: {}", orderCache.size());
} catch (Exception e) {
log.error("❌ 批量保存失败", e);
throw new RuntimeException("批量保存异常");
}
}
/**
* 数据校验
*/
private boolean isValidOrder(OrderDTO order) {
return order != null &&
StringUtils.isNotBlank(order.getOrderNo()) &&
order.getItems() != null && !order.getItems().isEmpty();
}
}
Controller层调用
调用的修改也很简单,调用导入方式时,传入监听器即可
@RestController
@RequestMapping("/api/order")
@Slf4j
public class OrderImportController {
@Autowired
private OrderImportListener orderImportListener;
/**
* 大文件导入接口
*/
@PostMapping("/import-large")
public ResponseEntity<?> importLargeOrder(@RequestParam("file") MultipartFile file) {
try {
// 创建导入参数
ImportParams params = new ImportParams();
params.setTitleRows(0);
params.setHeadRows(1);
// 关键配置:使用监听器模式
ExcelImportUtil.importExcel(
file.getInputStream(),
OrderDTO.class,
params,
orderImportListener // 👈 传入监听器
);
return ResponseEntity.ok("大文件导入已启动,请查看日志获取进度");
} catch (Exception e) {
log.error("大文件导入失败", e);
return ResponseEntity.badRequest().body("导入失败: " + e.getMessage());
}
}
}
总结
EasyPoi让复杂Excel导入变得如此简单:
- ✅ 一对多关系:
@ExcelCollection
+ 分组配置 - ✅ 图片导入:
type=2
+savePath
- ✅ 代码简洁:注解驱动,告别繁琐API
- ✅ 大量数据时使用监听器
从此告别加班,准时下班不是梦!