SpringCloud微服务:EasyExcel实现Excel的优雅导入导出
在企业级应用开发中,Excel的导入导出功能几乎是必不可少的。传统的实现方式往往存在代码耦合度高、扩展性差、性能低下等问题。本文将基于SpringCloud微服务架构,介绍一种优雅的Excel导入导出解决方案。
一、项目架构设计
本项目采用模块化设计,将Excel导入导出功能拆分为独立的微服务:
spring-cloud-excel/
├── excel-common # 公共模块(实体类、工具类、配置)
├── excel-export # 导出服务
├── excel-import # 导入服务
└── excel-test # 测试服务
架构优势:
- • 易于扩展:新增业务类型只需添加对应的Processor
二、核心设计模式
2.1 策略模式 + 工厂模式
通过自定义注解实现策略的自动注册,避免硬编码:
/**
* 导入业务注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ExcelImport {
ImportBusiness value();
}
/**
* 导入处理器工厂
*/
@Component
public class ImportProcessorFactory implements BeanPostProcessor {
private static final Map<ImportBusiness, ImportProcessor> IMPORT_STRATEGY_MAP = new ConcurrentHashMap<>();
public ImportProcessor getImportProcessor(ImportBusiness importBusiness) {
if (!IMPORT_STRATEGY_MAP.containsKey(importBusiness)) {
throw new BusinessException("The imported type does not exist.");
}
return IMPORT_STRATEGY_MAP.get(importBusiness);
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
// 只对实现了 ImportProcessor 的类做操作
if (bean instanceof ImportProcessor importProcessor) {
Class<?> clazz = AopUtils.getTargetClass(bean);
ExcelImport annotation = clazz.getAnnotation(ExcelImport.class);
// 绑定对应关系
IMPORT_STRATEGY_MAP.put(annotation.value(), importProcessor);
}
return bean;
}
}
这样做的好处:
- 1. 自动注册:通过
BeanPostProcessor 在Bean初始化后自动将处理器注册到工厂
2.2 监听器模式
使用EasyExcel的 ReadListener 实现流式读取:
@Slf4j
@Component
@AllArgsConstructor
public class StudentImportListener implements ReadListener<StudentDTO> {
private final List<StudentDTO> successList = new ArrayList<>();
private static final int BATCH_COUNT = 100000;
@Override
public void invoke(StudentDTO studentDTO, AnalysisContext analysisContext) {
// 数据校验
if (StringUtils.isBlank(studentDTO.getName()) || studentDTO.getAge() == null) {
log.error("读取错误的行数据:{}", studentDTO);
return;
}
successList.add(studentDTO);
// 达到批次阈值后异步处理
if (successList.size() >= BATCH_COUNT) {
processBatch();
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
// 处理剩余数据
if (CollectionUtils.isNotEmpty(successList)) {
processBatch();
}
}
}
这样做的好处:
- 1. 内存友好:逐行读取,不会一次性加载整个文件到内存
三、异步批处理实现
3.1 自定义线程池
public class ThreadPoolUtils {
private static final int FULL_PROCESSORS = 10;
/**
* 全局线程池
*/
public static final ExecutorService EXECUTOR_SERVICE = new ThreadPoolExecutor(
FULL_PROCESSORS, // 核心线程数
FULL_PROCESSORS, // 最大线程数
0L, // 空闲线程保活时间
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(FULL_PROCESSORS * 1000), // 阻塞队列
new UtilityElf.DefaultThreadFactory("full-thread-pool-%d", false),
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:由调用线程处理
);
}
线程池配置要点:
- • 核心线程数 = 最大线程数:避免线程创建销毁的开销
- • CallerRunsPolicy:拒绝策略保证任务不丢失
3.2 异步批处理代码
@Override
public void invoke(StudentDTO studentDTO, AnalysisContext analysisContext) {
// ... 数据收集 ...
if (successList.size() >= BATCH_COUNT) {
log.info("读取数据量:{}条", successList.size());
List<CompletableFuture<Boolean>> futures = new ArrayList<>();
// 分片处理:每1000条一个批次
List<List<StudentDTO>> lists = ListUtils.partition(successList, 1000);
for (List<StudentDTO> list : lists) {
// 异步处理数据
CompletableFuture<Boolean> future = CompletableFuture.supplyAsync(() -> {
List<Student> students = list.stream().map(item -> {
Student student = new Student();
student.setName(item.getName());
student.setAge(item.getAge());
return student;
}).collect(Collectors.toList());
return studentService.saveBatch(students);
}, ThreadPoolUtils.EXECUTOR_SERVICE);
futures.add(future);
}
// 等待所有异步任务完成
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
// 清空集合,释放资源
successList.clear();
lists.clear();
}
}
这样做的好处:
- 1. 高吞吐量:多线程并行处理,充分利用CPU资源
- 2. 分片处理:避免单次处理数据量过大导致事务超时
- 4. 优雅等待:
allOf().join() 确保所有任务完成后再继续
四、导出功能实现
4.1 导出处理器
@Component
@AllArgsConstructor
@ExportExcel(ExportBusiness.STUDENT_INFO)
public class StudentExportProcessor implements ExportProcessor {
private final StudentMapper studentMapper;
@Override
public List<?> process(ExportManagerDTO<?> exportManagerDTO) {
StudentQO studentQO = JSON.parseObject(JSON.toJSONString(exportManagerDTO.getQuery()), StudentQO.class);
// 分页查询
Page<Student> page = new Page<>(studentQO.getPageNum(), studentQO.getPageSize());
Page<Student> studentPage = studentMapper.selectPage(page, new LambdaQueryWrapper<Student>()
.like(StringUtils.isNotBlank(studentQO.getName()), Student::getName, studentQO.getName()));
// 数据转换
AtomicInteger index = new AtomicInteger(1);
return studentPage.getRecords().stream().map(item -> {
StudentVO studentVO = new StudentVO();
BeanUtils.copyProperties(item, studentVO);
studentVO.setReqNum(index.getAndIncrement());
return studentVO;
}).toList();
}
}
设计亮点:
- 1. 泛型设计:
ExportManagerDTO<?> 支持任意查询条件
4.2 导出服务
@Override
public void exportExcel(ExportManagerDTO<?> exportManagerDTO) {
ExportBusiness exportBusiness = exportManagerDTO.getExportBusiness();
// 创建导出记录
ExportManager exportManager = new ExportManager();
String id = UUID.randomUUID().toString().replace("-", "");
exportManager.setId(id);
exportManager.setBusinessCode(exportBusiness.getCode());
exportManager.setBusinessName(exportBusiness.getName());
exportManager.setFileName(exportBusiness.getName());
exportManager.setExcelType(ExcelTypeEnum.XLSX);
exportManager.setExportStatus(ExportStatus.ING.getCode());
exportManagerMapper.insert(exportManager);
ExportManager exportManagerUpdate = new ExportManager();
exportManagerUpdate.setId(id);
try {
// 获取对应的处理器
ExportProcessor exportProcessor = exportProcessorFactory.getExportProcessor(exportBusiness);
// 处理数据
List<?> list = exportProcessor.process(exportManagerDTO);
// 生成文件
String fileName = this.getClass().getResource("/").getPath() + exportBusiness.getName() + "_" + System.currentTimeMillis() + ".xlsx";
EasyExcelFactory.write(fileName, exportBusiness.getClazz())
.excelType(exportManagerDTO.getExcelType())
.sheet(exportBusiness.getName())
.doWrite(list);
exportManagerUpdate.setFilePath(fileName);
exportManagerUpdate.setExportStatus(ExportStatus.SUCCESS.getCode());
} catch (Exception e) {
log.error("导出异常", e);
exportManagerUpdate.setExportStatus(ExportStatus.FAILURE.getCode());
}
exportManagerMapper.updateById(exportManagerUpdate);
}
这样做的好处:
五、Feign服务调用
在测试服务中通过Feign调用导入导出服务:
@Service
@AllArgsConstructor
public class StudentServiceImpl implements StudentService {
private final ExportClient exportClient;
private final ImportClient importClient;
@Override
public void exportStudentExcel(StudentQO studentQO) {
ExportManagerDTO<StudentQO> exportManagerDTO = new ExportManagerDTO<>();
exportManagerDTO.setExportBusiness(ExportBusiness.STUDENT_INFO);
exportManagerDTO.setQuery(studentQO);
exportClient.exportExcel(exportManagerDTO);
}
@Override
public String importStudentExcel(MultipartFile file) {
return importClient.importExcel(ImportBusiness.STUDENT_INFO.getCode(), file);
}
}
微服务调用的优势:
- 2. 负载均衡:通过Consul实现服务发现和负载均衡
六、业务枚举设计
@Getter
@AllArgsConstructor
public enum ImportBusiness {
STUDENT_INFO("01", "学生信息", StudentDTO.class),
TEACHER_INFO("02", "教师信息", TeacherDTO.class),
;
private final String code;
private final String name;
private final Class<?> clazz;
public static ImportBusiness getByCode(String code) {
for (ImportBusiness importBusiness : ImportBusiness.values()) {
if (importBusiness.code.equals(code)) {
return importBusiness;
}
}
throw new BusinessException("ImportBusiness code error");
}
}
枚举设计的好处:
七、技术选型
八、总结
本文介绍的Excel导入导出方案具有以下特点:
- • 高内聚低耦合:通过策略模式和工厂模式实现业务解耦
- • 易扩展:新增业务类型只需添加对应的Processor实现
- • 微服务架构:独立部署、独立扩展,适应高并发场景
这种优雅的设计方案不仅适用于Excel导入导出场景,对于类似的文件处理需求也有很好的参考价值。
源码地址:https://github.com/LuckyKuang/leaning-demo
关注公众号「SCper技术」,获取更多技术干货!