Files
ccdi/openspec/changes/replace-poi-with-easyexcel/design.md
2026-01-27 17:55:53 +08:00

252 lines
6.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Design: Replace Apache POI with Alibaba EasyExcel
## Architecture Overview
### 当前架构POI
```
Controller
ExcelUtil<T> (1944 行)
├── init() → createWorkbook() → SXSSFWorkbook
├── fillExcelData() → 迭代 list → addCell()
└── importExcel() → WorkbookFactory → 解析整个文件
```
### 目标架构EasyExcel
```
Controller
ExcelUtil<T> (简化版)
├── exportExcel() → EasyExcel.write() → 流式写入
└── importExcel() → EasyExcel.read() + ReadListener → 流式读取
```
## Component Design
### 1. 依赖管理
**ruoyi-common/pom.xml**:
```xml
<!-- 移除 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
</dependency>
<!-- 新增 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.4</version>
</dependency>
```
**pom.xml**:
```xml
<properties>
<easyexcel.version>3.3.4</easyexcel.version>
</properties>
```
### 2. ExcelUtil 核心重构
#### 导出流程
```java
// 当前实现 (POI)
public void exportExcel(HttpServletResponse response, List<T> list, String sheetName) {
init(list, sheetName, title, Type.EXPORT);
writeSheet(); // 一次性写入所有数据
wb.write(response.getOutputStream());
}
// 新实现 (EasyExcel)
public void exportExcel(HttpServletResponse response, List<T> list, String sheetName) {
EasyExcel.write(response.getOutputStream(), clazz)
.sheet(sheetName)
.head(headGenerator) // 动态表头生成
.registerWriteHandler(styleStrategy) // 样式策略
.registerWriteHandler(mergeStrategy) // 合并策略
.doWrite(list); // 流式写入
}
```
#### 导入流程
```java
// 当前实现 (POI)
public List<T> importExcel(InputStream is, int titleNum) {
wb = WorkbookFactory.create(is); // 加载整个文件
// 遍历所有行,解析到内存 List
for (int i = titleNum + 1; i <= rows; i++) { ... }
return list;
}
// 新实现 (EasyExcel)
public List<T> importExcel(InputStream is, int titleNum) {
List<T> dataList = new ArrayList<>();
EasyExcel.read(is, clazz, new AnalysisEventListener<T>() {
@Override
public void invoke(T data, AnalysisContext context) {
// 逐行读取,不占用大量内存
dataList.add(data);
}
}).sheet().headRowNumber(titleNum).doRead();
return dataList;
}
```
### 3. 注解适配
#### Excel 注解调整
```java
// 当前 - 依赖 POI 类型
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.IndexedColors;
public @interface Excel {
HorizontalAlignment align() default HorizontalAlignment.CENTER;
IndexedColors headerBackgroundColor() default IndexedColors.GREY_50_PERCENT;
// ...
}
// 新实现 - EasyExcel 兼容
import com.alibaba.excel.enums.HorizontalAlignmentEnum;
public @interface Excel {
HorizontalAlignmentEnum align() default HorizontalAlignmentEnum.CENTER;
String headerBackgroundColor() default " grey_50_percent"; // 简化为字符串
// ...
}
```
### 4. 自定义处理器实现
#### 样式处理器
```java
public class CustomStyleStrategy extends AbstractCellStyleStrategy {
@Override
protected void setContentCellStyle(Cell cell, Head head, Integer relativeRowIndex) {
// 基于 @Excel 注解设置样式
Workbook workbook = cell.getSheet().getWorkbook();
Field field = getField(head.getHeadNameList());
Excel excel = field.getAnnotation(Excel.class);
CellStyle style = createStyle(workbook, excel);
cell.setCellStyle(style);
}
}
```
#### 合并策略
```java
public class MergeStrategy implements CellWriteHandler {
@Override
public void afterCellDispose(CellWriteHandlerContext context) {
// 处理 needMerge 字段的合并
if (needMerge(context)) {
context.getWriteSheetHolder().getSheet()
.addMergedRegion(region);
}
}
}
```
#### 自定义数据处理器适配
```java
// 原接口
public interface ExcelHandlerAdapter {
Object format(Object value, String[] args, Cell cell, Workbook wb);
}
// 适配 EasyExcel WriteHandler
public class CustomWriteHandler implements CellWriteHandler {
private ExcelHandlerAdapter handler;
private String[] args;
@Override
public void afterCellDispose(CellWriteHandlerContext context) {
Object value = handler.format(context.getCellData(), args,
context.getCell(), context.getWriteWorkbookHolder().getWorkbook());
// 应用处理后的值
}
}
```
## Migration Strategy
### 阶段 1双模式支持可选
```java
public class ExcelUtil<T> {
private boolean useEasyExcel = true; // 配置开关
public void exportExcel(...) {
if (useEasyExcel) {
exportWithEasyExcel(...);
} else {
exportWithPoi(...); // 保留旧实现
}
}
}
```
### 阶段 2直接替换推荐
1. 更新依赖
2. 重写 ExcelUtil
3. 更新注解
4. 逐模块测试
## Testing Strategy
### 单元测试
```java
@Test
void testLargeExport() {
List<DemoData> data = generateData(100_000);
ExcelUtil<DemoData> util = new ExcelUtil<>(DemoData.class);
// 验证内存占用
util.exportExcel(response, data, "test");
}
@Test
void testLargeImport() {
File file = createLargeExcel(10_000_000); // 1000万行
ExcelUtil<DemoData> util = new ExcelUtil<>(DemoData.class);
List<DemoData> data = util.importExcel(new FileInputStream(file));
// 验证解析正确性和内存占用
}
```
### 集成测试
- 测试所有现有 Controller 的导入导出接口
- 验证样式、合并、图片等功能
## Performance Considerations
| 操作 | POI (当前) | EasyExcel (目标) |
|------|-----------|-----------------|
| 10万行导出 | ~1GB 内存 | ~100MB 内存 |
| 100万行导出 | OOM 风险 | ~200MB 内存 |
| 100MB 文件导入 | ~2GB 内存 | ~150MB 内存 |
| 单元格字符限制 | 32,767 | 无限制 |
## Rollback Plan
如果出现问题,可以通过以下步骤回退:
1. 恢复 `ruoyi-common/pom.xml` 中的 POI 依赖
2. 恢复 `ExcelUtil.java` 和相关文件
3. 重新编译部署
建议在分支上进行完整测试后再合并到主分支。