Alibaba EasyExcel 导入导出全家桶

一、阿里巴巴EasyExcel的优势

        首先说下EasyExcel相对 Apache poi的优势:

        EasyExcel也是阿里研发在poi基础上做了封装,改进产物。它替开发者做了注解列表解析,表格填充等一系列代码编写工作,并将此抽象成通用和可扩展的框架。相对poi,在数据量比较大的时候,它有着更优越的性能体现。导出的时候,easyexcel使用优化的反射技术,避免poi频繁的去创建cell和row对象;导入的时候,它的解析器AnalysisEventListener,可设置批量阈值 BATCH_COUNT,达到阈值就往数据库插入数据,然后清空解析器内部缓存,相同的表格,easyexcel导入所占用的内存要比poi节省90%,避免了大数据量导入的时候,造成的内存占用井喷(这使得stop the world的时间可能会被集中,而系统可能会出现短暂的停摆。),而GC不能均衡调动垃圾回收。同时也避免堆积数据后,sql的巨量数据的批量插入,导致超出mybatis批量插入语句能承受的最大长度限制。

二、EasyExcel核心util类

@Slf4j
public class EasyExcels {

    public static final String EXT_NAME_XLSX = "xlsx";
    public static final String EXT_NAME_XLS = "xls";


    /**
     *
     * @param response
     * @param data
     * @param filename
     * @param sheetName
     * @param selectMap  自定义下拉列,但是既然数据都导出了,下拉用处何在?这个需求比较少
     * @param <T>
     * @throws IOException
     */
    public static <T> void write(HttpServletResponse response, List<T> data, String filename, String sheetName,
                                 List<KeyValue<ExcelColumn, List<String>>> selectMap) throws IOException {
        setResponse(response, filename);
        if (StringUtils.isBlank(sheetName)) {
            sheetName = filename;
        }
        // 输出 Excel
        try {
            EasyExcel.write(response.getOutputStream(), data != null && !data.isEmpty() ? data.get(0).getClass() : null)
                    .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度
                    .registerWriteHandler(new CustomCellWriteWeightConfig()) // Excel 列宽自适应
                    .registerWriteHandler(EasyExcelStyle.horizontalCellStyleStrategy) //内容样式
                    .registerWriteHandler(new SelectWriteHandler(selectMap)) // 基于固定 sheet 实现下拉框
                    .sheet(sheetName).doWrite(data);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            response.getOutputStream().close();
        }
    }

    // 简单导入读取,不做解析,不做校验
    public static <T> List<T> read(MultipartFile file, Class<T> head) throws IOException {
        return EasyExcel.read(file.getInputStream(), head, null)
                .autoCloseStream(false)  // 不要自动关闭,交给 Servlet 自己处理
                .doReadAllSync();
    }

    // 需要配合监听器解析数据
    public static <T> void read(MultipartFile file, Class<T> head, ReadListener<T> listener) throws IOException {
        EasyExcel.read(file.getInputStream(), head, listener)
                .sheet()
                .doRead();

    }

    // 不带下拉列的导出,用的比较多
    public static <T> void export(HttpServletResponse response, List<T> data, String filename, String sheetName) throws IOException {
        setResponse(response, filename);
        if (StringUtils.isBlank(sheetName)) {
            sheetName = filename;
        }
        EasyExcel.write(response.getOutputStream(), data != null && !data.isEmpty() ? data.get(0).getClass() : null)
                .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
                .registerWriteHandler(new CustomCellWriteWeightConfig())
                .registerWriteHandler(EasyExcelStyle.horizontalCellStyleStrategy)
                .sheet(sheetName).doWrite(data);
    }


   // 用于合并单元格列的导出
    public static <T> void export(HttpServletResponse response, List<T> data, String filename, String sheetName, RowWriteHandler handler) throws IOException {
        setResponse(response, filename);
        EasyExcel.write(response.getOutputStream(), data != null && !data.isEmpty() ? data.get(0).getClass() : null)
                .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
                .registerWriteHandler(new CustomCellWriteWeightConfig())
                .registerWriteHandler(EasyExcelStyle.horizontalCellStyleStrategy)
                .registerWriteHandler(handler)
                .sheet(sheetName).doWrite(data);
    }


   // 用于导出表头模板,填充导入数据用的excel模板,因为是模板,所以肯定会有下拉列的需求
    public static <T> void export(HttpServletResponse response, Class<T> clazz, String filename) throws IOException {
        setResponse(response, filename);
        Map<Integer, ExcelSelectedResolve> selectedMap = resolveSelectedAnnotation(clazz);
        EasyExcel.write(response.getOutputStream(), clazz)
                .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
//                .registerWriteHandler(new CustomCellWriteHeightConfig())
                .registerWriteHandler(new CustomCellWriteWeightConfig())
                .registerWriteHandler(EasyExcelStyle.horizontalCellStyleStrategy)
                .registerWriteHandler(new SelectSheetWriteHandler(selectedMap))
                .sheet(filename).doWrite(Collections.emptyList());
    }

    /**
     * 解析表头类中的下拉注解
     * @param head 表头类
     * @param <T> 泛型
     * @return Map<下拉框列索引, 下拉框内容> map
     */
    private static <T> Map<Integer, ExcelSelectedResolve> resolveSelectedAnnotation(Class<T> head) {
        Map<Integer, ExcelSelectedResolve> selectedMap = new HashMap<>();

        // getDeclaredFields(): 返回全部声明的属性;getFields(): 返回public类型的属性
        Field[] fields = head.getDeclaredFields();
        for (int i = 0; i < fields.length; i++){
            Field field = fields[i];
            // 解析注解信息
            ExcelSelected selected = field.getAnnotation(ExcelSelected.class);
            ExcelProperty property = field.getAnnotation(ExcelProperty.class);
            if (selected != null) {
                ExcelSelectedResolve excelSelectedResolve = new ExcelSelectedResolve();
                String[] source = excelSelectedResolve.resolveSelectedSource(selected);
                if (source != null && source.length > 0){
                    excelSelectedResolve.setSource(source);
                    excelSelectedResolve.setFirstRow(selected.firstRow());
                    excelSelectedResolve.setLastRow(selected.lastRow());
                    if (property != null && property.index() >= 0){
                        selectedMap.put(property.index(), excelSelectedResolve);
                    } else {
                        selectedMap.put(i, excelSelectedResolve);
                    }
                }
            }
        }

        return selectedMap;
    }

    public static void setResponse(HttpServletResponse response, String filename) throws IOException {
        setResponse(response, filename, EXT_NAME_XLSX);
    }

    public static void setResponse(HttpServletResponse response, String filename, String extName) throws IOException {
        String exportFilename = URLEncoder.encode(filename, StandardCharsets.UTF_8.name());
        response.setContentType("application/vnd.ms-excel");
        response.setCharacterEncoding("utf-8");
        response.setHeader("Access-Control-Expose-Headers", "token,Content-Type,Content-disposition");
        response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Connection, User-Agent, Cookie, token,Content-Type,Content-disposition");
        response.setHeader("Content-disposition", exportFilename + "." + extName);
    }

}

三、导入解析监听器

那要使用easyexcel,首先要解决解析器抽象类的实现:

        当时第一次使用easyexcel的时候,对这个工具框架不熟悉,项目时间被催的紧,没时间去做设计,当时修改每个类的字段注解index属性,每个字段单独写校验语句,简直苦不堪言。我只想说,磨刀不误砍柴工,不注重设计的公司,只会被拖延更多的时间。

/**
 * @Title: ExcelImportReadListener
 * @Description: 其他人如果觉得invoke()方法不满足其需求,可以自己实现一下
 * @Author: wenrong
 * @Date: 2024/4/25 17:08
 * @Version:1.0
 */
@Data
public abstract class ExcelImportReadListener<T extends ValidateBaseBo> extends AnalysisEventListener<T> {

    private static final Logger log = LoggerFactory.getLogger("excelReadListener");

    public static int BATCH_COUNT = 1000;

    private AtomicLong successNum = new AtomicLong();

    private final Class<T> clazz;

    private Validator<T> validator;

    private List<T> successData = new ArrayList<>();

    private List<T> failureData = new ArrayList<>();


    public ExcelImportReadListener(Class<T> clazz) {
        this.clazz = clazz;
    }

    @Override
    public void invoke(T data, AnalysisContext context) {
        log.info("解析到一条数据:{}", JSONObject.toJSON(data));
        StringBuilder errMsg = new StringBuilder();
        try {
            //根据excel数据实体中的javax.validation + 正则表达式来校验excel数据
            errMsg.append(EasyExcelValidateHandler.validateEntity(data));
            // 额外自定义校验,以及设置数据属性的逻辑
            if (validator != null) {
                errMsg.append(validator.validate(data));
            }
        } catch (NoSuchFieldException e) {
            log.error(e.getMessage());
        }
        if (StringUtils.isNotEmpty(errMsg.toString())) {
            data.setErrMsg(errMsg.toString());
            failureData.add(data);
        } else {
            successData.add(data);
            successNum.incrementAndGet();
        }

        if (BATCH_COUNT != 0 && successData.size() >= BATCH_COUNT) {
            try {
                saveData();
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            }
            successData.clear();
        }

    }

    @Override
    public final void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
        // 验证表头
        if (headMap.isEmpty()) {
            throw new ServiceException("无表头");
        }
        if (clazz != null) {
            try {
                Map<Integer, String> indexNameMap = getIndexNameMap(clazz);
                for (Integer index : indexNameMap.keySet()) {
                    log.info("表头字段:{}", headMap.get(index));
                    if (StringUtils.isEmpty(headMap.get(index))) {
                        throw new ServiceException("未设置index");
                    }
                    // 对比excel表头和解析数据的java实体类的,看是否匹配
                    if (!headMap.get(index).equals(indexNameMap.get(index))) {
                        throw new ServiceException("导入模板错误");
                    }
                }
            } catch (NoSuchFieldException e) {
                log.error(e.getMessage(), e);
            }
        }

    }

    @Override
    public final void doAfterAllAnalysed(AnalysisContext context) {
        log.info("所有数据解析完成!共校验成功{}条数据,校验失败{}条数据", successNum.get(), failureData.size());
        try {
            saveData();
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }

    /**
     * 将该类做成抽象类,在各service中实现saveDate方法,
     * 不侵入业务,同时不会让解析占用内存
     */
    public void saveData() throws Exception {
        log.info("开始往数据库插入数据");
    }

    private Map<Integer, String> getIndexNameMap(Class<T> clazz) throws NoSuchFieldException {
        Map<Integer, String> excelPropertyMap = new HashMap<>();
        Field field;
        Field[] fields = clazz.getDeclaredFields();
        int sequence = 0;
        for (Field item : fields) {
            field = clazz.getDeclaredField(item.getName());
            field.setAccessible(true);
            ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
            if (excelProperty != null) {
                // 避免每个列都要写index,插入或删除一个字段,所有的index都需要修改。默认为java实体类中字段的顺序。
                int index = excelProperty.index() == -1 ? sequence : excelProperty.index();
                String[] values = excelProperty.value();
                StringBuilder value = new StringBuilder();
                for (String v : values) {
                    value.append(v);
                }
                excelPropertyMap.put(index, value.toString());
                sequence++;
            }
        }
        return excelPropertyMap;
    }

}

3.1、解析成功的数据直接落库,错误数据导出

        ValidateBaseBo:用在导入的时候,将校验的错误保留下来,然后再把有问题的数据过滤出来,再导出,或者显示在前端的导入结果里,操作者可以按照错误信息把表格里的数据修改好后,再次导入,而且只将导入失败的数据导出,不用去原表中大片的数据中去找有错误信息的数据,目的是方便操作者快速定位表格里的问题数据。

/**
 * @Title: ValidateBaseBo
 * @Description:
 * @Author: wenrong
 * @Date: 2024/10/17 上午11:02
 * @Version:1.0
 */
@Data
public abstract class ValidateBaseBo {

    @ExcelProperty(value = "错误信息")
    @TableField(exist = false)
    @ApiModelProperty(hidden = true)
    private String errMsg;
}

3.2、解析过程中校验数据正确性 

        除了javax.validation,基础的注解校验之外,如果还需要额外的校验,就自定义校验器作补充。

/**
 * @Title: ValidData
 * @Description: javax.validation 以外校验函数
 * @Author: wenrong
 * @Date: 2024/4/26 19:51
 * @Version:1.0
 */
public interface Validator<T> {

    /**
     * 这里的实现方法,最后返回的如果为null,一定要返回"",否则会被转化为"null"
     *
     * @param T t
     * @return ""
     */
    String validate(T t);
}

3.3、导入/导出 Convertor 

        excel导入数据对应的实体类:要注意表格中的汉字和实际存入到数据库中数值的转换:Convertor

/**
 * @author wenrong
 * @date 2024-11-25 17:38:26
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("····")
public class YourClass extends ValidateBaseBo implements Serializable {

    private static final long serialVersionUID = 1L;
    public static Map<String, DyzProvinceSchool> provinceMap = new HashMap<>();
    public static Map<String, DyzProvinceSchool> schoolMap = new HashMap<>();
    @ExcelIgnore
    @TableId(type = IdType.AUTO)
    private Long id;

    @ApiModelProperty(value = "省份ID")
    @ExcelProperty(value = "省份", converter = ProvinceConvertor.class)
    @NotNull(message = "不能为空")
    private Integer provinceId;

    @ApiModelProperty(value = "学校ID")
    @ExcelProperty(value = "学校", converter = SchoolConvertor.class)
    @NotNull(message = "不能为空")
    private Integer schoolId;

    @ApiModelProperty(value = "节目代码")
    @ExcelProperty(value = "节目代码")
    @NotNull(message = "不能为空")
    private String worksNumber;

    @ApiModelProperty(value = "节目/项目名称")
    @ExcelProperty(value = "节目/项目名称")
    @NotNull(message = "不能为空")
    private String ``````;



    public String validate(Map<Integer, DyzProvinceSchool> provinceMap, Map<Integer, DyzProvinceSchool> schoolMap) {
        StringBuilder sb = new StringBuilder(this.getErrMsg() == null ? "" : this.getErrMsg());
        if (provinceMap.get(this.provinceId) == null) {
            sb.append("省份不存在: ").append(provinceId);
        }

        if (schoolMap.get(this.schoolId) == null) {
            sb.append("学校不存在: ").append(schoolId);
        }

        return sb.toString();
    }

    public static class GroupTypeConvertor implements Converter<Integer> {

        //导入的时候,将表格的汉字转换成java对应数据库的字段
        @Override
        public Integer convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty,
                                         GlobalConfiguration globalConfiguration) {
            switch (cellData.getStringValue()) {
                default:
                    return 2;
                case "小学组":
                    return 0;
                case "中学组":
                    return 1;
            }
        }
        
        // 导出的时候,将数据库中存储的值,转换为用户能看懂的汉字
        @Override
        public WriteCellData<?> convertToExcelData(Integer value,
                                                   ExcelContentProperty excelContentProperty,
                                                   GlobalConfiguration globalConfiguration) {
            switch (value) {
                default:
                    return new WriteCellData<>("其他组");
                case 0:
                    return new WriteCellData<>("小学组");
                case 1:
                    return new WriteCellData<>("中学组");
            }
        }

    }

    public static class PresentConvertor implements Converter<Integer> {
        @Override
        public Integer convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty,
                                         GlobalConfiguration globalConfiguration) {
            if (cellData.getStringValue().equals("否")) {
                return 0;
            } else {
                return 1;
            }
        }
        
        @Override
        public WriteCellData<?> convertToExcelData(Integer value,
                                                   ExcelContentProperty excelContentProperty,
                                                   GlobalConfiguration globalConfiguration) {
            switch (value) {
                default:
                    return new WriteCellData<>("-");
                case 0:
                    return new WriteCellData<>("否");
                case 1:
                    return new WriteCellData<>("是");
            }
        }
    }

    public static class ProvinceConvertor implements Converter<Integer> {
        @Override
        public Integer convertToJavaData(ReadCellData<?> cellData,
                                         ExcelContentProperty contentProperty,
                                         GlobalConfiguration globalConfiguration) throws Exception {
            if (provinceMap.isEmpty()) {
                throw new Exception("省份配置数据为空");
            }
            return StringUtils.isBlank(cellData.getStringValue()) ? null : provinceMap.get(cellData.getStringValue()).getId();
        }
    }

    public static class SchoolConvertor implements Converter<Integer> {
        @Override
        public Integer convertToJavaData(ReadCellData<?> cellData,
                                         ExcelContentProperty contentProperty,
                                         GlobalConfiguration globalConfiguration) throws Exception {
            if (schoolMap.isEmpty()) {
                throw new Exception("学校配置数据为空");
            }
            return StringUtils.isBlank(cellData.getStringValue()) ? null : schoolMap.get(cellData.getStringValue()).getId();
        }
        
        // 导出转换省略掉
    }

}

3.4、实现导入解析监听器    

        上面的解析监听器是个抽象类,是一种模板模式的设计思想应用,我们根据不同的业务,自己扩展invoke方法和saveData方法,但其实saveData也可以做成模板方法,只是需要依赖内部绑定一个数据层dao接口,Mapper,对于有的人来说,会耦合dao层,但我觉得如果dao层取一个接口,那么也没什么耦合的问题。节省不必要的重复代码,还是值得的。

那么上述的那个模板抽象解析监听器可以改为:

/**
 * @Title: ExcelImportReadListener
 * @Description: 其他人如果觉得invoke()方法不满足其需求,可以自己实现一下
 * @Author: wenrong
 * @Date: 2024/4/25 17:08
 * @Version:1.0
 */
@Data
public abstract class ExcelImportReadListener<T extends ValidateBaseBo, S extends IService<T>> extends AnalysisEventListener<T> {

    private static final Logger log = LoggerFactory.getLogger("excelReadListener");

    public static int BATCH_COUNT = 1000;

    private AtomicLong successNum = new AtomicLong();

    private final Class<T> clazz;

    private S service;

    private Validator<T> validator;

    private List<T> successData = new ArrayList<>();

    private List<T> failureData = new ArrayList<>();


    public ExcelImportReadListener(Class<T> clazz, S service) {
        this.clazz = clazz;
        this.service = service;
    }

    @Override
    public void invoke(T data, AnalysisContext context) {
        log.info("解析到一条数据:{}", JSONObject.toJSON(data));
        StringBuilder errMsg = new StringBuilder();
        try {
            //根据excel数据实体中的javax.validation + 正则表达式来校验excel数据
            errMsg.append(EasyExcelValidateHandler.validateEntity(data));
            // 额外自定义校验,以及设置数据属性的逻辑
            if (validator != null) {
                errMsg.append(validator.validate(data));
            }
        } catch (NoSuchFieldException e) {
            log.error(e.getMessage());
        }
        if (StringUtils.isNotEmpty(errMsg.toString())) {
            data.setErrMsg(errMsg.toString());
            failureData.add(data);
        } else {
            successData.add(data);
            successNum.incrementAndGet();
        }

        if (BATCH_COUNT != 0 && successData.size() >= BATCH_COUNT) {
            try {
                saveData();
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            }
            successData.clear();
        }

    }

    @Override
    public final void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
        // 验证表头
        if (headMap.isEmpty()) {
            throw new ServiceException("无表头");
        }
        if (clazz != null) {
            try {
                Map<Integer, String> indexNameMap = getIndexNameMap(clazz);
                for (Integer index : indexNameMap.keySet()) {
                    log.info("表头字段:{}", headMap.get(index));
                    if (StringUtils.isEmpty(headMap.get(index))) {
                        throw new ServiceException("未设置index");
                    }
                    if (!headMap.get(index).equals(indexNameMap.get(index))) {
                        throw new ServiceException("导入模板错误");
                    }
                }
            } catch (NoSuchFieldException e) {
                log.error(e.getMessage(), e);
            }
        }

    }

    @Override
    public final void doAfterAllAnalysed(AnalysisContext context) {
        log.info("所有数据解析完成!共校验成功{}条数据,校验失败{}条数据", successNum.get(), failureData.size());
        try {
            saveData();
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }

    /**
     * 将该类做成抽象类,在各service中实现saveDate方法,
     * 不侵入业务,同时不会让解析占用内存
     */
    public void saveData() throws Exception {
        log.info("开始往数据库插入数据");
        List<T> successData = this.getSuccessData();
        List<T> failureData = this.getFailureData();
        boolean saved = service.saveBatch(successData);
        if (!saved) {
            successData.forEach(work -> work.setErrMsg("保存失败"));
            failureData.addAll(successData);
        } else {
            this.setSuccessData(successData);
        }
    }

    private Map<Integer, String> getIndexNameMap(Class<T> clazz) throws NoSuchFieldException {
        Map<Integer, String> excelPropertyMap = new HashMap<>();
        Field field;
        Field[] fields = clazz.getDeclaredFields();
        int sequence = 0;
        for (Field item : fields) {
            field = clazz.getDeclaredField(item.getName());
            field.setAccessible(true);
            ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
            if (excelProperty != null) {
                int index = excelProperty.index() == -1 ? sequence : excelProperty.index();
                String[] values = excelProperty.value();
                StringBuilder value = new StringBuilder();
                for (String v : values) {
                    value.append(v);
                }
                excelPropertyMap.put(index, value.toString());
                sequence++;
            }
        }
        return excelPropertyMap;
    }

}

业务代码中实现模板解析监听器的代码示例:

    @Override
    @Transactional
    public ExcelImportReadListener<BasicWorks> importExcel(MultipartFile file) throws IOException {

        List<DyzProvinceSchool> schoolList = dyzProvinceSchoolService.getSchoolList();
        List<DyzProvinceSchool> provinceList = dyzProvinceSchoolService.getProvinceList();
        Map<String, DyzProvinceSchool> schoolMap = schoolList.stream().collect(Collectors.toMap(DyzProvinceSchool::getSchoolName, s -> s));
        Map<Integer, DyzProvinceSchool> schoolMap1 = schoolList.stream().collect(Collectors.toMap(DyzProvinceSchool::getId, s -> s));
        Map<String, DyzProvinceSchool> provinceMap = provinceList.stream().collect(Collectors.toMap(DyzProvinceSchool::getProvinceName, s -> s));
        Map<Integer, DyzProvinceSchool> provinceMap1 = provinceList.stream().collect(Collectors.toMap(DyzProvinceSchool::getId, s -> s));
        BasicWorks.schoolMap = schoolMap;
        BasicWorks.provinceMap = provinceMap;

        // 匿名内部类扩展模板监听器
        ExcelImportReadListener<BasicWorks> readListener = new ExcelImportReadListener<BasicWorks>(BasicWorks.class) {
            @Override
            public void invoke(BasicWorks data, AnalysisContext context) {
                Set<String> allDataExistInExcel = new HashSet<>();
                Set<String> allDataExistInDataSource = list().stream().map(BasicWorks::getWorksNumber).collect(Collectors.toSet());
                List<BasicWorks> failureData = this.getFailureData();

                StringBuilder errMsg = new StringBuilder(data.getErrMsg() == null ? "" : data.getErrMsg());
                if (StringUtils.isBlank(data.getWorksNumber())) {
                    errMsg.append("节目代码不能为空,");
                    data.setErrMsg(errMsg.toString());
                    failureData.add(data);
                } else {
                    if (allDataExistInExcel.contains(data.getWorksNumber())) {
                        errMsg.append("Excel表格中存在重复的数据,")
                                .append("节目代码:").append(data.getWorksNumber());
                        data.setErrMsg(errMsg.toString());
                        failureData.add(data);
                        allDataExistInExcel.add(data.getWorksNumber());

                    }
                    if (allDataExistInDataSource.contains(data.getWorksNumber())) {
                        errMsg.append("数据库中存在重复的数据,")
                                .append("节目代码:").append(data.getWorksNumber());
                        data.setErrMsg(errMsg.toString());
                        failureData.add(data);
                        allDataExistInExcel.add(data.getWorksNumber());
                    }
                }
                allDataExistInExcel.add(data.getWorksNumber());
                super.invoke(data, context);
            }
        
        // 设置javax.validation以外校验器,将会在invoke方法里执行校验
        readListener.setValidator(work -> work.validate(provinceMap1, schoolMap1));
        // 导入 Excel
        EasyExcels.read(file, BasicWorks.class, readListener);
        return readListener;
    }

另外还有需要将表格中图片导入后自动上传到文件服务,然后将url保存在数据库的需求:

public ExcelImportReadListener<BasicHotel> importExcel(MultipartFile file) throws IOException {
    //获取图片,联合Apache 的ExcelUtil,ExcelPicUtil工具类,获取图片数据对象PictureData
    ExcelReader reader = ExcelUtil.getReader(file.getInputStream());
    Map<String, PictureData> picMap = ExcelPicUtil.getPicMap(reader.getWorkbook(), 0);

    ExcelImportReadListener<BasicHotel> readListener = new ExcelImportReadListener<BasicHotel>(BasicHotel.class) {
        @Override
        public void invoke(BasicHotel data, AnalysisContext context) {
            Set<String> allDataExistInDataSource = list().stream().map(BasicHotel::getHotelName).collect(Collectors.toSet());
            Set<String> allDataExistInExcel = new HashSet<>();
            List<BasicHotel> failureData = this.getFailureData();

            StringBuilder errMsg = new StringBuilder(data.getErrMsg() == null ? "" : data.getErrMsg());
            if (StringUtils.isEmpty(data.getErrMsg())) {
                errMsg.append("酒店名称不能为空,");
                data.setErrMsg(errMsg.toString());
                failureData.add(data);
            } else {
                if (allDataExistInExcel.contains(data.getHotelName())) {
                    errMsg.append("Excel表格中存在重复的数据,")
                            .append("酒店名称:").append(data.getHotelName());
                    data.setErrMsg(errMsg.toString());
                    failureData.add(data);
                    allDataExistInExcel.add(data.getHotelName());
                }
                if (allDataExistInDataSource.contains(data.getHotelName())) {
                    errMsg.append("数据库中存在重复的数据,")
                            .append("酒店名称:").append(data.getHotelName());
                    data.setErrMsg(errMsg.toString());
                    failureData.add(data);
                    allDataExistInExcel.add(data.getHotelName());
                }
            }
            allDataExistInExcel.add(data.getHotelName());
            String err = "";
            int rowIndex = context.readRowHolder().getRowIndex() + 1;
            PictureData pictureData = picMap.get(rowIndex + "_0");
            if (pictureData == null) {
                err = String.format(data.getErrMsg() + "第%s行,%s", rowIndex, "酒店照片为空");
            }

            try {
                // 上传图片
                String fileUrl = ossFileController.ftpUploadFile(pictureData.getData(), "", data.getHotelName());
                data.setPicture(fileUrl);
            } catch (IOException ex) {
                err = String.format(data.getErrMsg() + "第%s行,%s", rowIndex, "酒店照片为空上传失败");
            }

            data.setErrMsg(err);
            super.invoke(data, context);
        }
    };
    // 导入 Excel
    EasyExcels.read(file, BasicHotel.class, readListener);
    return readListener;
}

        这是导入部分,导出部分,五花八门的需求就比较多了。

四、导出

4.1、导出 数据导入模板

模板一般会有下拉选项列的需求,下拉列一般用注解枚举几个就行了:


import java.lang.annotation.*;

/**
 * 标注导出的列为下拉框类型,并为下拉框设置内容
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ExcelSelected {
    /**
     * 固定下拉内容
     */
    String[] source() default {};

    /**
     * 设置下拉框的起始行,默认为第二行
     */
    int firstRow() default 1;

    /**
     * 设置下拉框的结束行,默认为最后一行
     */
    int lastRow() default 0x10000;
}

下拉注解解析器:

@Data
@Slf4j
public class ExcelSelectedResolve {
    /**
     * 下拉内容
     */
    private String[] source;

    /**
     * 设置下拉框的起始行,默认为第二行
     */
    private int firstRow;

    /**
     * 设置下拉框的结束行,默认为最后一行
     */
    private int lastRow;

    public String[] resolveSelectedSource(ExcelSelected excelSelected) {
        if (excelSelected == null) {
            return null;
        }

        // 获取固定下拉框的内容
        String[] source = excelSelected.source();
        if (source.length > 0) {
            return source;
        }

//        // 获取动态下拉框的内容
//        Class<? extends ExcelDynamicSelect>[] classes = excelSelected.sourceClass();
//        if (classes.length > 0) {
//            try {
//                ExcelDynamicSelect excelDynamicSelect = classes[0].newInstance();
//                String[] dynamicSelectSource = excelDynamicSelect.getSource();
//                if (dynamicSelectSource != null && dynamicSelectSource.length > 0) {
//                    return dynamicSelectSource;
//                }
//            } catch (InstantiationException | IllegalAccessException e) {
//                log.error("解析动态下拉框数据异常", e);
//            }
//        }
        return null;
    }

}

下拉handler:

public class SelectSheetWriteHandler implements SheetWriteHandler {

    private final Map<Integer, ExcelSelectedResolve> selectedMap;

    public SelectSheetWriteHandler(Map<Integer, ExcelSelectedResolve> selectedMap) {
        this.selectedMap = selectedMap;
    }

    /**
     * Called before create the sheet
     */
    @Override
    public void beforeSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {

    }

    /**
     * Called after the sheet is created
     */
    @Override
    public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
        // 这里可以对cell进行任何操作
        Sheet sheet = writeSheetHolder.getSheet();
        DataValidationHelper helper = sheet.getDataValidationHelper();
        selectedMap.forEach((k, v) -> {
            // 设置下拉列表的行: 首行,末行,首列,末列
            CellRangeAddressList rangeList = new CellRangeAddressList(v.getFirstRow(), v.getLastRow(), k, k);
            // 设置下拉列表的值
            DataValidationConstraint constraint = helper.createExplicitListConstraint(v.getSource());
            // 设置约束
            DataValidation validation = helper.createValidation(constraint, rangeList);
            // 阻止输入非下拉选项的值
            validation.setErrorStyle(DataValidation.ErrorStyle.STOP);
            validation.setShowErrorBox(true);
            validation.setSuppressDropDownArrow(true);
            validation.createErrorBox("提示", "请输入下拉选项中的内容");
            sheet.addValidationData(validation);
        });
    }

}

        其实如果是动态的下拉列表,不能固定枚举的话,直接从配置数据表中拉出业务配置列表,将列表作为传参,使用util类EasyExcels第一个方法就好。

    @ExcelProperty(value = "组别")
    @ExcelSelected(source = {"小学组", "初中组"})
    @ApiModelProperty(value = "组别:0-小学组,1-初中组,2-其他组")
    @NotNull(message = "不能为空")
    private String groupType;



    @ExcelProperty(value = "是否出席")
    @ExcelSelected(source = {"是", "否"})
    @ApiModelProperty(value = "是否出席:0-否,1是")
    @NotNull(message = "不能为空")
    private String present;

4.2、图片导出convertor:

public class UrlPictureConverter implements Converter<String> {
    public static int urlConnectTimeout = 2000;
    public static int urlReadTimeout = 6000;

    @Override
    public Class<?> supportJavaTypeKey() {
        return String.class;
    }

    @Override
    public WriteCellData<?> convertToExcelData(String url, ExcelContentProperty contentProperty,
                                               GlobalConfiguration globalConfiguration) throws IOException {
        InputStream inputStream = null;
        try {
            URL value = new URL(url);
            if (ObjectUtils.isEmpty(value)) {
                return new WriteCellData<>("");
            }
            URLConnection urlConnection = value.openConnection();
            urlConnection.setConnectTimeout(urlConnectTimeout);
            urlConnection.setReadTimeout(urlReadTimeout);
            inputStream = urlConnection.getInputStream();
            byte[] bytes = IoUtils.toByteArray(inputStream);
            return new WriteCellData<>(bytes);
        } catch (Exception e) {
            log.info("图片获取异常", e);
            return new WriteCellData<>("图片获取异常");
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }
        }
    }
}

4.3、有合并单元格导出:

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ExcelIgnoreUnannotated
@Slf4j
public class WorkJudgesStatisticsVo implements Serializable {
    private static final long serialVersionUID = 1L;

    @ExcelProperty(value = "序号", index = 0)
    private String sequence;



    public static class MergeStrategy implements RowWriteHandler {

        private int totalRowNum;

        public MergeStrategy(int totalRowNum) {
            this.totalRowNum = totalRowNum;
        }

        public static MergeStrategy build(int totalRowNum) {
            return new MergeStrategy(totalRowNum);
        }

        @Override
        public void afterRowDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Integer relativeRowIndex, Boolean isHead) {
            if (isHead) {
                // 处理表头
                return;
            }
            log.info("当前行号:{}", row.getRowNum());
            log.info("总行数:{}", totalRowNum);
            // 合并
            if (row.getRowNum() != totalRowNum + 1) {
                return;
            }
            writeSheetHolder.getSheet().addMergedRegion(new CellRangeAddress(writeSheetHolder.getLastRowIndex(), writeSheetHolder.getLastRowIndex(), 5, 6));
        }
    }


}

4.4、行转列,并使用模板的方式导出:

@Override
public void selectWorksJudgesResultReview(HttpServletResponse response) throws IOException {
    List<Map<String, Object>> views = scoreReviewWorksJudgesMapper.selectWorksJudgesResultReview();
    List<DyzScoreReviewWorksJudges> reviewWorksJudges = scoreReviewWorksJudgesMapper.selectList();
    List<DyzScoreWorksFiles> scoreWorksFiles = worksFilesMapper.selectList();
    HashMap<Long, List<DyzScoreWorksFiles>> fileMap = new HashMap<>();
    scoreWorksFiles.forEach(f -> {
        List<DyzScoreWorksFiles> files = fileMap.computeIfAbsent(f.getWorksId(), k -> new ArrayList<>());
        files.add(f);
    });

    HashMap<Long, List<DyzScoreReviewWorksJudges>> scoreMap = new HashMap<>();
    reviewWorksJudges.forEach(judge -> {
        List<DyzScoreReviewWorksJudges> judges = scoreMap.computeIfAbsent(judge.getWorksId(), k -> new ArrayList<>());
        judges.add(judge);
    });
    AtomicInteger sequence = new AtomicInteger(0);
    views.forEach(map -> {
        map.put("sequence", String.valueOf(sequence.incrementAndGet()));
        Long workId = Long.valueOf(map.get("workId").toString());
        List<DyzScoreReviewWorksJudges> judges = scoreMap.get(workId);
        for (int i = 0; i < 15; i++) {
            map.put("score" + (i + 1), "");
            map.put("correctness" + (i + 1), "");
        }
        map.put("avgScore", "");
        map.put("avgScore1", "");

        if (judges != null && judges.size() > 0) {
            AtomicInteger serialNo = new AtomicInteger(0);
            AtomicInteger serialNo1 = new AtomicInteger(0);
            judges.forEach(j -> {
                map.put("score" + serialNo.incrementAndGet(), j.getScore());
                map.put("correctness" + serialNo1.incrementAndGet(), j.getRemark());
            });
            judges.sort(Comparator.comparing(DyzScoreReviewWorksJudges::getScore));
            BigDecimal sum = judges.stream().map(DyzScoreReviewWorksJudges::getScore).reduce(BigDecimal.ZERO, BigDecimal::add);
            BigDecimal avg = sum.divide(BigDecimal.valueOf(judges.size()), 2, BigDecimal.ROUND_HALF_UP);
            map.put("avgScore", avg);
            if (judges.size() > 3) {
                judges.remove(0);
                judges.remove(judges.size() - 1);
                BigDecimal sum1 = judges.stream().map(DyzScoreReviewWorksJudges::getScore).reduce(BigDecimal.ZERO, BigDecimal::add);
                BigDecimal avg1 = sum1.divide(BigDecimal.valueOf(judges.size()), 2, BigDecimal.ROUND_HALF_UP);
                map.put("avgScore1", avg1);
            }
        }

        for (int i = 0; i < 4; i++) {
            map.put("fileName" + (i + 1), "");
        }
        List<DyzScoreWorksFiles> files = fileMap.get(workId);
        if (files != null && files.size() > 0) {
            AtomicInteger serialNo = new AtomicInteger(0);
            files.forEach(f -> map.put("fileName" + serialNo.incrementAndGet(), f.getUrl()));
        }

    });

    ConcurrentHashSet<String> columns = views.stream().flatMap(map -> map.keySet().stream()).collect(Collectors.toCollection(ConcurrentHashSet::new));
    List<String> scoreColumns = columns.stream().filter(c -> c.contains("score") || c.contains("avgScore")).collect(Collectors.toList());
    List<String> correctnessColumns = columns.stream().filter(c -> c.contains("correctness")).collect(Collectors.toList());

    //输入流
    InputStream inputStream = null;
    ServletOutputStream outputStream = null;
    ExcelWriter excelWriter = null;
    try {
        org.springframework.core.io.Resource templateFile = resourceLoader.getResource("classpath:templates\\XXXX报表.xlsx");
        inputStream = templateFile.getInputStream();
        // 获取文件名并转码
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        outputStream = response.getOutputStream();
        // 创建填充配置
        FillConfig fillConfig = FillConfig.builder().forceNewRow(true).build();
        // 创建写对象
        excelWriter = EasyExcel.write(outputStream)
                .withTemplate(inputStream).build();
        // 创建Sheet对象
        WriteSheet sheet = EasyExcel.writerSheet(0, "报名数量统计").build();
        excelWriter.fill(views, fillConfig, sheet);
        excelWriter.fill(new FillWrapper("scoreColumns", scoreColumns), sheet);
        excelWriter.fill(new FillWrapper("correctnessColumns", correctnessColumns), sheet);
    } catch (Exception e) {
        log.error("导出失败={}", e.getMessage());
    } finally {
        if (excelWriter != null) {
            excelWriter.finish();
        }
        //关闭流
        if (outputStream != null) {
            try {
                outputStream.close();
            } catch (IOException e) {
                log.error("关闭输出流失败", e);
            }
        }
        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (IOException e) {
                log.error("关闭输入流失败", e);
            }
        }

    }
}

模板里面的取值占位符写法

        还有一些表格宽度,高度自适应策略,美化风格的代码就不贴了,需要的话到我的资源中去下载。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/929675.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于SpringBoot+Vue的美妆购物网站

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏&#xff1a;…

【Altium Designer 】AD如何使用嘉立创元器件的3D封装

1.下载3D封装 以STM32F407VGT6为例&#xff0c;进入嘉立创商城网站&#xff0c;找到需要的元器件封装 复制编号&#xff0c;打开嘉立创EDA&#xff0c;编译器选择专业版&#xff0c;新建工程&#xff0c;点击PCB1 复制编号在搜索框中&#xff0c;点击搜索&#xff0c;然后放置…

轨道力学:兰伯特问题

轨道力学&#xff1a;兰伯特问题 引言 在轨道力学中&#xff0c;兰伯特问题是指在已知两个位置矢量和它们之间的飞行时间的情况下&#xff0c;求解连接这两个位置的轨道路径问题。该问题以18世纪的数学家约翰海因里希兰伯特&#xff08;Johann Heinrich Lambert&#xff09;命…

计算机网络 第5章 运输层

计算机网络 &#xff08;第8版&#xff09; 第 5 章 传输层5.4 可靠传输的原理5.4.1 停止等待协议5.4.2 连续ARQ协议 5.5 TCP报文段的首部格式5.6 TCP可靠传输的实现5.6.1 以字节为单位的滑动窗口5.6.2 超时重传时间的选择 5.7 TCP的流量控制5.7.1 利用滑动窗口实现流量控制 5.…

【AI系统】EfficientNet 系列

EfficientNet 系列 本文主要介绍 EffiicientNet 系列&#xff0c;在之前的文章中&#xff0c;一般都是单独增加图像分辨率或增加网络深度或单独增加网络的宽度&#xff0c;来提高网络的准确率。而在 EfficientNet 系列论文中&#xff0c;会介绍使用网络搜索技术(NAS)去同时探索…

debian编译失败

A、缘由和分析 debian的代码在删除该路径下的2个包后&#xff0c; 重新全编&#xff0c;编译不过的问题。 至于我为什么删除这2个包&#xff0c;这是因为在sdk第一次编译时一些文件已经打包进去了&#xff0c;我现在的修改无法更新进img中&#xff0c;而现在我的项目中不需要…

2024年12月5日Github流行趋势

项目名称&#xff1a;HelloGitHub 项目维护者&#xff1a;521xueweihan, yaowenqiang, daixiang0等项目介绍&#xff1a;分享 GitHub 上有趣、入门级的开源项目。项目star数&#xff1a;95,244项目fork数&#xff1a;9,707 项目名称&#xff1a;Best-websites-a-programmer-sh…

大数据新视界 -- 大数据大厂之 Hive 临时表与视图:灵活数据处理的技巧(上)(29 / 30)

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…

第一部分:基础知识 3. 数据类型 --[MySQL轻松入门教程]

第一部分:基础知识 3. 数据类型 --[MySQL轻松入门教程] MySQL 支持多种数据类型,这些数据类型可以分为几大类:数值类型、字符串类型、日期和时间类型、二进制类型以及枚举和集合。每种类型都有其特定的用途和存储需求。以下是 MySQL 中常用的数据类型的详细介绍: 1. 数值…

百问FB显示开发图像处理 - 图像调整

2.4 图像调整 2.4.1 图像的缩放 2.4.1.1 图像缩放算法浅析 图像缩放算法有很多种&#xff0c;这里参考网友"lantianyu520"所著的"图像缩放算法"。 原理浅析 ​ 要理解这个图像缩放算法的原理&#xff0c;最重要的是需要理解&#xff1a;对于图像上的每…

嵌入式驱动开发详解5(ioctl的使用)

文章目录 ioctl介绍应用层详解驱动层详解ioctl的cmd实验例程 ioctl介绍 linux内核给用户提供了两类系统调用函数&#xff1a;一类是数据操作函数&#xff0c;比如read、write…。 另外一类函数是非数据操作函数&#xff0c;比如ioctl…&#xff0c;用户程序可以用ioctl给底层设…

从零开始使用GOT-OCR2.0——多模态OCR项目:微调数据集构建 + 训练(解决训练报错,成功实验微调训练)

在上一篇文章记录了GOT-OCR项目的环境配置和基于官方模型参数的基础使用。环境安装的博文快速链接&#xff1a; 从零开始使用GOT-OCR2.0——多模态通用型OCR&#xff08;非常具有潜力的开源OCR项目&#xff09;&#xff1a;项目环境安装配置 测试使用-CSDN博客 本章在环境配置…

Facebook:筑牢隐私安全堡垒,守护社交净土

在全球社交媒体平台中&#xff0c;Facebook一直是风靡全球的佼佼者。然而&#xff0c;随着数字化信息的迅速膨胀&#xff0c;用户隐私保护的重要性日益凸显。面对用户对数据安全性的高度重视&#xff0c;Facebook致力于通过一系列措施来确保隐私保护&#xff0c;守护每位用户的…

VBA信息获取与处理第四个专题第二节:将工作表数据写入VBA数组

《VBA信息获取与处理》教程(版权10178984)是我推出第六套教程&#xff0c;目前已经是第一版修订了。这套教程定位于最高级&#xff0c;是学完初级&#xff0c;中级后的教程。这部教程给大家讲解的内容有&#xff1a;跨应用程序信息获得、随机信息的利用、电子邮件的发送、VBA互…

python通过ODBC连接神通数据库

1、安装神通数据库 2、安装python 3、安装pyodbc pip3 install pyodbc-5.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl 注&#xff1a;pyodbc要和python版本相对应 4、安装unixodbc 5、配置神通数据库ODBC数据源 6、示例代码如下 #!/usr/bin/python…

Vue Web开发(一)

1. 环境配置 1.1. 开发工具下载 1.1.1. HbuilderX 官网地址&#xff1a;https://uniapp.dcloud.net.cn/ 1.1.2. Visual Studio Code 官网地址&#xff1a;https://code.visualstudio.com/Download 1.1.3. Node环境 官网地址&#xff1a;https://nodejs.cn/   正常软件安装…

Tomcat新手成长之路:安装部署优化全解析(下)

接上篇《Tomcat新手成长之路&#xff1a;安装部署优化全解析&#xff08;上&#xff09;》: link 文章目录 7.应用部署7.1.上下文7.2.启动时进行部署7.3.动态应用部署 8.Tomcat 类加载机制8.1.简介8.2.类加载器定义8.3.XML解析器和 Java 9.JMS监控9.1.简介9.2.启用 JMX 远程监…

模拟实现vector(非常详细)

模拟实现vector 1.vector基本概念2.vector()默认构造函数3.size()成员函数迭代器4.capacity()成员函数5.empty()成员函数6.reverse()成员函数7.push_back()成员函数8.pop_back()成员函数9.operator[ ]成员函数10.resize()成员函数11.insert()成员函数12.erase()成员函数13.swap…

Elastic Cloud Serverless:深入探讨大规模自动扩展和性能压力测试

作者&#xff1a;来自 Elastic David Brimley, Jason Bryan, Gareth Ellis 及 Stewart Miles 深入了解 Elasticsearch Cloud Serverless 如何动态扩展以处理海量数据和复杂查询。我们探索其在实际条件下的性能&#xff0c;深入了解其可靠性、效率和可扩展性。 简介 Elastic Cl…

微信小程序之手机归属地查询

微信小程序之手机归属地查询 需求描述 API申请和小程序设置 API申请 第一步&#xff1a;完整账号注册 我们需要来到如下网站&#xff0c;注册账号&#xff1a;万维易源 第二步&#xff1a;账号注册完成以后&#xff0c;点击右上角的控制台信息。 第三步&#xff1a;在控制…