easyExcel的个人应用2.0
当时简单的写了下demo,留下了很多坑。但是我填坑也不太想填,怎么办呢。那就新开个坑。
准备工作
pom
<!-- easyexcel -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.1.4</version>
</dependency>
思路
既然我是想弄个模板,然后其他的配套服务都准备进来,那我们就简单的拆服务呗。
我们先拆文件服务,我的项目的文件,是利用s3来上传下载文件,那么我们就拆出文件服务接口。
public interface FileService {
File downloadS3File(String fileId);
String uploadS3File(File file);
}
然后我们拆任务服务,我的项目的话,是利用一个task来表示一个导入导出的进度。那么我们就拆任务服务实体和任务接口。
@Data
public class ExcelTask {
@Id
private String id;
/**
* 任务类型,1为导入,2为导出
*/
@Column(name = "task_type")
private Integer taskType;
/**
* 任务名
*/
@Column(name = "task_name")
private String taskName;
/**
* 总任务
*/
private Integer total;
/**
* 当前进度
*/
private Integer current;
/**
* 已完成
*/
private Integer completed;
/**
* 进度
*/
private Integer progcess;
/**
* 成功1,失败2,未开始-1,进行中0
*/
private Integer result;
/**
* 失败原因
*/
private String reason;
private String fileId;
}
任务接口
public interface TaskService {
String createTask(ExcelTask work);
void updateTotal(String taskId,Integer total);
void updateFileId(String taskId,String fileId);
void updateProgress(String taskId,Integer progress);
void successTask(String taskId);
void failedTask(String taskId,String reason);
}
那么就开撸导入
导入
通用的监听器,之前也分析过了,数据的入库和处理,都是一个处理函数而已。
public class NormalListener<T> extends AnalysisEventListener<T> {
private static final Logger LOGGER = LoggerFactory.getLogger(NormalListener.class);
/**
* 每隔100条存储数据库,实际使用中可以3000条,然后清理list ,方便内存回收
*/
private static final int BATCH_COUNT = 1000;
private int current=0;
private int total=0;
private TaskService taskService;
private ExcelTask task;
List<T> list = new ArrayList<T>();
/**
* 假设这个是一个DAO,当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。
*/
private Consumer<List<T>> mapper;
public Integer getTotal(){
return this.total;
}
public Integer getCurrent(){
return this.current;
}
public NormalListener(Consumer<List<T>> mapper, TaskService taskService, ExcelTask task) {
// 实际使用如果到了spring,请使用下面的有参构造函数
this.mapper=mapper;
this.taskService=taskService;
this.task=task;
}
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
ReadSheetHolder readSheetHolder = context.readSheetHolder();
Integer approximateTotalRowNumber = readSheetHolder.getApproximateTotalRowNumber();
Integer headRowNumber = readSheetHolder.getHeadRowNumber();
this.total=approximateTotalRowNumber-headRowNumber;
this.task.setTotal(this.total);
taskService.createTask(task);
}
/**
* 这个每一条数据解析都会来调用
*
* @param data
* @param context
*/
@Override
public void invoke(T data, AnalysisContext context) {
LOGGER.info("解析到一条数据:{}", data);
list.add(data);
this.current=this.current+1;
// 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
if (list.size() >= BATCH_COUNT) {
saveData();
// 存储完成清理 list
list.clear();
}
}
/**
* 所有数据解析完成了 都会来调用
*
* @param context
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 这里也要保存数据,确保最后遗留的数据也存储到数据库
saveData();
LOGGER.info("所有数据解析完成!");
taskService.successTask(task.getId());
}
/**
* 加上存储数据库
*/
private void saveData() {
LOGGER.info("{}条数据,开始存储数据库!", list.size());
mapper.accept(list);
taskService.updateProgress(task.getId(),current);
LOGGER.info("存储数据库成功!");
}
}
那么就开始撸导入的模板
public class NormalImportTemplate<T> {
public String fileId;
public Class clazz;
private Consumer<List<T>> mapper;
private Integer sheetNo;
private static final Logger LOGGER = LoggerFactory.getLogger(NormalImportTemplate.class);
private TaskService taskService;
private ExcelTask task;
private FileService fileService;
public NormalImportTemplate(String fileId, Class<T> clazz, Consumer<List<T>> mapper, Integer sheetNo, TaskService taskService, ExcelTask task, FileService fileService) {
this.fileId = fileId;
this.clazz = clazz;
this.mapper = mapper;
this.sheetNo = sheetNo;
this.taskService = taskService;
this.task = task;
this.fileService = fileService;
}
public NormalImportTemplate(String fileId, Class<T> clazz, Consumer<List<T>> mapper, TaskService taskService, ExcelTask task, FileService fileService) {
this.fileId = fileId;
this.clazz = clazz;
this.mapper = mapper;
this.sheetNo = 0;
this.taskService = taskService;
this.task = task;
this.fileService = fileService;
}
public String simpleRead(){
// 有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
// 写法1:
// // 这里 需要指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭
// EasyExcel.read(fileName, User.class, new UserListener(mapper)).sheet().doRead();
// 写法2:
String taskId = taskService.createTask(this.task);
File file = fileService.downloadS3File(this.fileId);
new Thread(()->{
ExcelReader excelReader = null;
NormalListener<T> listener =new NormalListener<T>(list->{
mapper.accept(list);
},taskService, this.task);
try {
excelReader = EasyExcel.read(file.getPath(), clazz, listener).build();
ReadSheet readSheet = EasyExcel.readSheet(this.sheetNo).build();
excelReader.read(readSheet);
taskService.successTask(taskId);
} catch (Exception e){
LOGGER.error("导入数据失败{}",e);
taskService.failedTask(this.task.getId(),e.getMessage());
} finally {
if (excelReader != null) {
// 这里千万别忘记关闭,读的时候会创建临时文件,到时磁盘会崩的
excelReader.finish();
}
if(file!=null && file.exists()){
file.delete();
}
}
}).start();
return taskId;
}
}
来看看怎么使用吧
@Override
public String importData(String fileId) {
ExcelTask excelTask = new ExcelTask();
excelTask.setTaskName("专属库导入");
excelTask.setTaskType(1);
excelTask.setId(IdGen.uuid());
NormalImportTemplate template = new NormalImportTemplate<ItoStockNewOwnerWhidVO>(fileId,ItoStockNewOwnerWhidVO.class,
list->{
try {
batchInsert(list);
} catch (BusinessException e) {
e.printStackTrace();
}
},
taskService,excelTask,fileService);
return template.simpleRead();
}
是不是少了什么,taskService的实现和fileService的实现。
这个的话,这个不同项目有不同的实现,我的建议的话,还是各写各的,不同项目不同实现,然后,实现类给spring管理,然后注入到实现的地方。
@Service
@Slf4j
public class FileServiceImpl implements FileService {
@Autowired
AttachmentClient attachmentClient;
@Autowired
private FileConfig fileConfig;
@Override
public File downloadS3File(String fileId) {
SkfItoAttachmentEntity attachment = attachmentClient.getAttachmentById(fileId);
// 项目路径
String projectPath = FileAddrConstants.FILE_DIR;
// 文件名
String fileName = attachment.getFileName();
return FileDownloadUtil.getNetUrl(attachment.getUrl(), projectPath, fileName);
}
@Override
public String uploadS3File(File file) {
if(!file.exists()) return null;
String fileSuffix = FileUtil.getMultipartFileSuffix(file.getName());
String fileKey = IdWorker.get32UUID() + fileSuffix;
String url = UploadUtil.uploadToS3(file, fileConfig.getKeys() + fileKey, fileConfig.getBucketName());
SkfItoAttachmentEntity entity = new SkfItoAttachmentEntity();
entity.setFileKey(fileKey);
entity.setFileName(file.getName());
entity.setUrl(url);
Result save = attachmentClient.save(entity);
Object data = save.getData();
if (data != null) {
log.info("---------------------文件上传aws的id为" + data.toString() + "---------------------------------");
return data.toString();
}
return null;
}
}
@Slf4j
@Service
public class TaskServiceImpl implements TaskService {
@Autowired
SystemClient systemClient;
@Override
public String createTask(ExcelTask work) {
ItoBusinessWorkEntity entity = new ItoBusinessWorkEntity();
entity.setId(work.getId());
entity.setWorkType(work.getTaskType());
entity.setWorkName(work.getTaskName());
String taskId = systemClient.startBusinessWork(entity);
return taskId;
}
@Override
public void updateTotal(String taskId, Integer total) {
return;
}
@Override
public void updateFileId(String taskId, String fileId) {
systemClient.completeTask(taskId,fileId);
}
@Override
public void updateProgress(String taskId, Integer progress) {
systemClient.updateProgress(taskId,progress);
}
@Override
public void successTask(String taskId) {
systemClient.updateProgress(taskId,100);
}
@Override
public void failedTask(String taskId, String reason) {
systemClient.taskFailed(taskId,reason);
}
}
导出
导出的话,简单的就是给与条件,筛选出数据,然后导出,上传s3,返回fileId给task。
那么我们就抽象出一个conditon类,当做标识。
public interface Condition {
}
没意义。跟序列化接口一样,标识而已。
然后导出数据,不能在用Consumer来做了,因为,可能要分页导出,也可能不分页。
那我们就抽象出一个数据导出接口。
public interface DataExportService<T> {
List<T> selectByCondition(Condition condition);
default List<T> selectByConditionAndPage(Condition condition, MyPager pager){
throw new RuntimeException("no such method");
}
}
然后就可以开撸,导出模板了。
public class EasyExportTemplate<T> {
public Class clazz;
private static final Logger LOGGER = LoggerFactory.getLogger(NormalImportTemplate.class);
private TaskService taskService;
private ExcelTask task;
private FileService fileService;
private String fileName;
private String sheetName="0";
private DataExportService<T> dataExportService;
private Condition condition;
public EasyExportTemplate(Class<T> clazz, ExcelTask task,String fileName, Condition condition,TaskService taskService,
FileService fileService, DataExportService<T> dataExportService) {
this.clazz = clazz;
this.taskService = taskService;
this.task = task;
this.fileService = fileService;
this.fileName = fileName;
this.dataExportService = dataExportService;
this.condition = condition;
}
public String simpleWrite() {
String taskId = taskService.createTask(this.task);
new Thread(()-> {
String projectPath = FileAddrConstants.FILE_DIR;
// 写法2a
String fileName = projectPath + File.separator + this.fileName + IdGen.uuid() + ".xlsx";
// 这里 需要指定写用哪个class去写
// ExcelWriter excelWriter = null;
File file = null;
try {
// excelWriter = EasyExcel.write(fileName, clazz).build();
// WriteSheet writeSheet = EasyExcel.writerSheet(sheetName).build();
// excelWriter.write(dataExportService.selectByCondition(condition), writeSheet);
EasyExcel.write(fileName, clazz).sheet(sheetName).doWrite(dataExportService.selectByCondition(condition));
file = new File(fileName);
String fileId = fileService.uploadS3File(file);
taskService.updateFileId(taskId,fileId);
} catch (Exception e){
LOGGER.error("导入数据失败{}",e);
taskService.failedTask(taskId,e.getMessage());
} finally {
// // 千万别忘记finish 会帮忙关闭流
// if (excelWriter != null) {
// excelWriter.finish();
// }
if(file!=null && file.exists()){
file.delete();
}
}
}).start();
return taskId;
}
}
使用
@Override
public String export(ItoOwnerWhidCondition condition) {
ExcelTask excelTask = new ExcelTask();
excelTask.setTaskName("专属库导出");
excelTask.setTaskType(2);
excelTask.setId(IdGen.uuid());
EasyExportTemplate template = new EasyExportTemplate<ItoStockNewOwnerWhidVO>(ItoStockNewOwnerWhidVO.class,
excelTask,"专属库导出",condition,taskService,fileService,this
);
return template.simpleWrite();
}
@Override
public List selectByCondition(Condition condition) {
ItoOwnerWhidCondition itoOwnerWhidCondition= (ItoOwnerWhidCondition) condition;
List<ItoStockNewOwnerWhid> itoStockNewOwnerWhids = mapper.selectByExample(createExample(itoOwnerWhidCondition));
List<ItoStockNewOwnerWhidVO> collect = itoStockNewOwnerWhids.stream().map(itoStockNewOwnerWhid -> {
return getConvert().apply(itoStockNewOwnerWhid);
}).collect(Collectors.toList());
return collect;
}
简简单单。
思路出来了,做也就很简单了,但是这个也就是通用的模板,一些比较精细的导出,建议还是自己写吧。
然后,这个模板还有很多问题,导入的话,进度都是0-100.然后还有导出的话,大文件导出,应该会oom。
可以做个限制,数据接口里加个限制条件,如果超过多少条不让导出。
也可以利用easyexcel的分批次导出。
还有的话,就是如果导入的数据有问题,导出出问题,怎么办,
其实的话,建议显示到task里,也可以导入的时候返回一个fileid。里面是excel。然后加两列,导入是否成功,原因。
方法很多,看需求吧。
然后这个导入的话,没有撤销功能。我的话,
建议导入隔离到一个草稿状态的数据库表(就是跟原表一摸一样的表,加个taskId的表)里,显示给用户看,那些是可以导入的。然后再导入到真正的数据库里。
简单的来说就是,导入的数据加个任务号。客户还有一步操作,把这次任务的数据导入。
还是看需求。
哎,就是给多少钱干多少活。
精细化的话,都给你搞上。
懒癌犯了,下了