java实现excel的导入导出(带参数校验:非空校验、数据格式校验)更新版

一、前言

​ 在一个后台管理系统中,对excel的操作在项目中的使用是难免的,之前为了解决带参数校验的excel导入导出我整合了一个小demo,比较简单可能现实代码也不是很完善,比如以下问题没有得到很好的解决

​ 1.参数校验类型比较单一且不能进行组合校验。

​ 2.实现ReadListener 在监听读取数据的时候同时进行校验代码比较复杂,也会影响本来的读取效率。

​ 3.最终的检验结果获取不太优雅,结果就是一个拼接的字符串也不太符合面向对象的思想。

​ 4.没有完整的仓库案例导致读者不能拿到完整的代码,和留言问题没有得到回复。本次将会在gitee上上传完整代码。

二、优化思路

​ 针对上面的一些问题进行了本次的改造,同样使用的是阿里巴巴的开源框架easy-excel结合自定义注解实现,
本次采用了分布和结果封装,将读取数据和数据校验分开了,而不是在读取数据的同时进行数据校验,将校验结果用CheckResult来进行封装。实现了定位更加精确以至于某行,某个字段,错误原因,和错误信息都进行了记录。

三、结构说明

​ 如图:

在这里插入图片描述

1.annotaion :

自定义注解配合属性实现检验规则的配置。

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NotNull {
    String name() default ""; //字段名称

    String message() default CheckResult.MSG_REQUIRE;//错误信息

    String reason() default CheckResult.REASON_REQUIRE; // 错误原因

    String cfName() default ""; // 条件必填的字段名称

    String cfValue() default ""; // 条件必填的值,多个英文逗号隔开

    boolean regex() default false;// 是否进行格式判断;
    String regexValue() default CheckResult.REGEX_DEFAULT;// 格式正则表达式;

}
2.ExcelUtil:

提供了导入导出方法和一些配套实现的方法。

package com.mr.yushao.exceldemo.utils;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.read.builder.ExcelReaderBuilder;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.excel.write.metadata.fill.FillConfig;
import com.alibaba.excel.write.metadata.fill.FillWrapper;
import com.alibaba.fastjson2.JSON;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.*;

/**
 * excel 导入导出工具类
 */
@Slf4j
public class ExcelUtil {
    public static final String MEAT_DATA_LINE_NUMBER = "line_number";
    public static final String MEAT_DATA_LINE_NUMBER_CAM = "lineNumber";

    /**
     * 按模板导出excel
     *
     * @param resp     响应流
     * @param template 模板输入流
     * @param filename 导出文件名称
     * @param dataList 数据
     * @param <T>      数据泛型
     * @throws IOException IO异常
     */
    public static <T> void exportWithTemplate(HttpServletResponse resp, InputStream template, String filename, List<T> dataList) throws IOException {
        resp.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        resp.setCharacterEncoding("utf-8");
        resp.setHeader("Content-disposition", "attachment;filename*=utf-8''" + filename + ".xlsx");
        // 写入模板
        ServletOutputStream outputStream = resp.getOutputStream();
        ExcelWriter writer = EasyExcel.write(outputStream).withTemplate(template).build();
        Map<String, List<T>> dataMap = new HashMap<>();
        dataMap.put("data", dataList);
        dataMap.forEach((k, v) -> writer.fill(new FillWrapper(k, v), new FillConfig(), EasyExcel.writerSheet(0).build()));
        writer.finish();
        outputStream.close();
    }

    /**
     * 按模板导出生成inputStream
     *
     * @param template 模板输入流
     * @param dataList 数据
     * @param <T>      数据泛型
     * @throws IOException IO异常
     */
    public static <T> InputStream exportWithTemplate(InputStream template, Collection<T> dataList) throws IOException {
        // 写入模板
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        ExcelWriter writer = EasyExcel.write(os).withTemplate(template).build();
        Map<String, Collection<T>> dataMap = new HashMap<>();
        dataMap.put("data", dataList);
        dataMap.forEach((k, v) -> writer.fill(new FillWrapper(k, v), new FillConfig(), EasyExcel.writerSheet(0).build()));
        writer.finish();
        byte[] buffer = os.toByteArray();
        return new ByteArrayInputStream(buffer);
    }

    /**
     * 按模板导出生成inputStream
     *
     * @param template 模板输入流
     * @param dataList 数据
     * @param <T>      数据泛型
     * @param sheetNo  指定sheet
     * @throws IOException IO异常
     */
    public static <T> InputStream exportWithTemplate(InputStream template, Collection<T> dataList, Integer sheetNo) throws IOException {
        // 写入模板
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        ExcelWriter writer = EasyExcel.write(os).withTemplate(template).build();
        Map<String, Collection<T>> dataMap = new HashMap<>();
        dataMap.put("data", dataList);
        dataMap.forEach((k, v) -> writer.fill(new FillWrapper(k, v), new FillConfig(), EasyExcel.writerSheet(sheetNo).build()));
        writer.finish();
        byte[] buffer = os.toByteArray();
        return new ByteArrayInputStream(buffer);
    }

    /**
     * 校验读取当前行的数据是否全部为空或者空串
     *
     * @param dto 当前行数据
     * @param <T> 泛型
     * @return 全部为空返回true反之false
     */
    public static <T> Boolean checkAllNull(T dto) {
        boolean flag = true;
        Map<String, Object> tmp = JSON.parseObject(JSON.toJSONString(dto));
        for (String s : tmp.keySet()) {
            if (MEAT_DATA_LINE_NUMBER.equals(s)) {
                continue;
            }
            if (StringUtils.hasText(Objects.nonNull(tmp.get(s)) ? tmp.get(s).toString() : "")) {
                flag = false;
                break;
            }
        }
        return flag;
    }

    /**
     * 读取Excel表格返回数据集合,默认读取第一个sheet表数据(不需要校验)
     *
     * @param inputStream   输入流
     * @param clazz         数据映射类
     * @param headRowNumber 标题占用的行数
     * @return List结果集
     */
    public static <T> List<T> readExcel(InputStream inputStream, Class<T> clazz, Integer headRowNumber) {
        if (inputStream == null || clazz == null) {
            return null;
        }
        ExcelListener<T> excelListener = new ExcelListener<>();
        ExcelReaderBuilder read = EasyExcel.read(inputStream, clazz, excelListener);
        read.sheet().headRowNumber(headRowNumber).doRead();
        return excelListener.getDataList();
    }

    /**
     * 读取Excel表格返回数据集合,支持校验数据格式
     *
     * @param is       输入流
     * @param clazz    excel每一行对应的实体类
     * @param startRow 数据开始行号,第一行序号为0
     * @param <T>      泛型
     * @return 数据集合
     */
    public static <T> List<T> readExcelForCheck(InputStream is, Class<T> clazz, Integer startRow) {
        long start = System.currentTimeMillis();
        List<T> list = new ArrayList<>();
        EasyExcel.read(is, clazz, new ReadListener<T>() {
            @Override
            public void invoke(T t, AnalysisContext analysisContext) {
                Integer rowIndex = analysisContext.readRowHolder().getRowIndex();
                try {
                    Field declaredField = t.getClass().getDeclaredField(MEAT_DATA_LINE_NUMBER_CAM);
                    declaredField.setAccessible(true);
                    declaredField.set(t, rowIndex + 1);
                } catch (NoSuchFieldException e) {
                    throw new RuntimeException("实体类不包含行号");
                } catch (IllegalAccessException e) {
                    throw new RuntimeException(e);
                }
                if (!checkAllNull(t)) {
                    list.add(t);
                }
            }

            @Override
            public void doAfterAllAnalysed(AnalysisContext analysisContext) {
                long end = System.currentTimeMillis();
                log.info("excel读取完毕,共计{}条数据,耗时:{}", list.size(), end - start);
            }
        }).sheet().headRowNumber(startRow).doRead();
        return list;
    }

    /**
     * 读取文件解析监听类
     *
     * @param <T>
     */
    public static class ExcelListener<T> extends AnalysisEventListener<T> {
        /**
         * 存放读取后的数据
         */
        public List<T> dataList = new ArrayList<>();

        /**
         * 逐条读取数据
         */
        @Override
        public void invoke(T t, AnalysisContext analysisContext) {
            dataList.add(t);
        }

        /**
         * 解析读取数据总条数
         */
        @Override
        public void doAfterAllAnalysed(AnalysisContext analysisContext) {
            log.info("读取数据条数:{}条!", dataList.size());
        }

        public List<T> getDataList() {
            return this.dataList;
        }
    }

    /**
     * 模板准确性检查
     *
     * @param file         excel 文件
     * @param templateName 正确返回true,错误返回false
     * @return
     */
    public static Boolean templateCheck(MultipartFile file, String templateName) {
        Workbook workbook;
        String filename = file.getOriginalFilename();
        try {
            InputStream fis = file.getInputStream();
            if (filename.endsWith(".xlsx")) {
                workbook = new XSSFWorkbook(fis);
            } else if (filename.endsWith(".xls")) {
                workbook = new HSSFWorkbook(fis);
            } else {
                throw new IllegalArgumentException("不支持的文件类型");
            }
        } catch (IOException e) {
            log.debug(e.getMessage());
            return false;
        }
        return templateName.equals(workbook.getSheetName(0));
    }
}

四、使用方法

1.导入excel并校验数据

step1:ExcelUtil读取excel数据—>readExcelForCheck()

step2:CheckResult执行校验数据的方法—>CheckResult.checkResultList()

准备待导入的excel数据如下:

在这里插入图片描述

根据表格的内容我们定义用于接收excel数据的实体类User:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    /**
     * 行号,提示错误信息时展示行号
     */
    @ExcelIgnore
    private Integer lineNumber;
    @NotNull(name = "姓名")
    private String name;
    @NotNull(name = "年龄")
    private Integer age;
    @NotNull(name = "性别")
    private String sex;
    @NotNull(name = "邮箱", regex = true, regexValue = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$\n")
    private String email;
    @NotNull(name = "地址", cfName = "sex", cfValue = "女", message = "性别为女,地址必填", regex = true)
    private String address;
}

这时候只需要在要校验的字段上加@NotNull注解就可以了,

例子的规则:姓名—必填,年龄–必填,性别–必填,邮箱–必填、格式为邮箱,地址–当性别为‘女’的时候必填。

tips1:lineNumber只是为了标注excel的行号,对于实体类无实际意义,需要加@ExcelIgnore来忽略。
tips2: 当cfValue有多个值的时候有英文逗号隔开,例如“男,女”。

执行结果如下:
在这里插入图片描述

当然前端可以ui做出表格弹出更加美观!!!

controller代码如下:

package com.mr.yushao.exceldemo.controller;

import com.mr.yushao.exceldemo.utils.ExcelUtil;
import com.mr.yushao.exceldemo.vo.CheckResult;
import com.mr.yushao.exceldemo.vo.R;
import com.mr.yushao.exceldemo.vo.User;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

@RestController
@RequestMapping("/api/v1")
public class TestController {
    @PostMapping("read")
    public R readExcelAndCheck(MultipartFile file) {
        try {
            List<User> users = ExcelUtil.readExcelForCheck(file.getInputStream(), User.class, 1);
            //检查格式
            List<CheckResult> checkResults = CheckResult.checkResultList(users);
            if (!CollectionUtils.isEmpty(checkResults)) {
                return R.failer(checkResults);
            }
            return R.success();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @GetMapping("down")
    public void export(HttpServletResponse response) {
        try {
            List<User> users = new ArrayList<>();
            users.add(new User(1, "张三", 20, "男", "11@qq.com", "xxxxx"));
            users.add(new User(2, "李四", 18, "男", "22@qq.com", "xxxxx"));
            InputStream inputStream = this.getClass().getResourceAsStream("/static/exceltemplates/users.xlsx");
            ExcelUtil.exportWithTemplate(response, inputStream, "users", users);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
2.导出数据

本次的导出需要准备一个模板,例如/static\exceltemplates\users.xlsx。虽然比起直接导出多了一个模板但是也有好处,当你模板比较复杂的时候如合并表头、提前准备的文字描述等都可以直接在模板中设定好,然后通过数据绑定来是实体集合按照对于的绑定关系导出到excel。

如何指定实体类和excel模板的导出关系?

只需要在模板中加上{data.字段名称}。{}为框架规定的标识符,data是自己设定的前缀,后面紧跟实体类的字段就完成了绑定。User实体与excel模板的绑定如下:

在这里插入图片描述

执行结果如下:
在这里插入图片描述

五、总结

​ 本次的小案例可以直接做出jar包使用,就完成了一个待参数校验的导入导出工具包,当然校验规则可以在注解里面去丰富完善,相比上版的案例还是简单了不少哦。后续可能还会有其他方案更新!
代码仓库地址(click)

有问题留言大家一起交流学习。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值