Skip to content

Halfmoonly/easyExcel-practice

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

easyExcel-practice

可配置化

需求:我们希望在Controller层只做1组导入导出接口,适配无数的Web页面,这样的好处是避免Controller层的导入导出接口泛滥

思路:对于每个Web页面的导入导出逻辑,根据请求传参type的不同,后台都有相应的解析器Resolver来处理

接下来有两种设计思路:

设计方案1:基于Spring装配Resolver的全量至IOC容器

实现出所有页面导入导出对应的Resolver,并打上@Component注解,初始化项目阶段装载所有的的Resolver实例至IOC容器

为了做到配置化,我们需要提前准备好所有的[请求参数type,对应Resolver实例名称]的枚举信息Enum

在导入导出的逻辑中,根据请求参数type,通过Spring-API来获取对应BeanName的Resolver来处理任务

这种方式的优点是任务执行之前Resolver就已经实例化好了,缺点是要在项目启动阶段就就创建全部的Resolver实例会耗更多的内存

设计方案2:基于手动反射按需实例化Resolver

依然是为了做到配置化,我们需要提前准备好所有的[type,对应解析器Resolver.class]的枚举信息,

TEST_EXCEL("testExcel", TestExcelEntity.class, TestExcelResolver.class, "template\\testExcel.xlsx"),
NONE("not_exist_type", null, null, null);

与方案1不同的地方是,只编写好页面导入导出对应的解析器Resolver,但不是将所有的Resolver一股脑全实例化到Spring容器中,而是在请求到来之后根据请求参数type,获取指定的Resolver.class对象,然后和SpringIOC一样的实现原理,手动通过反射机制创建出该Resolver的实例对象,来处理具体的导入导出任务

这种方式的优点是每次按需创建Resolver实例比较节省内存,缺点是在请求到来之后再通过反射实例化Resolver效率会比较低

总的来说,任何设计方案乃至架构,到最后都是性能和内存之间的一种取舍(升华了,哈哈哈)

对于本项目来说,只是为了让大家学习设计思想,因此用哪种方案都可以.

最终本项目仅以方案2为例来做示范,感兴趣的朋友也可以借鉴本项目的代码与工程结构,对方案1做出实现

在吃透了任意一种方案之后,另一种思路实现起来应该就不难了

设计模式分析

抽象类常用于骨架设计,提供一组通用的公共方法。

但是抽象类的定义中是存在,未被实现的abstract方法待子类去实现,那么这些abstract方法和Interface中的接口方法在设计上有什么区别呢?

这是接口和抽象类的区别,应该没有异议:

接口:更倾向于表示能力或行为的集合,强调的是“能做什么”。

抽象类:更倾向于表示一个具体的概念,给出初版的接口实现,强调的是“是什么”

但是,依旧没有解释Abstract类中的abstract方法和Interface中的接口方法在设计上有什么区别呢?下面我个人一言以蔽之:

  • Interface中的接口方法用于对外部提供访问的途径
  • Abstract类中的abstract方法用于对内提供实现的途径

具体而言,看如下的设计思想:

对外提供ExcelResolver接口:ExcelResolver.javaExcelResolver

public interface ExcelResolver<E> {
    /**
     * 导入数据
     * @param request
     * @param response
     * @param file
     * @param clazz
     * @param excelImportUtil
     * @return
     */
    Result<?> importData(HttpServletRequest request, HttpServletResponse response, MultipartFile file,
                         Class<E> clazz, ExcelImportUtil<E, E> excelImportUtil);
    /**
     * excel导出数据
     * @param param
     * @param clazz
     * @param response
     * @param excelImportUtil
     */
    Result<String> exportData(HttpServletResponse response, HashMap<String, Object> param, Class<E> clazz,
                              ExcelExportUtil<E, E> excelImportUtil);
}

抽象类AbstractExcelResolver实现上述接口:AbstractExcelResolver.java

@Slf4j
public abstract class AbstractExcelResolver<E extends IExcelRowInfoAware> implements ExcelResolver<E> {
    /**
     * 获取未分页的导出数据列表
     *
     * @param param
     * @return
     */
    protected abstract List<E> retriveDataForExport(Map<String, Object> param);

    /**
     * 筛选出可以正常导入的数据记录
     * @implNote 该方法需校验数据,但校验失败不会影响其他记录的导入
     *
     * @param excelList excel原始导入数据
     * @param toSaveList 待保存的数据列表,初始为空,需将excelList中校验成功的数据保存到该列表中
     * @return 数据异常明细
     */
    protected abstract List<ExcelErrorResultVo> convertDataForImport(List<E> excelList, List<E> toSaveList);

    /**
     * 保存导入数据
     * @param toSaveList
     */
    protected abstract void saveAllDataForImport(List<E> toSaveList);

    protected ExcelImportUtil<E, E> excelImportUtil;

    protected ExcelExportUtil<E, E> excelExportUtil;

    @Override
    public Result<String> exportData(HttpServletResponse response, HashMap<String, Object> param, Class<E> clazz,
                                     ExcelExportUtil<E, E> excelExportUtil) {
        this.excelExportUtil = excelExportUtil;
        String fileName = "PullReplenishConfigExportFile";
        List<E> list = retriveDataForExport(param);
        Map<Class, List<? extends Object>> dataMap = new HashMap<>();
        dataMap.put(clazz, list);
        try {
            excelExportUtil.exportMultipleSheetData(response, fileName, dataMap, clazz);
        } catch (Exception ex) {
            log.error("处理excel文件失败");
        }
        return Result.fail("处理excel文件失败");
    }

    @Override
    public Result<?> importData(HttpServletRequest request, HttpServletResponse response, MultipartFile file,
                                Class<E> clazz, ExcelImportUtil<E, E> excelImportUtil) {
        this.excelImportUtil = excelImportUtil;
        List<E> excelList;
        try {
            excelList = excelImportUtil.importData(clazz, file);
        } catch (Exception e) {
            log.error("import data error", e);
            return Result.fail(LyflexiErrorType.IMPORT_TEMPLATE_ERROR.getMesg());
        }
        // 导入数据合法性校验
        List<ExcelErrorResultVo> errorResultVoList = checkDuplicate(excelList);
        if (CollectionUtils.isNotEmpty(errorResultVoList)) {
            return Result.failWithData(errorResultVoList);
        }
        // 业务方向数据合法性校验
        List<E> toSaveList = new ArrayList<>(excelList.size());
        List<ExcelErrorResultVo> errorResultVoList2 = this.convertDataForImport(excelList, toSaveList);
        // 存在数据通过校验,可以导入
        if (!toSaveList.isEmpty()) {
            this.saveAllDataForImport(toSaveList);
        }
        if (CollectionUtils.isNotEmpty(errorResultVoList2)) {
            return Result.failWithData(errorResultVoList2);
        }
        return Result.success("importSuccess");
    }

    /**
     * 校验数据合法性:是否重复
     * @implNote 该方法会校验数据,校验失败会导致整个导入请求失败
     *
     * 同时校验字段非空
     * @param excelList
     * @return 数据异常明细
     */
    private List<ExcelErrorResultVo> checkDuplicate(List<E> excelList) {
        List<ExcelErrorResultVo> errorResultVoList = new ArrayList<>();
        if (excelList.isEmpty()) {
            return errorResultVoList;
        }
        ExcelErrorResultVo errorVo;

        // 导入中重复的编码
        Map<String, Long> map = excelList.stream()
                .collect(Collectors.groupingBy(o -> o.getUniqueKey(), Collectors.counting()));
        List<String> repeatCode = map.entrySet().stream()
                .filter(entry -> entry.getValue() > 1).map(Map.Entry::getKey).collect(Collectors.toList());
        int index = 1;
        for (E entity : excelList) {
            index++;
            String errorInfo = excelImportUtil.validateExcel(entity);
            if (StringUtils.isNotBlank(errorInfo)) {
                errorVo = new ExcelErrorResultVo(index, entity.getUniqueKey(), entity.getRecordName(),
                        "新建", errorInfo);
                errorResultVoList.add(errorVo);
            } else {
                boolean isRepeat = CollectionUtils.isNotEmpty(repeatCode) && repeatCode.contains(entity.getUniqueKey());
                if (isRepeat) {
                    errorVo = new ExcelErrorResultVo(index, entity.getUniqueKey(), entity.getRecordName(),
                            "新建", "重复的导入数据");
                    errorResultVoList.add(errorVo);
                }
            }
        }
        return errorResultVoList;
    }
}

在抽象类AbstractExcelResolver实现ExcelResolver接口的时候,抽取或者叫新增了三个方法:

  • abstract retriveDataForExport
  • abstract convertDataForImport
  • abstract saveAllDataForImport

因此这三个方法就是对内的抽象方法,这就是抽象方法与ExcelResolver接口方法的设计区别

更新日志

2024/09/15更新:

2024/09/15更新:补充了设计方案1:见模块custom-framework-spring

实现出所有页面导入导出对应的Resolver,并打上@Component注解,初始化项目阶段装载所有的的Resolver实例至IOC容器

为了做到配置化,我们需要提前准备好所有的[请求参数type,对应Resolver实例名称]的枚举信息Enum

TEST_EXCEL("testExcel", TestExcelEntity.class, "testExcelResolver", "template\\testExcel.xlsx"),
NONE("not_exist_type", null, null, null);

在导入导出的逻辑中,根据请求参数type,通过Spring-API来获取对应BeanName的Resolver来处理任务

try {
    exportResolver = SpringUtils.getBean(exportEnum.GetResolverName(), ExcelResolver.class);
} catch (Exception e) {
    throw new RuntimeException(e);
}

这种方式的优点是任务执行之前Resolver就已经实例化好了,缺点是要在项目启动阶段就就创建全部的Resolver实例会耗更多的内存

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages