easyExcel的个人应用2.0

14 篇文章 0 订阅
1 篇文章 0 订阅

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的表)里,显示给用户看,那些是可以导入的。然后再导入到真正的数据库里。

简单的来说就是,导入的数据加个任务号。客户还有一步操作,把这次任务的数据导入。

还是看需求。

哎,就是给多少钱干多少活。

精细化的话,都给你搞上。

懒癌犯了,下了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值