在企业级Java开发中,数据导出为Excel是常见的需求。尤其在订单管理系统中,往往需要将一个订单及其包含的多个物料信息导出为结构清晰的Excel表格。本文将详细介绍如何使用 EasyPOI 实现这一功能,重点讲解其强大的模板语法表达式语言,并通过完整代码示例带你从零实现订单+多物料导出。
一、EasyPOI 简介
EasyPOI 是基于 Apache POI 的封装库,极大简化了 Java 操作 Excel 的复杂度。它支持:
- 简单对象导出
- 模板导出(支持表达式)
- 多Sheet导出
- 图片导出
- 合并单元格等高级功能
本文重点使用 模板导出功能(TemplateExportService) ,通过 .xlsx
模板文件定义样式和结构,结合 EasyPOI 的表达式语言动态填充数据。
二、需求分析:订单+物料导出
我们有一个订单(Order),每个订单包含多个物料(Material)。需要导出如下结构的Excel:
订单编号 | 订单名称 | 客户名称 | 下单日期 |
---|---|---|---|
O20250731001 | 采购订单 | 某某公司 | 2025-07-31 |
物料编号 | 物料名称 | 数量 | 单价 |
M001 | 螺丝钉 | 100 | 0.5 |
M002 | 垫片 | 200 | 0.3 |
合计 |
要求:订单信息在上,物料列表在下,自动合并订单信息行,金额合计自动计算。
三、EasyPOI 模板语法表达式详解
EasyPOI 支持在Excel模板中使用 ${}
语法进行数据绑定和逻辑控制。常用表达式如下:
1. 基本变量引用
bash
${order.orderNo}
${order.customerName}
${order.orderDate}
2. 集合循环(核心!)
使用 fe:for
语法遍历集合:
ruby
{{fe:for mat in order.materials}}
${mat.materialNo}
${mat.materialName}
${mat.quantity}
${mat.unitPrice}
${mat.amount}
{{fe:end for}}
fe:for 变量 in 集合路径
:开始循环fe:end for
:结束循环- 可嵌套使用
3. 条件判断
css
{{fe:if order.status == 'PAID'}}
已付款
{{fe:else}}
未付款
{{fe:end if}}
4. 函数调用(内置函数)
xml
${fe:formatDate(order.orderDate, "yyyy-MM-dd")} <!-- 格式化日期 -->
${fe:currency(order.totalAmount)} <!-- 货币格式 -->
${fe:sum(order.materials, "amount")} <!-- 求和 -->
5. 单元格合并控制
通过 fe:merge
实现自动合并:
sql
{{fe:merge size="4" start="1" direction="down"}}
${order.orderNo}
{{fe:end merge}}
表示向下合并4行(包括当前行)
四、模板设计(order_template.xlsx)
在 resources/templates/
下创建 order_template.xlsx
,设计如下:
A | B | C | D | E |
---|---|---|---|---|
订单编号 | ${order.orderNo} | 客户名称 | ${order.customerName} | |
订单名称 | ${order.orderName} | 下单日期 | ${fe:formatDate(order.orderDate, "yyyy-MM-dd")} | |
物料编号 | 物料名称 | 数量 | 单价 | 金额 |
{{fe:for mat in order.materials}} | ||||
${mat.materialNo} | ${mat.materialName} | ${mat.quantity} | ${mat.unitPrice} | ${mat.amount} |
{{fe:end for}} | ||||
合计 | ${fe:sum(order.materials, "amount")} |
注意:合并订单信息行可通过
fe:merge
实现,此处简化。
五、Java代码实现
1. 引入Maven依赖
xml
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-base</artifactId>
<version>4.4.0</version>
</dependency>
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-web</artifactId>
<version>4.4.0</version>
</dependency>
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-annotation</artifactId>
<version>4.4.0</version>
</dependency>
2. 定义实体类
kotlin
// Material.java
public class Material {
private String materialNo;
private String materialName;
private Integer quantity;
private Double unitPrice;
private Double amount; // quantity * unitPrice
// 构造器、Getter、Setter 省略
}
java
// Order.java
import java.util.Date;
import java.util.List;
public class Order {
private String orderNo;
private String orderName;
private String customerName;
private Date orderDate;
private List<Material> materials;
// 构造器、Getter、Setter 省略
}
3. 导出服务类
java
// OrderExportService.java
import cn.afterturn.easypoi.excel.entity.TemplateExportParams;
import cn.afterturn.easypoi.view.PoiBaseView;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
@Service
public class OrderExportService {
public void exportOrder(Order order, HttpServletResponse response) {
// 1. 配置模板路径
String templatePath = "templates/order_template.xlsx";
File templateFile = new File(this.getClass().getClassLoader().getResource(templatePath).getFile());
// 2. 设置模板参数
TemplateExportParams params = new TemplateExportParams();
params.setTemplateUrl(templateFile.getAbsolutePath());
// 3. 准备数据模型
Map<String, Object> map = new HashMap<>();
map.put("order", order); // 数据绑定到模板中的 ${order.xxx}
try {
// 4. 执行导出
ModelAndView mv = new ModelAndView(new PoiBaseView());
mv.addObject(PoiBaseView.TEMPLATE_URL, templatePath);
mv.addObject(PoiBaseView.DATA_MAP, map);
mv.addObject(PoiBaseView.PARAMS, params);
mv.addObject(PoiBaseView.FILE_NAME, "订单_" + order.getOrderNo());
// 设置响应头
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition", "attachment; filename=" + new String(("订单_" + order.getOrderNo() + ".xlsx").getBytes(), "ISO8859-1"));
// 使用 PoiBaseView 渲染并输出
PoiBaseView.render(mv, request, response);
} catch (Exception e) {
e.printStackTrace();
// 处理异常
}
}
}
注意:
PoiBaseView
是 EasyPOI 提供的 Spring MVC 视图类,需确保 Spring 环境。
4. 控制器调用
less
// OrderController.java
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private OrderExportService exportService;
@GetMapping("/export/{id}")
public void exportOrder(@PathVariable String id, HttpServletResponse response) {
// 模拟查询订单数据
Order order = buildMockOrder(id);
exportService.exportOrder(order, response);
}
private Order buildMockOrder(String id) {
Order order = new Order();
order.setOrderNo("O20250731001");
order.setOrderName("采购订单");
order.setCustomerName("某某公司");
order.setOrderDate(new Date());
List<Material> materials = Arrays.asList(
new Material("M001", "螺丝钉", 100, 0.5, 50.0),
new Material("M002", "垫片", 200, 0.3, 60.0)
);
order.setMaterials(materials);
return order;
}
}
六、关键点分析
1. 模板路径问题
- 模板文件应放在
resources
目录下,使用ClassLoader
加载。 - 确保路径正确,避免
FileNotFoundException
。
2. 表达式语法严格性
${}
中的变量名必须与 Java 对象属性一致(区分大小写)。- 集合循环必须配对
{{fe:for}}
和{{fe:end for}}
。
3. 自动合并单元格
若需合并订单信息行(如订单编号跨2行),可在模板中:
sql
{{fe:merge size="2" start="0" direction="down"}}
${order.orderNo}
{{fe:end merge}}
4. 金额合计
使用 ${fe:sum(order.materials, "amount")}
自动计算物料金额总和,无需在Java中额外计算。
七、常见问题与解决方案
问题 | 原因 | 解决方案 |
---|---|---|
模板找不到 | 路径错误 | 使用 getClassLoader().getResource() |
数据未填充 | 变量名不匹配 | 检查 ${} 中的字段名 |
循环不生效 | fe:for 语法错误 |
检查开始/结束标签是否配对 |
合并失败 | fe:merge 参数错误 |
检查 size 和 direction |
八、总结
通过 EasyPOI 的模板导出功能,我们可以:
- 分离样式与逻辑:模板定义样式,Java处理数据。
- 提升开发效率:无需手动创建单元格、设置样式。
- 支持复杂结构:轻松实现一对多数据导出。
- 灵活控制:通过表达式语言实现循环、条件、计算等。
EasyPOI 的模板语法虽然简单,但功能强大,特别适合订单、报表等结构化数据导出场景。掌握其表达式语言是高效开发的关键。
欢迎关注我的技术博客,持续分享Java实战经验!