使用poi导出嵌套对象(一对多、多对多)

  • 设计该项目的初衷是为了解决嵌套对象自动生成表头并自动合并行的问题,同时在考虑如果写代码跟操作excel一样简单易懂那就更好了,如我先设置全局样式,在设置表头样式,列格式,在一行一行添加数据等等,所以才有以下代码的实现。
  • 通过匹配单元格可以自定义单元格样式,处理值、值格式化、批注等。
  • 通过注解支持excel原生格式化、列宽、表头样式、表头批注等。
  • 支持多个嵌套对象、无限嵌套对象等等(自动合并单元格->横向扩展纵向合并),你也可以实现其他任何样式或格式。
  • 对poi无侵入性,支持构建普通行、多个sheet等,每个sheet可以单独设置样式等等。最终生成原生workbook。 拿到原生workbook后你想做其他任何事情请自便。

代码已开源:GitHub - yyfcode/fastexcel: 快速读写excel,代码简洁。支持嵌套对象导出。

更多样例:   GitHub - yyfcode/fastexcel-demo: 读写示例

疑问加群:钉钉群:2880006273 

<dependency>
    <groupId>com.jeeapp</groupId>
    <artifactId>fastexcel</artifactId>
    <version>0.0.3</version>
</dependency>
/**
 * @author Justice
 */
@Data
@AllArgsConstructor
public class Class {

	@ExcelProperty(name = "班级", format = "#,##", column = 1)
	private String name;

	@ExcelProperty(name = "班主任", column = 2, width = 15)
	private String teacher;

	@ExcelProperty(name = "班级人数", format = "#,##", column = 3)
	private Long number;

	@ExcelProperty(name = "教师列表", column = 4,
header = @Header(fillForegroundColor = IndexedColors.GREY_25_PERCENT))
	private List<Teacher> teachers;

	@ExcelProperty(name = "学生列表", column = 5,
header = @Header(fillForegroundColor = IndexedColors.ORANGE))
	private List<Student> students;
}

/**
 * @author Justice
 */
@Data
@AllArgsConstructor
public class Family {

	@ExcelProperty(name = "家长类型", column = 0)
	private String type;

	@ExcelProperty(name = "家长姓名", column = 1)
	private String name;
}

/**
 * @author Justice
 */
@Data
@AllArgsConstructor
public class Student {

	@ExcelProperty(name = "姓名", column = 1)
	private String name;

	@ExcelProperty(name = "性别", format = "[=1]\"男\";[=2]\"女\"", column = 2)
	private Integer sex;

	@ExcelProperty(name = "出生日期", format = "yyyy-MM-dd", column = 3, width = 15)
	private Date birthday;

	@ExcelProperty(name = "语文", format = "#,##0.00", column = 4,header = @Header(fillForegroundColor = IndexedColors.LIGHT_GREEN))
	private BigDecimal chinese;

	@ExcelProperty(name = "数学", format = "#,##0.00", column = 5,header = @Header(fillForegroundColor = IndexedColors.LIGHT_BLUE))
	private Double math;

	@ExcelProperty(name = "英语", format = "#,##0.00", column = 6,header = @Header(fillForegroundColor = IndexedColors.LIGHT_YELLOW))
	private Float english;

	@ExcelProperty(name = "家庭成员", column = 7)
	private List<Family> families;
}

/**
 * @author Justice
 */
@Data
@AllArgsConstructor
public class Teacher {

	@ExcelProperty(name = "老师姓名", column = 0)
	private String name;

	@ExcelProperty(name = "教学内容", column = 1)
	private String type;
}
public static void main(String[] args) throws IOException {
		Family f1 = new Family("父亲", "张三父");
		Family f2 = new Family("母亲", "张三母");

		Family f3 = new Family("父亲", "李四父");
		Family f4 = new Family("母亲", "李四母");
		Family f5 = new Family("爷爷", "李四爷");
		Family f6 = new Family("奶奶", "李四奶");

		Family f7 = new Family("父亲", "小红父");
		Family f8 = new Family("母亲", "小红母");
		Family f9 = new Family("妹妹", "小红妹");
		Family f10 = new Family("哥哥", "小红哥");

		Family f11 = new Family("父亲", "小明父");
		Family f12 = new Family("母亲", "小明母");

		Family f13 = new Family("父亲", "小兰父");
		Family f14 = new Family("母亲", "小兰母");
		Family f15 = new Family("弟弟", "小兰弟");

		Teacher t1 = new Teacher("张老师", "语文");
		Teacher t2 = new Teacher("李老师", "语文");
		Teacher t3 = new Teacher("王老师", "数学");
		Teacher t4 = new Teacher("合老师", "英语");
		Teacher t5 = new Teacher("黄老师", "音乐");
		Teacher t6 = new Teacher("牛老师", "体育");

		Student st1 = new Student("张三", 1, new Date(), BigDecimal.valueOf(59.1), 82d, 80f, Arrays.asList(f1, f2));
		Student st2 = new Student("李四", 1, new Date(), BigDecimal.valueOf(69), 50.4d, 90.2f, Arrays.asList(f3, f4, f5, f6));
		Student st3 = new Student("小红", 2, new Date(), BigDecimal.valueOf(100), 70.32d, 92f, Arrays.asList(f7, f8, f9, f10));
		Student st4 = new Student("小明", 1, new Date(), BigDecimal.valueOf(42), 63.34d, 50f, Arrays.asList(f11, f12));
		Student st5 = new Student("小兰", 2, new Date(), BigDecimal.valueOf(80), 84d, 90f, Arrays.asList(f13, f14, f15));

		Class cl1 = new Class("1班", "李老师", 3L, Arrays.asList(t1, t5, t3, t4,t6), Arrays.asList(st1, st2, st3));
		Class cl2 = new Class("2班", "王老师", 2L, Arrays.asList(t2, t5, t6), Arrays.asList(st4, st5));

		Workbook workbook = new WorkbookBuilder(new SXSSFWorkbook())
			.setDefaultRowHeight(20)
			// 全局样式
			.matchingAll()
			.setFontHeight(12)
			.setFontName("微软雅黑")
			.setVerticalAlignment(VerticalAlignment.CENTER)
			.setAlignment(HorizontalAlignment.CENTER)
			.setWrapText(true)
			.addCellStyle()
			.matchingCell(cell -> {
				// 只匹配分数列
				if (cell == null || cell.getColumnIndex() < 7) {
					return false;
				}
				// 小于60分的标红
				String cellValue = CellUtils.getCellValue(cell);
				if (NumberUtils.isCreatable(cellValue)) {
					return Double.parseDouble(cellValue) < 60D;
				}
				return false;
			})
			.setStrikeout(true)
			.setFontColor(IndexedColors.RED.getIndex())
			.addCellStyle()
			.matchingCell(cell -> {
				// 只匹配分数列
				if (cell == null || cell.getColumnIndex() < 7) {
					return false;
				}
				// 等于100分的标蓝
				String cellValue = CellUtils.getCellValue(cell);
				if (NumberUtils.isCreatable(cellValue)) {
					return Double.parseDouble(cellValue) == 100D;
				}
				return false;
			})
			.setFontColor(IndexedColors.BLUE.getIndex())
			.addCellStyle()
			.createSheet("成绩单")
            .createRow("班级概况")
			.addCellRange(0, 0, 0, 12)
			.merge()
			.rowType(Class.class)
			.createHeader()// 可以指定导出部分字段,不传代表导出所有
			.createRows(Arrays.asList(cl1, cl2))
			.end()
			.build();

		try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
			workbook.write(os);
			File resFile = new File("src/test/resources/test.xlsx");
			if (resFile.isFile()) {
				resFile.delete();
			}
			FileUtils.writeByteArrayToFile(resFile, os.toByteArray());
		}
	}

最终效果:

### 如何使用 EasyExcel 实现一对的数据导入和导出 #### 1. 明确需求背景 在实际开发过程中,经常会遇到需要处理一对关系的场景。例如,在一个订单系统中,一个订单可能对应个商品明细项。这种情况下,传统的单表 Excel 数据结构无法满足需求,因此需要设计一种能够支持一对关系的解决方案。 通过分析提供的参考资料[^1][^2][^3][^4],可以总结出以下几点关于 EasyExcel 的特性: - **高效性**:相比于 POI 库,EasyExcel 对于大数据量的操作更加友好,占用内存较少。 - **灵活性**:可以通过自定义监听器、实体类注解等方式实现复杂的业务逻辑。 - **扩展性强**:适用于种场景下的数据导入与导出。 --- #### 2. 技术实现思路 ##### (1) 配置 Maven 依赖 为了使用 EasyExcel,首先需要引入其对应的 Maven 依赖: ```xml <dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>3.0.5</version> </dependency> ``` 上述版本号可以根据实际情况调整[^4]。 --- ##### (2) 设计实体类 假设存在如下一对关系模型: - `Order` 表示订单信息; - `OrderDetail` 表示订单中的商品详情列表。 以下是两个实体类的设计示例: ```java // 订单实体类 @Data public class Order { private String orderId; // 订单ID private String customerName; // 客户名称 private List<OrderDetail> details; // 商品详情列表 } // 商品详情实体类 @Data public class OrderDetail { private String productId; // 商品ID private String productName; // 商品名称 private int quantity; // 数量 } ``` 注意:这里的关键在于 `Order` 类中包含了 `List<OrderDetail>` 属性来表示一对关系。 --- ##### (3) 自定义解析策略 由于默认的 EasyExcel 不直接支持嵌套对象一对解析,因此需要借助自定义监听器完成这一功能。 ###### a. 编写读取监听器 创建一个继承自 `AnalysisEventListener<T>` 的监听器用于捕获每一行的数据并组装成最终的对象结构。 ```java public class OrderDataListener extends AnalysisEventListener<Map<Integer, String>> { private List<Order> orderList = new ArrayList<>(); private Map<String, Order> currentOrders = new HashMap<>(); @Override public void invoke(Map<Integer, String> data, AnalysisContext context) { // 假设第一列是订单ID,第二列是客户名,第三列为商品ID... String orderId = data.get(0); String customerName = data.get(1); // 获取当前订单实例 Order order = currentOrders.computeIfAbsent(orderId, id -> { Order o = new Order(); o.setOrderId(id); o.setCustomerName(customerName); o.setDetails(new ArrayList<>()); return o; }); // 如果有商品信息,则加入到订单的商品列表中 if (data.containsKey(2)) { // 判断是否存在商品字段 OrderDetail detail = new OrderDetail(); detail.setProductId(data.get(2)); detail.setProductName(data.get(3)); detail.setQuantity(Integer.parseInt(data.get(4))); order.getDetails().add(detail); } } @Override public void doAfterAllAnalysed(AnalysisContext context) { this.orderList.addAll(currentOrders.values()); // 将所有订单保存下来 } public List<Order> getOrders() { return orderList; } } ``` 以上代码实现了将每一条记录按照订单 ID 进行分组,并动态构建出完整的 `Order` 和 `OrderDetail` 结构。 --- ##### (4) 导入方法实现 编写服务端接口用于接收上传文件并将其中的内容转换为 Java 对象。 ```java @PostMapping("/import") public ResponseEntity<List<Order>> importExcel(@RequestParam("file") MultipartFile file) throws IOException { InputStream inputStream = file.getInputStream(); // 创建监听器实例 OrderDataListener listener = new OrderDataListener(); // 使用 EasyExcel 解析文件 EasyExcel.read(inputStream).headMap(null).sheet().doRead(listener); // 返回解析后的订单集合 return ResponseEntity.ok(listener.getOrders()); } ``` --- ##### (5) 导出方法实现 对于导出操作,同样需要考虑如何将一对的数据展平以便存储在一个二维表格中。通常的做法是在同一张工作簿下按照行顺序依次排列父级和子级的信息。 下面是一个简单的导出示例: ```java @GetMapping("/export") public void export(HttpServletResponse response) throws IOException { // 设置响应头 response.setContentType("application/vnd.ms-excel"); response.setCharacterEncoding("utf-8"); String fileName = URLEncoder.encode("orders", "UTF-8").replaceAll("\\+", "%20"); response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx"); // 构造测试数据 List<Order> orders = generateSampleData(); // 此处省略具体生成逻辑 // 批量写出数据 try ( BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream()) ) { WriteSheet writeSheet = EasyExcel.writerSheet("Sheet1").build(); EasyExcel.write(out) .registerWriteHandler(new CustomTableStyle()) // 可选样式处理器 .head(getHeadRows()) // 头部配置 .sheet() .doWrite(convertToFlatRecords(orders)); // 平铺数据 } } private List<List<Object>> convertToFlatRecords(List<Order> orders) { List<List<Object>> result = new ArrayList<>(); for (Order order : orders) { for (OrderDetail detail : order.getDetails()) { result.add(Arrays.asList( order.getOrderId(), order.getCustomerName(), detail.getProductId(), detail.getProductName(), detail.getQuantity())); } } return result; } ``` 此处的核心是对原始对象进行扁平化处理后再逐条写入目标单元格位置。 --- #### 3. 注意事项 - 确保输入模板格式固定且清晰明了,便于后续维护人员理解。 - 当涉及大量数据时需启用流式 API 来降低内存消耗风险。 - 若某些字段允许为空则应在验证阶段加以判断以免引发异常。 ---
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值