252 lines
6.3 KiB
Markdown
252 lines
6.3 KiB
Markdown
|
|
# 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. 重新编译部署
|
|||
|
|
|
|||
|
|
建议在分支上进行完整测试后再合并到主分支。
|