首页 > 技术文章 > EasyPoi 使用工具

yiMro 2022-03-08 15:48 原文

Easypoi 为谁而开发

  • 不太熟悉poi的
  • 不想写太多重复太多的
  • 只是简单的导入导出的
  • 喜欢使用模板的

都可以使用easypoi

Easypoi的目标是什么
Easypoi的目标不是替代poi,而是让一个不懂导入导出的快速使用poi完成Excel和word的各种操作,而不是看很多api才可以完成这样工作

独特的功能

  • 基于注解的导入导出,修改注解就可以修改Excel
  • 支持常用的样式自定义
  • 基于map可以灵活定义的表头字段
  • 支持一堆多的导出,导入
  • 支持模板的导出,一些常见的标签,自定义标签
  • 支持HTML/Excel转换,如果模板还不能满足用户的变态需求,请用这个功能
  • 支持word的导出,支持图片,Excel

开始使用:starter依赖util都可以直接放到common里面

<dependency>
    <groupId>cn.afterturn</groupId>
    <artifactId>easypoi-spring-boot-starter</artifactId>
    <version>4.0.0</version>
</dependency>

使用下面几个方法,大致康康就行了

/**
     * Excel 导入 数据源本地文件,不返回校验结果 导入 字 段类型 Integer,Long,Double,Date,String,Boolean
     * 
     * @param file
     * @param pojoClass
     * @param params
     * @return
     */
    public static <T> List<T> importExcel(File file, Class<?> pojoClass, ImportParams params) {
        FileInputStream in = null;
        try {
            in = new FileInputStream(file);
            return new ExcelImportService().importExcelByIs(in, pojoClass, params, false).getList();
        } catch (ExcelImportException e) {
            throw new ExcelImportException(e.getType(), e);
        } catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
            throw new ExcelImportException(e.getMessage(), e);
        } finally {
            IOUtils.closeQuietly(in);
        }
    }

    /**
     * Excel 导入 数据源IO流,不返回校验结果 导入 字段类型 Integer,Long,Double,Date,String,Boolean
     * 
     * @param inputstream
     * @param pojoClass
     * @param params
     * @return
     * @throws Exception
     */
    public static <T> List<T> importExcel(InputStream inputstream, Class<?> pojoClass,
                                          ImportParams params) throws Exception {
        return new ExcelImportService().importExcelByIs(inputstream, pojoClass, params, false).getList();
    }
View Code
/**
     * @param entity    表格标题属性
     * @param pojoClass Excel对象Class
     * @param dataSet   Excel对象数据List
     */
    public static Workbook exportExcel(ExportParams entity, Class<?> pojoClass,
                                       Collection<?> dataSet) {
        Workbook workbook = getWorkbook(entity.getType(), dataSet.size());
        new ExcelExportService().createSheet(workbook, entity, pojoClass, dataSet);
        return workbook;
    }
View Code

对象不固定的导入导出
导出使用自己构造ExcelExportEntity,导入的话对象改成map就可以了

/**
     * 根据Map创建对应的Excel
     *
     * @param entity     表格标题属性
     * @param entityList Map对象列表
     * @param dataSet    Excel对象数据List
     */
    public static Workbook exportExcel(ExportParams entity, List<ExcelExportEntity> entityList,
                                       Collection<?> dataSet) {
        Workbook workbook = getWorkbook(entity.getType(), dataSet.size());
        ;
        new ExcelExportService().createSheetForMap(workbook, entity, entityList, dataSet);
        return workbook;
    }
View Code

导入还是上面方法,entity选择map就可以了,展示下map转Key,方便下一步处理

 public class MapImportHandler extends ExcelDataHandlerDefaultImpl<Map<String, Object>> {

        @Override
        public void setMapValue(Map<String, Object> map, String originKey, Object value) {
            if (value instanceof Double) {
                map.put(getRealKey(originKey), PoiPublicUtil.doubleToString((Double) value));
            } else {
                map.put(getRealKey(originKey), value != null ? value.toString() : null);
            }
        }

        private String getRealKey(String originKey) {
            if (originKey.equals("姓名")) {
                return "name";
            }
            if (originKey.equals("身份证")) {
                return "sfz";
            }
            if (originKey.equals("班主任寄语")) {
                return "content";
            }
            if (originKey.equals("学科")) {
                return "subjectName";
            }
            if (originKey.equals("学科成绩")) {
                return "subjectScore";
            }
            if (originKey.equals("子学科")) {
                return "childSubjectName";
            }
            if (originKey.equals("子学科成绩")) {
                return "childSubjectScore";
            }
            return originKey;
        }
    }
View Code

量稍微有点大10W内
依然建议上面的写法,但不建议使用校验,可以加入下面参数加快时间,但是因为是一次返回可能量还是有点大,不支持一对多

/**
     * 是否并行计算
     */
    private boolean             concurrentTask = false;

写出基本上不用改,10W以上会自己使用SXSSFWorkbook,不支持图片了等其他一些特性,速度回快一些可以使用USE_SXSSF_LIMIT这个参数调教

更大量的读取,或者需要校验等时间稍长,或者频繁导入容易内存溢出
那就使用sax吧,这个更加高效,低内存但是支持较少

/**
     * Excel 通过SAX解析方法,适合大数据导入,不支持图片
     * 导入 数据源本地文件,不返回校验结果 导入 字 段类型 Integer,Long,Double,Date,String,Boolean
     * 
     * @param inputstream
     * @param pojoClass
     * @param params
     * @param handler
     */
    public static void importExcelBySax(InputStream inputstream, Class<?> pojoClass,
                                        ImportParams params, IReadHandler handler) {
        new SaxReadExcel().readExcel(inputstream, pojoClass, params, handler);
    }
View Code

不在处理完在回调,而是每处理一行就回答一次的方式方便大家更快的同步入库或者处理异常

@Test
    public void test() {
        try {
            ImportParams params = new ImportParams();
            params.setTitleRows(1);
            params.setStartSheetIndex(1);
            long start = new Date().getTime();
            ExcelImportUtil.importExcelBySax(
                    new FileInputStream(
                            new File(FileUtilTest.getWebRootPath("import/saxtest.xlsx"))),
                    Map.class, params, 
                    // 这个是匿名类,不建议这样,最好还是自己实现一个类方便开发管理,泛型参数
                    new IReadHandler<Map>() {
                        @Override
                        public void handler(Map o) {
                            System.out.println(o);
                        }
                        // 所有数据执行完毕会调用这个类
                        @Override
                        public void doAfterAll() {
                            System.out.println("全部执行完毕了--------------------------------");
                        }
                    });
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }
View Code

针对大数据量推荐方式是如下,每隔一段处理一次,cpu和内存都可以稳定在一定的范围内

new IReadHandler<Map>() {

    private List list = new ArrayList();
    @Override
    public void handler(Map o) {
        list.add(o);
        if(list.size == 1000){
            handlerData();
        }
    }
    // 所有数据执行完毕会调用这个类
    @Override
    public void doAfterAll() {
        handlerData();
    }

    private void handlerData(){
        //sava 或者其他操作
        save(list);
        list.clear();
    }
});
View Code

更加大量的数据,百万级以上
使用csv把,这个靠谱的多,百万级读取数据也相当的快,内存也不高

/**
     * Csv 导入流适合大数据导入
     * 导入 数据源IO流,不返回校验结果 导入 字段类型 Integer,Long,Double,Date,String,Boolean
     *
     * @param inputstream
     * @param pojoClass
     * @param params
     * @return
     */
    public static <T> List<T> importCsv(InputStream inputstream, Class<?> pojoClass,
                                        CsvImportParams params) {
        return new CsvImportService().readExcel(inputstream, pojoClass, params, null);
    }

    /**
     * Csv 导入流适合大数据导入
     * 导入 数据源IO流,不返回校验结果 导入 字段类型 Integer,Long,Double,Date,String,Boolean
     *
     * @param inputstream
     * @param pojoClass
     * @param params
     * @return
     */
    public static void importCsv(InputStream inputstream, Class<?> pojoClass,
                                        CsvImportParams params, IReadHandler readHandler) {
        new CsvImportService().readExcel(inputstream, pojoClass, params, readHandler);
    }

    /**
     * @param params    表格标题属性
     * @param pojoClass Excel对象Class
     * @param dataSet   Excel对象数据List
     */
    public static void exportCsv(CsvExportParams params, Class<?> pojoClass,
                                 Collection<?> dataSet, OutputStream outputStream) {
        new CsvExportService().createCsv(outputStream, params, pojoClass, dataSet);
    }

    /**
     * 根据Map创建对应的Excel
     *
     * @param params     表格标题属性
     * @param entityList Map对象列表
     * @param dataSet    Excel对象数据List
     */
    public static void exportCsv(CsvExportParams params, List<ExcelExportEntity> entityList,
                                 Collection<?> dataSet, OutputStream outputStream) {
        new CsvExportService().createCsvOfList(outputStream, params, entityList, dataSet);
    }
View Code

这边作者自己写了一个util,虽然只支持注解Excel导出,不过也能解决大部分需要啦!

如大数量导出推荐使用csv,或者多个excel导出

@Slf4j
@Component
public class ExcelUtil {

    private static ClassExcelVerifyHandler verifyHandler;
    private static Environment env;

    @Autowired
    public void setClassExcelVerifyHandler(ClassExcelVerifyHandler verifyHandler) {
        ExcelUtil.verifyHandler = verifyHandler;
    }
    @Autowired
    public void setEnvironment(Environment env) {
        ExcelUtil.env = env;
    }


    /**
     * 导出 xlsx
     * @param list
     * @param title
     * @param pojoClass
     * @param response
     * @param <T>
     */
    public static <T> void exportExcel(List<T> list, String title, Class<T> pojoClass, HttpServletResponse response) {
        try {
            exportExcel(list, title, title, title, pojoClass, true, response);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * excel 导出  xlsx
     *
     * @param list           数据
     * @param title          标题
     * @param sheetName      sheet名称
     * @param pojoClass      pojo类型
     * @param fileName       文件名称
     * @param isCreateHeader 是否创建表头
     * @param response
     * @throws IOException
     */
    public static <T> void exportExcel(List<T> list, String title, String sheetName, String fileName, Class<T> pojoClass, boolean isCreateHeader, HttpServletResponse response) throws IOException{
        ExportParams exportParams = new ExportParams(title, sheetName, ExcelType.XSSF);
//        exportParams.setAddIndex(true);
//        exportParams.setIndexName("序号");
        exportParams.setCreateHeadRows(isCreateHeader);
        Workbook workbook = ExcelExportUtil.exportExcel(exportParams, pojoClass, list);
        downLoadExcel(fileName + ExcelTypeEnum.XLSX.getValue(), response, workbook);
    }

    /**
     * excel 导出  xls
     *
     * @param list           数据
     * @param title          标题
     * @param sheetName      sheet名称
     * @param pojoClass      pojo类型
     * @param fileName       文件名称
     * @param isCreateHeader 是否创建表头
     * @param response
     * @throws IOException
     */
    public static <T> void exportExcel0(List<T> list, String title, String sheetName, String fileName, Class<T> pojoClass, boolean isCreateHeader, HttpServletResponse response) throws IOException{
        ExportParams exportParams = new ExportParams(title, sheetName, ExcelType.HSSF);
        exportParams.setCreateHeadRows(isCreateHeader);
        Workbook workbook = ExcelExportUtil.exportExcel(exportParams, pojoClass, list);
        downLoadExcel(fileName + ExcelTypeEnum.XLS.getValue(), response, workbook);
    }

    /**
     * 导入 不带图片
     *
     * @param file  文件
     * @param pojoClass  实体类
     * @param lastOfInvalidRow   不读取几
     * @return
     * @throws Exception
     */
    public static <T> List<T> importExcel(MultipartFile file, Class<T> pojoClass, Integer lastOfInvalidRow) {
        if (file.isEmpty()) {
            log.warn("文件不存在!");
            return null;
        }
        //获取excel临时存放路径
        String path = env.getProperty("file-save-path");
        path = StringUtils.isNoneBlank(path)?path+"/excel":"";
        try {
            return defaultImport(file, 0, 1, false, lastOfInvalidRow, pojoClass, path);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return Collections.emptyList();
    }

    /**
     * excel 导入 带图片
     *
     * @param file      excel文件
     * @param pojoClass pojo类型
     * @throws IOException
     */
    public static  <T> List<T> importExcel(MultipartFile file, Class<T> pojoClass, String path) throws Exception{
        if (file.isEmpty()) {
            log.warn("文件不存在!");
            return null;
        }
        return defaultImport(file, 0, 1, false, 0, pojoClass, path);
    }


    /**
     * excel 导入 保存地址
     *
     * @param file       上传的文件
     * @param titleRows  标题行
     * @param headerRows 表头行
     * @param needVerfiy 是否检验excel内容
     * @param lastOfInvalidRow 不读取几行
     * @param pojoClass  pojo类型
     * @return
     * @throws IOException
     */
    public static <T> List<T> defaultImport(MultipartFile file, Integer titleRows, Integer headerRows, boolean needVerfiy, Integer lastOfInvalidRow, Class<T> pojoClass, String path) throws Exception{
        ImportParams params = new ImportParams();
        params.setTitleRows(titleRows);
        params.setHeadRows(headerRows);
        params.setSaveUrl(path);
        params.setNeedSave(true);
        params.setNeedVerify(needVerfiy);
        params.setLastOfInvalidRow(lastOfInvalidRow);
        params.setVerifyHandler(verifyHandler);
        return ExcelImportUtil.importExcel(file.getInputStream(), pojoClass, params);
    }

    /**
     * 下载
     *
     * @param fileName 文件名称
     * @param response
     * @param workbook excel数据
     */
    private static void downLoadExcel(String fileName, HttpServletResponse response, Workbook workbook) throws IOException {
        try {
            response.setCharacterEncoding("UTF-8");
            response.setHeader("content-Type", "application/vnd.ms-excel");
            response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));
            workbook.write(response.getOutputStream());
        } catch (Exception e) {
            throw new IOException(e.getMessage());
        }
    }

    /**
     * Excel 类型枚举
     */
    enum ExcelTypeEnum {
        XLS(".xls"), XLSX(".xlsx");
        private String value;

        ExcelTypeEnum(String value) {
            this.value = value;
        }

        public String getValue() {
            return value;
        }

        public void setValue(String value) {
            this.value = value;
        }
    }

}
View Code

easypoi会出现文件缓存的问题,官方有提供两种解决方案

@Component
public class ExcelListener implements ApplicationListener<ApplicationReadyEvent> {


    @Override
    public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
        POICacheManager.setFileLoader(new FileLoaderImpl());
    }

    /**
     * 一次线程有效
     * @param fileLoder
     */
//    public static void setFileLoderOnce(IFileLoader fileLoder) {
//        if (fileLoder != null) {
//            LOCAL_FILELOADER.set(fileLoder);
//        }
//    }
}
View Code
文件加载类,根据路径加载指定文件
@Slf4j
public class FileLoaderImpl implements IFileLoader {
    @Override
    public byte[] getFile(String url) {
        InputStream fileis = null;
        ByteArrayOutputStream baos = null;
        try {
            //判断是否是网络地址
            if (url.startsWith("http")) {
                URL urlObj = new URL(url);
                URLConnection urlConnection = urlObj.openConnection();
                urlConnection.setConnectTimeout(3000);
                urlConnection.setReadTimeout(6000);
                urlConnection.setDoInput(true);
                fileis = urlConnection.getInputStream();
            } else {
                //先用绝对路径查询,再查询相对路径
                try {
                    fileis = new FileInputStream(url);
                } catch (FileNotFoundException e) {
                    //获取项目文件
                    fileis = FileLoaderImpl.class.getClassLoader().getResourceAsStream(url);
                    if (fileis == null) {
                        fileis = FileLoaderImpl.class.getResourceAsStream(url);
                    }
                }
            }
            baos = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int len;
            while ((len = fileis.read(buffer)) > -1) {
                baos.write(buffer, 0, len);
            }
            baos.flush();
            return baos.toByteArray();
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        } finally {
            IOUtils.closeQuietly(fileis);
            IOUtils.closeQuietly(baos);
        }
        log.error(fileis + "这个路径文件没有找到,请查询");
        return null;
    }
}
View Code

easypoi自定义校验IExcelVerifyHandler,如果不需要可以去掉

@Component
public class ClassExcelVerifyHandler implements IExcelVerifyHandler<Object> {

    @SneakyThrows
    @Override
    public ExcelVerifyHandlerResult verifyHandler(Object obj) {
        ExcelVerifyHandlerResult result=new ExcelVerifyHandlerResult(true);
        result.setSuccess(!checkObjAllFieldsIsNull(obj));
        return result;
    }

    /**
     * 判断对象中属性值是否全为空
     *
     * @param object
     * @return
     */
    public static boolean checkObjAllFieldsIsNull(Object object) {
        if (null == object) {
            return true;
        }
        try {
            for (Field f : object.getClass().getDeclaredFields()) {
                f.setAccessible(true);
                System.out.print(f.getName() + ":");
                System.out.println(f.get(object));
                if (!StringUtils.equals("serialVersionUID",f.getName())&&f.get(object) != null && StringUtils.isNotBlank(f.get(object).toString())) {
                    return false;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return true;
    }

}
View Code

文件相关的工具类

public class FileUtil {

    /**
     * 判断是否为Linux
     * @return
     */
    public static Boolean bitLinux() {
        String os = System.getProperty("os.name").toLowerCase();
        return os.startsWith("win") ? false : true;
    }

    /**
     * 获取项目绝对路径
     * @return
     */
    public static String getPath() {
        return System.getProperty("user.dir");
    }

    /**
     * 生成文件
     * @param file
     * @param fileName
     * @param propertyPath
     */
    public static void createFile(MultipartFile file, String fileName, String propertyPath) {
        File dest = new File(propertyPath + File.separator + fileName);
        if (!dest.getParentFile().exists()) {
            dest.getParentFile().mkdirs();
        }
        try {
            file.transferTo(dest);
        } catch (IOException e) {
            log.error("文件生成失败!", e);
        }
    }


    /**
     * 判断文件是否为图片类型
     * @param s
     * @return
     */
    public static Boolean isFileSuffix(String s) {
        Boolean flag = false;
        switch (s){
            case ".jpg": flag = true;
                break;
            case ".png": flag = true;
                break;
            default:break;
        }
        return flag;
    }


    /**
     * 删除上传的文件
     * @param fileName
     * @return
     */
    public static boolean delFile(String fileName) {
        File file = new File(getPath() + File.separator + fileName);
        // 如果文件路径所对应的文件存在,并且是一个文件,则直接删除
        if (file.exists() && file.isFile()) {
            if (file.delete()) {
                log.warn("删除单个文件" + fileName + "成功!");
                return true;
            } else {
                log.error("删除单个文件" + fileName + "失败!");
                return false;
            }
        } else {
            log.error("删除单个文件失败:" + fileName + "不存在!");
            return false;
        }
    }
}
View Code

使用起来也是非常简单

###@Excel 将这个注解放到OrdUser类里面需要导出的字段上面

这个是必须使用的注解,如果需求简单只使用这一个注解也是可以的,涵盖了常用的Excel需求,需要大家熟悉这个功能,主要分为基础,图片处理,时间处理,合并处理几块,name_id是上面讲的id用法,这里就不累言了,我学废了 ( \> - </ )

更多操作查看Easypoi官方文档http://doc.wupaas.com/docs/easypoi/easypoi-1c0u6ksp2r091

属性类型默认值功能
name String null 列名,支持name_id
needMerge boolean fasle 是否需要纵向合并单元格(用于含有list中,单个的单元格,合并list创建的多个row)
orderNum String “0” 列的排序,支持name_id
replace String[] {} 值得替换 导出是{a_id,b_id} 导入反过来
savePath String “upload” 导入文件保存路径,如果是图片可以填写,默认是upload/className/ IconEntity这个类对应的就是upload/Icon/
type int 1 导出类型 1 是文本 2 是图片,3 是函数,10 是数字 默认是文本
width double 10 列宽
height double 10 列高,后期打算统一使用@ExcelTarget的height,这个会被废弃,注意
isStatistics boolean fasle 自动统计数据,在追加一行统计,把所有数据都和输出[这个处理会吞没异常,请注意这一点]
isHyperlink boolean false 超链接,如果是需要实现接口返回对象
isImportField boolean true 校验字段,看看这个字段是不是导入的Excel中有,如果没有说明是错误的Excel,读取失败,支持name_id
exportFormat String “” 导出的时间格式,以这个是否为空来判断是否需要格式化日期
importFormat String “” 导入的时间格式,以这个是否为空来判断是否需要格式化日期
format String “” 时间格式,相当于同时设置了exportFormat 和 importFormat
databaseFormat String “yyyyMMddHHmmss” 导出时间设置,如果字段是Date类型则不需要设置 数据库如果是string 类型,这个需要设置这个数据库格式,用以转换时间格式输出
numFormat String “” 数字格式化,参数是Pattern,使用的对象是DecimalFormat
imageType int 1 导出类型 1 从file读取 2 是从数据库中读取 默认是文件 同样导入也是一样的
suffix String “” 文字后缀,如% 90 变成90%
isWrap boolean true 是否换行 即支持\n
mergeRely int[] {} 合并单元格依赖关系,比如第二列合并是基于第一列 则{0}就可以了
mergeVertical boolean fasle 纵向合并内容相同的单元格
fixedIndex int -1 对应excel的列,忽略名字
isColumnHidden boolean false 导出隐藏列

推荐阅读