Java动态解析excel

1、使用的技术

  • javassist

Javassist 使 Java 字节码操作变得简单。它是一个用于在 Java 中编辑字节码的类库。它使 Java 程序可以在运行时定义新类,并在 JVM 加载它时修改类文件。与其他类似的字节码编辑器不同,Javassist 提供了两个级别的 API:源级别和字节代码级别。如果用户使用源代码级 API,则他们可以在不了解 Java 字节码规范的情况下编辑类文件。整个 API 仅使用 Java 语言的词汇表进行设计。您甚至可以以源文本的形式指定插入的字节码。Javassist 可以即时对其进行编译。另一方面,字节码级别的 API 允许用户像其他编辑器一样直接编辑类文件。

  • easyPOI

Easypoi 是一个基于 Apache POI 的 Java 库,极大地简化了 Excel 文件的读写操作。它提供了一套简单易用的 API,使得开发者可以高效地处理 Excel 数据导入导出任务,无需深入学习复杂的 POI 库细节。Easypoi 支持模板导出、数据验证、自定义样式等多种功能,广泛应用于报表生成、数据预处理等场景。

快速入门可以看我的这篇文章:JAVA操作Excel(POI、easyPOI、easyExcel)

2、包导入(基于SpringBoot)

        <!--easypoi-->
        <dependency>
            <groupId>cn.afterturn</groupId>
            <artifactId>easypoi-spring-boot-starter</artifactId>
            <version>4.4.0</version>
        </dependency>
		<!-- javassist easypoi 自带了,可以不用引入 下图有展示 -->
<!--        <dependency>-->
<!--            <groupId>org.javassist</groupId>-->
<!--            <artifactId>javassist</artifactId>-->
<!--            <version>3.25.0-GA</version>-->
<!--        </dependency>-->

		<!-- easypoi 的 javassist 如果和其他包的冲突了,可以使用以下代码排除掉该包的 javassist 下图有展示 -->
<!--        <dependency>-->
<!--            <groupId>cn.afterturn</groupId>-->
<!--            <artifactId>easypoi-spring-boot-starter</artifactId>-->
<!--            <version>4.4.0</version>-->
<!--            <exclusions>-->
<!--                <exclusion>-->
<!--                    <groupId>org.javassist</groupId>-->
<!--                    <artifactId>javassist</artifactId>-->
<!--                </exclusion>-->
<!--            </exclusions>-->
<!--        </dependency>-->

依赖有冲突示意图

在这里插入图片描述

3、代码(注意:我下面有时说的字段、有时说的属性,是一个东西)

(1)、动态创建类的工具类


/**
 * 动态生成 含有注解 @Excel 的实体工具类
 */
public class CreateClassUtils {

    // class 所在的堆名
    public static final String CLASS_NAME_PREFIX = "com.example.test.pojo.CreateBeanDto@";
    // easyPOI 注解的类包名
    public static final String ANNOTATION_PACKAGE_NAME = "cn.afterturn.easypoi.excel.annotation.Excel";
    // 目前只能使用 字符串
    public static final String STRING_PACKAGE_NAME = "java.lang.String";

    /**
     * 动态生成类
     */
    public static Class<?> generatePrototypeClass(List<BeanDto> list) throws NotFoundException, CannotCompileException, IOException {
        // 类目拼接
        String className = CLASS_NAME_PREFIX + UUID.randomUUID().toString();
        // 类池
        ClassPool pool = ClassPool.getDefault();
        // 往池子里面新增一个 类 赋名字
        CtClass clazz = pool.makeClass(className);
        // 获取类文件
        ClassFile ccFile = clazz.getClassFile();
        // 获取文件常量池
        ConstPool constpool = ccFile.getConstPool();
        // 添加字段
        addExpressField(pool, clazz, constpool, list);

        return clazz.toClass();
    }

    private static void addExpressField(ClassPool pool, CtClass clazz, ConstPool constpool, List<BeanDto> list) throws CannotCompileException, NotFoundException {
        // 将数据库查出动态附上 属性
        for (BeanDto bean : list) {
            // 属性需要小写 toLowerCase()
            addFieldAndAnnotation(pool, clazz, constpool, bean.getAnnotationName(), bean.getProperty().toLowerCase());
        }
    }

    /**
     * 添加字段和注解
     * 目前只支持字符串 其实基本上也够了 可以拿到字符串转成其他的类型
     * 如果后续我又兴趣会更新其他类型的
     */
    private static void addFieldAndAnnotation(ClassPool pool, CtClass clazz, ConstPool constpool, String titleName, String fieldName) throws NotFoundException, CannotCompileException {

        // 生成field
        CtField field = new CtField(pool.get(STRING_PACKAGE_NAME), fieldName, clazz);
        field.setModifiers(Modifier.PRIVATE);

        // 添加 easyPoi 的注解
        AnnotationsAttribute fieldAttr = new AnnotationsAttribute(constpool, AnnotationsAttribute.visibleTag);
        Annotation annotation = new Annotation(ANNOTATION_PACKAGE_NAME, constpool);
        // 添加注解名 name 的含义就是指 表头
        annotation.addMemberValue("name", new StringMemberValue(titleName, constpool));
        // 添加宽度
        annotation.addMemberValue("width", new DoubleMemberValue(30.0, constpool));
        fieldAttr.addAnnotation(annotation);
        field.getFieldInfo().addAttribute(fieldAttr);

        // 生成get/set方法
        clazz.addMethod(CtNewMethod.getter("get" + upperFirstLatter(fieldName), field));
        clazz.addMethod(CtNewMethod.setter("set" + upperFirstLatter(fieldName), field));

        // 类 添加属性
        clazz.addField(field);
    }

    /**
     * 拼接 get/set 方法
     */
    public static String upperFirstLatter(String letter) {
        return letter.substring(0, 1).toUpperCase() + letter.substring(1);
    }

    /**
     * 需要动态生成类的参数
     */
    @Data
    public static class BeanDto {
        /**
         * 属性名
         */
        private String property;
        /**
         * 注解名
         */
        private String annotationName;
    }
}

(2)、测试类


@RestController
@Slf4j
@RequestMapping("/test")
public class TestController {

    /**
     * 导出实体
     */
    @Data
    public static class ImportExcelDto {
        /**
         * 注解名
         */
        private String[] annotationName;
        /**
         * 属性
         */
        private String[] property;
        /**
         * 文件
         */
        private MultipartFile file;
    }

    /**
     * 导入实体
     */
    @Data
    public static class ExportExcelDto {
        /**
         * 注解名
         */
        private String[] annotationName;
        /**
         * 属性
         */
        private String[] property;
    }

    /**
     * 导出
     * 参数示意:
     * {
     *     "property":["code","name","phone"],
     *     "annotationName":["编码","名字","电话"]
     * }
     */
    @PostMapping("/exportExcel")
    public void exportExcel(@RequestBody ExportExcelDto dto, HttpServletResponse response) throws Exception {
        try {
            exportExcelService(dto, response);
        } catch (Exception e) {
            log.error("exportExcel 报错", e);
        }
    }

    /**
     * 导入
     * 参数示意:
     
     // 表头字段
     String[] annotationName = new String[]{"名字", "电话"};
     // 属性字段
     String[] property = new String[]{"name", "phone"};
     
     */
    @PostMapping("/importExcel")
    public Results importExcel(ImportExcelDto dto) throws Exception {
        try {
            importExcelService(dto);
            return Results.success();
        } catch (Exception e) {
            log.error("importExcel 报错", e);
            return Results.failed(e.getMessage());
        }
    }

    public void exportExcelService(ExportExcelDto dto, HttpServletResponse response) throws Exception {
        String[] name = dto.getAnnotationName();
        String[] property = dto.getProperty();
        // 模拟数据
        String datas = "[\n" +
                "    {\n" +
                "        \"code\":\"111\",\n" +
                "        \"name\":\"222\",\n" +
                "        \"phone\":\"333\"\n" +
                "    },\n" +
                "    {\n" +
                "        \"code\":\"444\",\n" +
                "        \"name\":\"555\",\n" +
                "        \"phone\":\"666\"\n" +
                "    },\n" +
                "    {\n" +
                "        \"code\":\"777\",\n" +
                "        \"name\":\"888\",\n" +
                "        \"phone\":\"999\"\n" +
                "    }\n" +
                "]";

        if (name.length != property.length) {
            throw new Exception("入参错误");
        }
        // 遍历成我们需要的 值
        List<CreateClassUtils.BeanDto> beanDtos = new ArrayList<>();
        for (int i = 0; i < name.length; i++) {
            CreateClassUtils.BeanDto beanDto = new CreateClassUtils.BeanDto();
            beanDto.setAnnotationName(name[i]);
            beanDto.setProperty(property[i]);
            beanDtos.add(beanDto);
        }

        JSONArray data = JSONArray.parseArray(datas);

        // 动态生成excel
        Class<?> salaryArchivesClass = CreateClassUtils.generatePrototypeClass(beanDtos);

        //列名
        List<ExcelExportEntity> headList = new ArrayList<>();

        // 遍历表头
        for (CreateClassUtils.BeanDto beanDto : beanDtos) {
            String annotationName = beanDto.getAnnotationName();
            String key = beanDto.getProperty();
            // 这个 annotationName 就是excel的表头 key与导出的属性值对应
            // 也可以使用 salaryArchivesClass 中 注解 Excel 的 name 值
            ExcelExportEntity exportEntity = new ExcelExportEntity(annotationName, key);
            // 反射获取属性(字段)
            Field field = salaryArchivesClass.getDeclaredField(key);
            // 获取动态生成的 Excel 注解的 width 值
            if (field.isAnnotationPresent(Excel.class)) {
                Excel annotation = field.getAnnotation(Excel.class);
                double width = annotation.width();
                exportEntity.setWidth(width);
            }

            headList.add(exportEntity);
        }
        // 导出
        EasyPoiUtil.exportExcel(null, "666", "666.xlsx", headList, data, response);
    }

    /**
     * 导入服务
     */
    public void importExcelService(ImportExcelDto dto) throws Exception {
        String[] annotationName = dto.getAnnotationName();
        String[] property = dto.getProperty();
        if (annotationName.length != property.length) {
            throw new Exception("入参错误");
        }
        List<CreateClassUtils.BeanDto> beanDtos = new ArrayList<>();
        // 把参数往 塞入 BeanDto
        for (int i = 0; i < annotationName.length; i++) {
            CreateClassUtils.BeanDto beanDto = new CreateClassUtils.BeanDto();
            beanDto.setAnnotationName(annotationName[i]);
            beanDto.setProperty(property[i]);
            beanDtos.add(beanDto);
        }
        // 创建类
        Class<?> salaryArchivesClass = CreateClassUtils.generatePrototypeClass(beanDtos);
        // 新建实例
        Object o = salaryArchivesClass.newInstance();
        // 获取 CreateBeanDto 中的 get 方法
        Method get = salaryArchivesClass.getDeclaredMethod("get" + CreateClassUtils.upperFirstLatter(property[0].toLowerCase()));
        Object invoke = get.invoke(o);
        log.info("Object = {}", o);
        log.info("get = {}", invoke);
        List<?> objects = EasyPoiUtil.importExcel(dto.getFile(), 0, 1, salaryArchivesClass);
        String jsonString = JSON.toJSONString(objects);
        log.info("jsonString = {}", jsonString);
    }
}

(3)、easyPOI工具类

/**
 * @author ZHAOPINGAN
 * @Title: EasyPoiUtil
 * @ProjectName
 * @Description:
 */
@Slf4j
public class EasyPoiUtil {

    /**
     * Excel大数据导入
     *
     * @param file       文件信息
     * @param titleRows 标题占几行,从哪行开始读取 ,默认 0
     * @param headerRows header占几行 ,默认 1
     * @param pojoClass     对象Class
     */
    public static <T> List<T> importBigExcel(MultipartFile file, Integer titleRows, Integer headerRows, Class<T> pojoClass) throws Exception {
        if (file == null) {
            throw new Exception("未找到上传的文件!");
        }
        ImportParams params = new ImportParams();
        params.setTitleRows(titleRows);
        params.setHeadRows(headerRows);
        params.setNeedVerify(true); // 开启校验规则
        List<T> list = new ArrayList<>();

        try {
            log.info("正在读取文件: " + file.getOriginalFilename() + ",开始导入数据。");
            ExcelImportUtil.importExcelBySax(file.getInputStream(), pojoClass, params, new IReadHandler<T>() {
                @Override
                public void handler(T o) {
                    list.add(o);
                }

                @Override
                public void doAfterAll() {

                }
            });
        } catch (Exception e) {
            log.error("导入错误",e);
        } finally {
            file.getInputStream().close();
        }

        return list;
    }

    /**
     * Excel 普通根据本地路径导入
     * @param filePath 本地文件路径
     * @param titleRows 标题占几行,从哪行开始读取 ,默认 0
     * @param headerRows header占几行 ,默认 1
     * @param pojoClass 对象Class
     */
    public static <T> List<T> importExcel(String filePath, Integer titleRows, Integer headerRows, Class<T> pojoClass) {
        if (StringUtils.isBlank(filePath)) {
            return null;
        }
        ImportParams params = new ImportParams();
        params.setTitleRows(titleRows);
        params.setHeadRows(headerRows);
        List<T> list = null;
        try {
            list = ExcelImportUtil.importExcel(new File(filePath), pojoClass, params);
        } catch (NoSuchElementException e) {
            throw new ExcelImportException("模板不能为空");
        } catch (Exception e) {
            e.printStackTrace();
            throw new ExcelImportException(e.getMessage());
        }
        return list;
    }

    /**
     * Excel 普通导入
     * @param file  文件信息
     * @param titleRows 标题占几行,从哪行开始读取 ,默认 0
     * @param headerRows header占几行 ,默认 1
     * @param pojoClass 对象Class
     */
    public static <T> List<T> importExcel(MultipartFile file, Integer titleRows, Integer headerRows, Class<T> pojoClass) {
        if (file == null) {
            return null;
        }
        ImportParams params = new ImportParams();
        params.setTitleRows(titleRows);//标题占几行,从哪行开始读取
        params.setHeadRows(headerRows);//header占几行
        params.setSheetNum(1);
        List<T> list = null;
        try {
            list = ExcelImportUtil.importExcel(file.getInputStream(), pojoClass, params);
        } catch (NoSuchElementException e) {
            throw new ExcelImportException("excel文件不能为空");
        } catch (Exception e) {
            throw new ExcelImportException(e.getMessage());
        }
        return list;
    }


    /**
     * 导出excel
     *
     * @param list           数据list
     * @param title          标题
     * @param sheetName      sheet名称
     * @param pojoClass      实体的class
     * @param fileName       文件名称
     * @param isCreateHeader 是否创建头
     * @param response       响应
     */
    public static void exportExcel(List<?> list, String title, String sheetName, Class<?> pojoClass, String fileName, boolean isCreateHeader, HttpServletResponse response) {
        ExportParams exportParams = new ExportParams(title, sheetName);
        exportParams.setCreateHeadRows(isCreateHeader);
        defaultExport(list, pojoClass, fileName, response, exportParams);

    }

    /**
     * 导出excel
     *
     * @param list      数据list
     * @param title     标题
     * @param sheetName sheet名称
     * @param pojoClass 实体class
     * @param fileName  文件名
     * @param response  响应
     */
    public static void exportExcel(List<?> list, String title, String sheetName, Class<?> pojoClass, String fileName, HttpServletResponse response) {
        defaultExport(list, pojoClass, fileName, response, new ExportParams(title, sheetName));
    }

    /**
     * 导出excel2
     */
    public static void exportExcel(String title, String sheetName, String fileName, List<ExcelExportEntity> entityList, List<?> list, HttpServletResponse response) {
        defaultExport(list, entityList, fileName, response, new ExportParams(title, sheetName));
    }

    /**
     * 导出excel
     *
     * @param list     多个Map key title 对应表格Title key entity 对应表格对应实体 key data
     * @param fileName 标题
     * @param response 响应
     */
    public static void exportExcel(List<Map<String, Object>> list, String fileName, HttpServletResponse response) {
        defaultExport(list, fileName, response);
    }


    /**
     * 返回字节码
     *
     * @param list      数据list
     * @param title     标题
     * @param sheetName sheet名称
     * @param pojoClass 实体class
     */
    public static ByteArrayOutputStream exportExcelBackByte(List<?> list, String title, String sheetName, Class<?> pojoClass) throws IOException {
        Workbook workbook = ExcelExportUtil.exportExcel(new ExportParams(title, sheetName), pojoClass, list);
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

        workbook.write(byteArrayOutputStream);
        return byteArrayOutputStream;
    }

    /**
     * Excel大数据导出
     *
     * @param list          数据List
     * @param pojoClass     对象Class
     */
    public static void exportBigExcelByte(Collection<?> list, Class<?> pojoClass, String fileName, HttpServletResponse response) throws Exception {
        Workbook workbook;

        // 设置导出单sheet页最大一百万行数据
        ExportParams exportParams = new ExportParams();
        exportParams.setMaxNum(1000000);
        exportParams.setType(ExcelType.XSSF);

        workbook = ExcelExportUtil.exportBigExcel(exportParams, pojoClass, (queryParams, num) -> {
            // 只导出一次,第二次返回null终止循环
            if (((int) queryParams) == num) {
                return null;
            }
            log.info("正在进行大数据量导出,条数: " + list.size());
            return Arrays.asList(list.toArray());
        }, 2);

        OutputStream outputStream = null;
        try {
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/vnd.ms-excel");
            response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
                    "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8")+ ";" + "filename*=utf-8''" + URLEncoder.encode(fileName, "UTF-8"));
            outputStream = response.getOutputStream();
            workbook.write(outputStream);//保存Excel文件
            if (outputStream != null) {
                outputStream.close();//关闭文件流
            }
        } catch (Exception e) {
            log.info("execel流输出时错误,错误详情:{}", e.getMessage());
            throw new Exception("execel流输出时错误");
        }
    }


    /**
     * 导出多个excel
     *
     * @param workbooks 多个excel文件 通过ExcelExportUtil.exportExcel往workbooks内放入excel
     * @param fileNames 文件名 每个excel文件的名称顺序必须一致且名称请务必保证不重复
     * @param fileName  压缩包文件名
     * @param response  标题
     */
    public static void exportExcels(List<Workbook> workbooks, List<String> fileNames, String fileName, HttpServletResponse response) {
        try {
//            response.setHeader("Content-Disposition",
//                "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8") + ".zip");
            response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
                    "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8")+ ";"
                            + "filename*=utf-8''" + URLEncoder.encode(fileName, "UTF-8") + ".zip");
            OutputStream toClient = new BufferedOutputStream(response.getOutputStream());
            ZipOutputStream zipOut = new ZipOutputStream(toClient);
            for (int i = 0; i < workbooks.size(); i++) {
                ZipEntry entry = new ZipEntry(fileNames.get(i) + ".xls");
                zipOut.putNextEntry(entry);
                workbooks.get(i).write(zipOut);
            }
            zipOut.flush();
            zipOut.close();
        } catch (IOException e) {
            throw new ExcelExportException(e.getMessage());
        }
    }

    /**
     * 导出excel
     *
     * @param list      数据list
     * @param pojoClass 实体class
     * @param fileName  文件名
     * @param response  响应
     */
    private static void defaultExport(List<?> list, Class<?> pojoClass, String fileName, HttpServletResponse response, ExportParams exportParams) {
        Workbook workbook = ExcelExportUtil.exportExcel(exportParams, pojoClass, list);
        downLoadExcel(fileName, response, workbook);
    }

    /**
     * 导出excel2
     */
    private static void defaultExport(List<?> list, List<ExcelExportEntity> entityList, String fileName, HttpServletResponse response, ExportParams exportParams) {
        Workbook workbook = ExcelExportUtil.exportExcel(exportParams, entityList, list);
        downLoadExcel(fileName, response, workbook);
    }

    private static void downLoadExcel(String fileName, HttpServletResponse response, Workbook workbook) {
        try {
            response.setCharacterEncoding("UTF-8");
            response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
                "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8")+ ";" + "filename*=utf-8''" + URLEncoder.encode(fileName, "UTF-8"));
            response.setContentType("application/vnd.ms-excel;charset=UTF-8");
            response.setHeader(HttpHeaders.PRAGMA, "no-cache");//关闭缓存
            response.setHeader(HttpHeaders.CACHE_CONTROL, "no-cache");//关闭缓存
            workbook.write(response.getOutputStream());
            workbook.close();
        } catch (IOException e) {
            throw new ExcelImportException(e.getMessage());
        }
    }

    private static void defaultExport(List<Map<String, Object>> list, String fileName, HttpServletResponse response) {
        Workbook workbook = ExcelExportUtil.exportExcel(list, ExcelType.HSSF);
        downLoadExcel(fileName, response, workbook);
    }
}

(4)、json返回类(可以改成lombok的,我懒得改了)

/**
 * 用于返回
 * @param <T>
 */
@ApiModel("统一返回类")
public class Results<T> {
    public static final String ERROR = "500";
    public static final String SUCCESS = "200";

    /**
     * 返回码
     */
    @ApiModelProperty("返回码,正确码为:200")
    private String resCode ;

    /**
     * 返回消息
     */
    @ApiModelProperty("返回消息")
    private String msg ;

    /**
     * 返回实体
     */
    @ApiModelProperty("返回实体")
    private T obj;

    public static <T> Results<T> success(){
        return success(SUCCESS,"成功",null);
    }

    public static <T> Results<T> success(String msg){
        return success(SUCCESS,msg,null);
    }

    public static <T> Results<T> success(T obj){
        return success(SUCCESS,"成功",obj);
    }

    public static <T> Results<T> success(String msg,T obj){
        return success(SUCCESS,msg,obj);
    }

    public static <T> Results<T> success(String resCode,String msg,T obj){
        Results<T> result = new Results<T>();
        result.setResCode(resCode);
        result.setMsg(msg);
        result.setObj(obj);
        return result;
    }

    public static <T> Results<T> failed() {
        return failed(ERROR,"失败",null);
    }

    public static <T> Results<T> failed(String msg) {
        return failed(ERROR,msg,null);
    }

    public static <T> Results<T> failed(String msg,T obj) {
        return failed(ERROR,msg,obj);
    }

    public static <T> Results<T> failed(String resCode,String msg) {
        return failed(resCode,msg,null);
    }

    public static <T> Results<T> failed(Integer resCode,String msg) {
        return failed(String.valueOf(resCode),msg);
    }

    public static <T> Results<T> failed(String resCode,String msg,T obj) {
        Results<T> result = new Results<T>();
        result.setResCode(resCode);
        result.setMsg(msg);
        result.setObj(obj);
        return result;
    }

    public String getResCode() {
        return resCode;
    }

    public void setResCode(String resCode) {
        this.resCode = resCode;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public T getObj() {
        return obj;
    }

    public void setObj(T obj) {
        this.obj = obj;
    }

    @Override
    public String toString() {
        return "Results{" +
                "resCode='" + resCode + '\'' +
                ", msg='" + msg + '\'' +
                ", obj=" + obj +
                '}';
    }
}

4、测试

(1)、导出

在这里插入图片描述

在这里插入图片描述

(2)、导入(使用导出的666.xlsx,就只用名字和电话两个字段来展示)

在这里插入图片描述
在这里插入图片描述

我的其他关于excel的文章(里面的工具类都有些许不同,我根据实际情况都有所删减和添加)

JAVA操作Excel(POI、easyPOI、easyExcel)
https://blog.csdn.net/qq_57581439/article/details/131206886

Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐