SpringBoot导入导出你会用吗?(EasyPoi)

Easypoi包的使用

前言

最近老板让我实现一个导入导出的功能,这个项目因为是一个很老的维护项目,关于excel的导入导出用的还是 jexcelapi这个包,我虽然已经封装了工具类了,但是我还是感觉很麻烦~ ,尤其是标题,不停的add(菜鸡的我)

最近,看了看一些技术博客,正好看到了关于文件导入导出好用的包,EasyPoiEasyExcel。恩,看这名字,咱来看看用这两个有多Easy~~

首先说一下这两个包的区别:

  • EasyPoi 占用内存大,性能不够好(看和谁比)。这个在学习得时候看到官网上有很多人评论说了对应得问题,但是把,我还是觉得比 jexcelapi好用~~
  • EasyExcel 是一款阿里开源,大厂保证,应该性能没得说把(狗头保命)官话:具有处理快速、占用内存小、使用方便的特点,在Github上已有22k+Star,可非常流行。

这篇主要来讲解我学习的关于 Easypoi 包中的知识,EasyExcel 下篇博客安排上,我先去玩玩~~

好了,回归正题,咱们来看看 Easypoi 是个啥子,怎么玩吧~

官话: easypoi功能如同名字easy,主打的功能就是容易,让一个没见接触过poi的人员 就可以方便的写出Excel导出,Excel模板导出,Excel导入,Word模板导出,通过简单的注解和模板 语言(熟悉的表达式语法),完成以前复杂的写法。

人话:会加@注解就能使用这个包就会导入导出了,是不是特别easy。

特点:

  • 设计精巧,来加注解,调用一些工具类,就可以使用,使用简单
  • 提供了很多接口,扩展很简单
  • 搞了很多的默认值,这样的话,兜底不怕了~
  • 支撑SpringMVC、SpringBoot

功能

Excel自适应xls和xlsx两种格式,word只支持docx模式

  • Excel导入

    • 注解导入

    • Map导入

    • 大数据量导入sax模式

    • 导入文件保存

    • 文件校验

    • 字段校验

  • Excel导出

    • 注解导出
    • 模板导出
    • html导出
  • Excel转html

  • Word导出

  • PDF导出

官方列举这么多功能,到底有没有这么厉害的,咱们来一点点玩玩~

使用

像这种第三方的包,使用就分三步走:导包、官网举例参考使用、验证。具体的原理有兴趣的自己去探寻探寻,有空我学习学习后再来叨叨~

1、导包

在项目的pom.xml 文件夹中导入包。

这里有两种导入方式,分为SpringBoot项目与SpringMvc项目,如果你简单的使用excel导入导出功能的话,这个包够用了。

下面设计到的源码也是针对 4.3.0 这个版本来的,其他版本可以看对应的源码来~

// SpringBoot项目中导入,这个包里面包含了下面的几个包
<dependency>
     	<groupId>cn.afterturn</groupId>
     	<artifactId>easypoi-spring-boot-starter</artifactId>
     	<version>4.3.0</version>
</dependency>

// SpringMvc项目中导入
<dependency>
		<groupId>cn.afterturn</groupId>
		<artifactId>easypoi-base</artifactId> //导入导出的工具包,可以完成Excel导出,导入,Word的导出,Excel的导出功能
		<version>4.3.0</version>
</dependency>
<dependency>
		<groupId>cn.afterturn</groupId>
		<artifactId>easypoi-web</artifactId> //耦合了spring-mvc 基于AbstractView,极大的简化spring-mvc下的导出功能
		<version>4.3.0</version>
</dependency>
<dependency>
		<groupId>cn.afterturn</groupId>
		<artifactId>easypoi-annotation</artifactId> //基础注解包,作用与实体对象上,拆分后方便maven多工程的依赖管理
		<version>4.3.0</version>
</dependency>
		

如果你是需要校验、PDF导出、Word导出、 sax ,自己根据需求加吧~

			<!-- sax 读取时候用到的 -->
			<dependency>
				<groupId>xerces</groupId>
				<artifactId>xercesImpl</artifactId>
				<version>${xerces.version}</version>
				<optional>true</optional>
			</dependency>
			<dependency>
				<groupId>org.apache.poi</groupId>
				<artifactId>poi-scratchpad</artifactId>
				<version>${poi.version}</version>
				<optional>true</optional>
			</dependency>
			
			<!-- Word 需要使用 -->
            <dependency>
                <groupId>org.apache.poi</groupId>
                <artifactId>ooxml-schemas</artifactId>
                <version>1.3</version>
                <optional>true</optional>
            </dependency>
			
			<!-- 校验,下面两个实现 -->
			<dependency>
				<groupId>org.hibernate</groupId>
				<artifactId>hibernate-validator</artifactId>
				<version>5.1.3.Final</version>
				<optional>true</optional>
			</dependency>
			
			<dependency>
				<groupId>org.apache.bval</groupId>
				<artifactId>org.apache.bval.bundle</artifactId>
				<version>1.1.0</version>
			</dependency>
			
			<!-- PDF -->
			<dependency>
				<groupId>com.itextpdf</groupId>
				<artifactId>itextpdf</artifactId>
				<version>5.5.6</version>
				<optional>true</optional>
			</dependency>

			<dependency>
				<groupId>com.itextpdf</groupId>
				<artifactId>itext-asian</artifactId>
				<version>5.2.0</version>
				<optional>true</optional>
			</dependency>

2、注解说明

这个包的注解其实挺少的,有六个,@CelleStyle没啥东西,我主要介绍其他五个把:

注解使用位置描述
Excel只适用在 属性FIELD 上Excel中的一列
ExcelCollection只适用在 属性FIELD 上表示一个集合,主要针对一对多的导出
ExcelEntity只适用在 属性FIELD 上表示一个继续深入导出的实体
说明这个Feild是一个实体类,里面的每个属性都要导出
ExcelIgnore只适用在 属性FIELD 上导出的时候跳过这个Fileld
ExcelTarget适用在 类 接口 枚举 上放在最外层
描述这个对象的id,以便支持一个对象可以针对不同导出做出不同处理
2.1 注解基础知识的介绍
// 表明注解可以使用在哪里
// 这里表示注解可以使用在 方法 属性  注解  构造方法  参数  type 
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
//描述保留注解的各种策略,它们与元注解(@Retention)一起指定注释要保留多长时间
@Retention(RetentionPolicy.RUNTIME)
//表明这个注解是由 javadoc记录的
@Documented
public @interface MyOwnAnnotation {
    
}
2.2 @Excel

@Excel注解一般我们标记在FILED上,表示这个FILED是Excel中的一列,一般都靠它实现,对应字段及其解释如下(看了一下,可实现功能还是挺多的)

注解源码:

package cn.afterturn.easypoi.excel.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface Excel {
    // 列名,支持name_id,必须给定
    String name();
    
    // 导出时间设置,如果字段是Date类型则不需要设置 
    // 数据库如果是string 类型,这个需要设置这个数据库格式,用以转换时间格式输出
    // 默认yyyyMMddHHmmss
    String databaseFormat() default "yyyyMMddHHmmss";

    // 导出的时间格式
    // 如果这个值为空,就不需要格式化日期
    // 如果不为空,则需要格式化日期
    String exportFormat() default "";
    
    // 导入的时间格式
    // 如果这个值为空,就不需要格式化日期
    // 如果不为空,则需要格式化日期
    String importFormat() default "";

    //时间格式,相当于同时设置了exportFormat 和 importFormat
    String format() default "";
    
    // 列的排序
    String orderNum() default "0";

    // 值的替换 导出是{a_1,b_2} 导入反过来
    // 就是当值为1的时候,替换为1,值为2的时候,替换为b
    String[] replace() default {};
    
    // 数字格式化,参数是Pattern,使用的对象是DecimalFormat
    String numFormat() default "";
    
    
    // 文字后缀,如% 90 变成90%,在导出该值的后面加上suffix指定的值
    String suffix() default "";
    
    // 单元格宽度
    double width() default 10.0D;
    
    
    // 时区,GMT zone的Zone ID,具体取值可查看java.util.ZoneId里面的可选值
    String timezone() default "";

    
    /** @deprecated */
    @Deprecated
    double height() default 10.0D;

    // 导出类型 
    // 1 从file读取 2 是从数据库中读取 
    // 默认是 1 从文件读取 同样导入也是一样的
    int imageType() default 1;

    
    // 是否换行 即支持\n
    // 默认支撑
    boolean isWrap() default true;

    // 是否需要纵向合并单元格(用于含有list中,单个的单元格,合并list创建的多个row)
    // 值为合并的单元格的row数组
    int[] mergeRely() default {};
    
    // 是否需要纵向合并单元格(用于含有list中,单个的单元格,合并list创建的多个row)
    // 如果需要合并,需要指定mergeRely
    boolean needMerge() default false;

    // 是否纵向合并内容相同的单元格
    // 默认false 不合并
    boolean mergeVertical() default false;
    
    // 组名称,这个对应 @ExcelEntity 的name,用于导出的不同处理
    String groupName() default "";

    // 字典名称
    String dict() default "";
    
 	// 是否插入下拉
    boolean addressList() default false;
	
    // 保存图片的地址
    String savePath() default "/excel/upload/img";
    
	// 单元格的类型,对应的type,默认为1
    // STRING_TYPE = 1; string类型
    // IMAGE_TYPE = 2; 图片类型
    // FUNCTION_TYPE = 3; 函数
    // DATE_TYPE = 4; 日期
    // DOUBLE_TYPE = 10; double精度
    int type() default 1;

    
    // 是否是统计
    boolean isStatistics() default false;

    // 这个是不是超链接,如果是需要实现接口返回对象
    boolean isHyperlink() default false;
    
	// 导入校验字段,默认为false,就行无需校验
    String isImportField() default "false";

    // 固定的列
    int fixedIndex() default -1;

    // 是否隐藏列
    boolean isColumnHidden() default false;

    // 枚举导出属性字段
    String enumExportField() default "";
    
	// 枚举导入静态方法
    String enumImportMethod() default "";
    
	// 脱敏规则
    String desensitizationRule() default "";
}

2.2 @ExcelCollection

@ExcelCollection注解一般我们标记在FILED上,表示一个继续深入导出的实体,说明这个Feild是一个实体类,里面的每个属性都要针对@Excel标记的进行导出

注解源码:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface ExcelCollection {
    // 列名称,必不可少
    String name();
    // name如果不指定,支持name_id (如果标注ExcelTarget,这个id可以为其value)
    String id() default "";
    // 排序号
    String orderNum() default "0";
	// 默认该字段是ArrayList,可指定该字段Collection类型
    Class<?> type() default ArrayList.class;
}
2.3 @ExcelEntity

@ExcelEntity注解一般我们标记在FILED上

注解源码:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface ExcelEntity {
    //列的名称,支持name_id(如果标注ExcelTarget,这个id可以为其value)
    String id() default "";
	// 列名称,如果show()为true,一定要指定
    String name() default "";
	// 是否要展示这个filed,如果为true的话,name一定要指定
    boolean show() default false;
}
2.4 @ExcelIgnore

@ExcelIgnore注解一般我们标记在FILED上,用于是否忽略,忽略的属性标注 @ExcelIgnore 即可

注解源码:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface ExcelIgnore {
}
2.5 @ExcelTarget

@ExcelTarget注解一般我们标记在类 接口 枚举 上,放在最外层,描述这个对象的id,以便支持一个对象可以针对不同导出做出不同处理

注解源码:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface ExcelTarget {
    // 标注是哪个导出,@Excel的name可以为该值
    String value();
}

3、工具类说明

3.1 ExportParams

导出参数,主要来定义 Excel的一些信息,在ExcelExportUtil 里面进行文件导出的,都是使用这个参数来作为导出参数。

该类涉及的字段与含义如下,仅仅提供了几个构造函数:

  • 设置 表格第一行title 、 表格第二行子title、sheet名称
  • 设置 表格第一行title 、 sheet名称
  • 设置 表格第一行title 、 sheet名称、导出文件的格式(默认xls)
/**
 * Excel 导出参数
 */
@Data
public class ExportParams extends ExcelBaseParams {
    /**
     * 表格第一行title名称
     */
    private String  title;

    /**
     * 表格第一行title高度
     */
    private short   titleHeight = 10;

    /**
     * 表格第一行子title名称
     */
    private String   secondTitle;

    /**
     * 表格第一行子title高度
     */
    private short    secondTitleHeight = 8;
    /**
     * sheetName
     */
    private String    sheetName;
    /**
     * 过滤的属性
     */
    private String[]  exclusions;
    /**
     * 是否添加index
     */
    private boolean   addIndex;
    /**
     * index名称
     */
    private String    indexName         = "序号";
    /**
     * 冰冻列
     */
    private int       freezeCol;
    /**
     * 表头颜色 &  标题颜色
     */
    private short     color             = HSSFColor.HSSFColorPredefined.WHITE.getIndex();
    /**
     * 第二行标题颜色
     * 属性说明行的颜色 例如:HSSFColor.SKY_BLUE.index 默认
     */
    private short     headerColor       = HSSFColor.HSSFColorPredefined.SKY_BLUE.getIndex();
    /**
     * Excel 导出版本
     */
    private ExcelType type              = ExcelType.HSSF;
    /**
     * Excel 导出style
     */
    private Class<?>  style             = ExcelExportStylerDefaultImpl.class;

    /**
     * 表头高度
     */
    private double  headerHeight     = 9D;
    /**
     * 是否创建表头
     */
    private boolean isCreateHeadRows = true;
    /**
     * 是否动态获取数据
     */
    private boolean isDynamicData    = false;
    /**
     * 是否追加图形
     */
    private boolean isAppendGraph    = true;
    /**
     * 是否固定表头
     */
    private boolean isFixedTitle     = true;
    /**
     * 单sheet最大值
     * 03版本默认6W行,07默认100W
     */
    private int     maxNum           = 0;

    /**
     * 导出时在excel中每个列的高度 单位为字符,一个汉字=2个字符
     * 全局设置,优先使用
     */
    private short height = 0;

    /**
     * 只读
     */
    private boolean readonly = false;
    /**
     * 列宽自适应,如果没有设置width 也自适应宽度
     */
    private boolean autoSize = false;
	// 构造函数
    public ExportParams() {

    }
    public ExportParams(String title, String sheetName) {
        this.title = title;
        this.sheetName = sheetName;
    }
    public ExportParams(String title, String sheetName, ExcelType type) {
        this.title = title;
        this.sheetName = sheetName;
        this.type = type;
    }
    public ExportParams(String title, String secondTitle, String sheetName) {
        this.title = title;
        this.secondTitle = secondTitle;
        this.sheetName = sheetName;
    }
    public short getSecondTitleHeight() {
        return (short) (secondTitleHeight * 50);
    }
    public short getTitleHeight() {
        return (short) (titleHeight * 50);
    }
    public short getHeight() {
        return height == -1 ? -1 : (short) (height * 50);
    }
    public short getHeaderHeight() {
        return (short) (titleHeight * 50);
    }

}
3.2 ExcelExportUtil

导出工具类。构造导出需要的参数ExportParams,用该类中的静态方法进行导出指定excel文件

源码如下,对应的方法与参数含义如下:(具体可以看看下面的使用范例)

/**
 * excel 导出工具类
 */
public final class ExcelExportUtil {

    public static       int    USE_SXSSF_LIMIT = 1000000;
    public static final String SHEET_NAME      = "sheetName";

    private ExcelExportUtil() {
    }
    /**
     * 大数据量导出
     *
     * @param entity    表格标题属性
     * @param pojoClass Excel对象Class
     */
    public static IWriter<Workbook> exportBigExcel(ExportParams entity, Class<?> pojoClass) {
        ExcelBatchExportService batchServer = new ExcelBatchExportService();
        batchServer.init(entity, pojoClass);
        return batchServer;
    }

    /**
     * 大数据量导出
     *
     * @param entity 表格标题属性
     * @param excelParams cell映射类参数list
     * @return
     */
    public static IWriter<Workbook> exportBigExcel(ExportParams entity, List<ExcelExportEntity> excelParams) {
        ExcelBatchExportService batchServer = new ExcelBatchExportService();
        batchServer.init(entity, excelParams);
        return batchServer;
    }

    /**
     * 大数据量导出
     *
     * @param entity      表格标题属性
     * @param pojoClass   Excel对象Class
     * @param server      查询数据的接口
     * @param queryParams 查询数据的参数
     */
    public static Workbook exportBigExcel(ExportParams entity, Class<?> pojoClass,
                                          IExcelExportServer server, Object queryParams) {
        ExcelBatchExportService batchServer = new ExcelBatchExportService();
        batchServer.init(entity, pojoClass);
        return batchServer.exportBigExcel(server, queryParams);
    }

    /**
     * 大数据量导出
     * @param entity 表格标题属性
     * @param excelParams 表格标题属性
     * @param server      查询数据的接口
     * @param queryParams 查询数据的参数
     * @return
     */
    public static Workbook exportBigExcel(ExportParams entity, List<ExcelExportEntity> excelParams,
                                          IExcelExportServer server, Object queryParams) {
        ExcelBatchExportService batchServer = new ExcelBatchExportService();
        batchServer.init(entity, excelParams);
        return batchServer.exportBigExcel(server, queryParams);
    }


    /**
     * @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;
    }

    private static Workbook getWorkbook(ExcelType type, int size) {
        if (ExcelType.HSSF.equals(type)) {
            return new HSSFWorkbook();
        } else {
            return new XSSFWorkbook();
        }
    }

    /**
     * 根据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;
    }

    /**
     * 根据Map创建对应的Excel(一个excel 创建多个sheet)
     *
     * @param list 多个Map key title 对应表格Title key entity 对应表格对应实体 key data
     *             Collection 数据
     */
    public static Workbook exportExcel(List<Map<String, Object>> list, ExcelType type) {
        Workbook workbook = getWorkbook(type, 0);
        for (Map<String, Object> map : list) {
            ExcelExportService service = new ExcelExportService();
            ExportParams params = (ExportParams) map.get("title");
            params.setType(type);
            service.createSheet(workbook,params,
                    (Class<?>) map.get("entity"), (Collection<?>) map.get("data"));
        }
        return workbook;
    }

    /**
     * 导出文件通过模板解析,不推荐这个了,推荐全部通过模板来执行处理
     * @param params    导出参数类
     * @param pojoClass 对应实体
     * @param dataSet   实体集合
     * @param map       模板集合
     * @return
     */
    @Deprecated
    public static Workbook exportExcel(TemplateExportParams params, Class<?> pojoClass,
                                       Collection<?> dataSet, Map<String, Object> map) {
        return new ExcelExportOfTemplateUtil().createExcelByTemplate(params, pojoClass, dataSet,
                map);
    }

    /**
     * 导出文件通过模板解析只有模板,没有集合
     *
     * @param params 导出参数类
     * @param map    模板集合
     * @return
     */
    public static Workbook exportExcel(TemplateExportParams params, Map<String, Object> map) {
        return new ExcelExportOfTemplateUtil().createExcelByTemplate(params, null, null, map);
    }

    /**
     * 导出文件通过模板解析只有模板,没有集合
     * 每个sheet对应一个map,导出到处,key是sheet的NUM
     *
     * @param params 导出参数类
     * @param map    模板集合
     * @return
     */
    public static Workbook exportExcel(Map<Integer, Map<String, Object>> map,
                                       TemplateExportParams params) {
        return new ExcelExportOfTemplateUtil().createExcelByTemplate(params, map);
    }

    /**
     * 导出文件通过模板解析只有模板,没有集合
     * 每个sheet对应一个list,按照数量进行导出排序,key是sheet的NUM
     *
     * @param params 导出参数类
     * @param map    模板集合
     * @return
     */
    public static Workbook exportExcelClone(Map<Integer, List<Map<String, Object>>> map,
                                            TemplateExportParams params) {
        return new ExcelExportOfTemplateUtil().createExcelCloneByTemplate(params, map);
    }

}
3.4 ImportParams

导入的参数设置,主要根据你文件里面的内容的格式来进行设置

/**
 * 导入参数设置
 */
@Data
public class ImportParams extends ExcelBaseParams {

    public static final String SAVE_URL = "/excel/upload/excelUpload";

    /**
     * 表格标题行数,默认0
     */
    private int                 titleRows        = 0;
    /**
     * 表头行数,默认1
     */
    private int                 headRows         = 1;
    /**
     * 字段真正值和列标题之间的距离 默认0
     */
    private int                 startRows        = 0;

    /**
     * 主键设置,如何这个cell没有值,就跳过 或者认为这个是list的下面的值
     * 大家不理解,去掉这个
     */
    private Integer             keyIndex         = null;
    /**
     * 开始读取的sheet位置,默认为0
     */
    private int                 startSheetIndex  = 0;
    /**
     * 上传表格需要读取的sheet 数量,默认为1
     */
    private int                 sheetNum         = 1;
    /**
     * 是否需要保存上传的Excel,默认为false
     */
    private boolean             needSave         = false;
    /**
     * 校验组
     */
    private Class[]             verifyGroup = null;
    /**
     * 是否需要校验上传的Excel,默认为false
     */
    private boolean             needVerify = false;
    /**
     * 返回文件是否分割,默认是分割
     */
    private boolean             verifyFileSplit = true;
    /**
     * 校验处理接口
     */
    private IExcelVerifyHandler verifyHandler;
    /**
     * 保存上传的Excel目录,默认是 如 TestEntity这个类保存路径就是
     * upload/excelUpload/Test/yyyyMMddHHmss_***** 保存名称上传时间_五位随机数
     */
    private String              saveUrl          = SAVE_URL;
    /**
     * 最后的无效行数
     */
    private int                 lastOfInvalidRow = 0;
    /**
     * 手动控制读取的行数
     */
    private int                 readRows = 0;
    /**
     * 导入时校验数据模板,是不是正确的Excel
     */
    private String[]            importFields;
    /**
     * 导入时校验excel的标题列顺序。依赖于importFields的配置顺序
    */
    private boolean             needCheckOrder = false;
    /**
     * Key-Value 读取标记,以这个为Key,后面一个Cell 为Value,多个改为ArrayList
     */
    private String              keyMark = ":";
    /**
     * 按照Key-Value 规则读取全局扫描Excel,但是跳过List读取范围提升性能
     * 仅仅支持titleRows + headRows + startRows 以及 lastOfInvalidRow
     */
    private boolean             readSingleCell = false;
    /**
     * 是否并行计算
     */
    private boolean             concurrentTask = false;
    /**
     * 最小截取大小
     */
    private Integer             critical = 1000;
}
3.3 ExcelImportUtil

导入工具。传入之地那个的文件、导出的entity的类,对应的import的参数来进行文件的导入。

/**
 * Excel 导入工具
 */
@SuppressWarnings({ "unchecked" })
public class ExcelImportUtil {

    private ExcelImportUtil() {
    }

    private static final Logger LOGGER = LoggerFactory.getLogger(ExcelImportUtil.class);

    /**
     * Excel 导入 数据源本地文件,不返回校验结果 导入 字 段类型 Integer,Long,Double,Date,String,Boolean
     * 
     * @param file 导入的文件
     * @param pojoClass 需要导入数据成的entity类
     * @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 需要导入数据成的entity类
     * @param params 导入的参数
     */
    public static <T> List<T> importExcel(InputStream inputstream, Class<?> pojoClass,
                                          ImportParams params) throws Exception {
        return new ExcelImportService().importExcelByIs(inputstream, pojoClass, params, false).getList();
    }

    /**
     * Excel 导入 数据源IO流 字段类型 Integer,Long,Double,Date,String,Boolean
     * 支持校验,支持Key-Value
     * 
     * @param inputstream 文件流
     * @param pojoClass 需要导入数据成的entity类
     * @param params 导入的参数
     */
    public static <T> ExcelImportResult<T> importExcelMore(InputStream inputstream,
                                                             Class<?> pojoClass,
                                                             ImportParams params) throws Exception {
        return new ExcelImportService().importExcelByIs(inputstream, pojoClass, params, true);
    }

    /**
     * Excel 导入 数据源本地文件 字段类型 Integer,Long,Double,Date,String,Boolean
     * 支持校验,支持Key-Value
      * @param inputstream 文件流
     * @param pojoClass 需要导入数据成的entity类
     * @param params 导入的参数
     */
    public static <T> ExcelImportResult<T> importExcelMore(File file, Class<?> pojoClass,
                                                             ImportParams params) {
        FileInputStream in = null;
        try {
            in = new FileInputStream(file);
            return new ExcelImportService().importExcelByIs(in, pojoClass, params, true);
        } 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 通过SAX解析方法,适合大数据导入,不支持图片
     * 导入 数据源本地文件,不返回校验结果 导入字段类型 Integer,Long,Double,Date,String,Boolean
     * 
      * @param inputstream 文件流
     * @param pojoClass 需要导入数据成的entity类
     * @param params 导入的参数
     * @param handler 接口自定义处理类,用来解析对象
     */
    public static void importExcelBySax(InputStream inputstream, Class<?> pojoClass,
                                        ImportParams params, IReadHandler handler) {
        new SaxReadExcel().readExcel(inputstream, pojoClass, params, handler);
    }

}

4 简单使用

我们主要对上面的注解的属性来试试,给一个很直观的感受,到底怎么用

这里也仅仅测试一些我觉得可能经常用到的属性,复杂的一些属性,需要自己遇到之后再来记录在这里啦(后续更新)~

4.1 @Entity 使用一:利用简单属性导出excel

主要对以下属性来使用一下:

  • namedatabaseFormatformat(exportFormat、importFormat)orderNumreplacenumFormatsuffixwidth

再来回顾一下这几个属性干嘛的:

    // 列名,支持name_id,必须给定
    String name();
    
    // 导出时间设置,如果字段是Date类型则不需要设置 
    // 数据库如果是string 类型,这个需要设置这个数据库格式,用以转换时间格式输出
    // 默认yyyyMMddHHmmss
    String databaseFormat() default "yyyyMMddHHmmss";

    // 导出的时间格式
    // 如果这个值为空,就不需要格式化日期
    // 如果不为空,则需要格式化日期
    String exportFormat() default "";
    
    // 导入的时间格式
    // 如果这个值为空,就不需要格式化日期
    // 如果不为空,则需要格式化日期
    String importFormat() default "";

    //时间格式,相当于同时设置了exportFormat 和 importFormat
    String format() default "";
    
    // 列的排序
    String orderNum() default "0";

    // 值的替换 导出是{a_1,b_2} 导入反过来
    // 就是当值为1的时候,替换为1,值为2的时候,替换为b
    String[] replace() default {};
    
    // 数字格式化,参数是Pattern,使用的对象是DecimalFormat
    String numFormat() default "";
    
    // 文字后缀,如% 90 变成90%,在导出该值的后面加上suffix指定的值
    String suffix() default "";

 	// 单元格宽度,默认是10
    double width() default 10.0D;
    

(1)定义entity类

主要分别对一些属性来定义了一些字段,在输出文件的时候,可以来对应看属性是否生效,从而来确定属性的使用

package com.study.pojo;

import cn.afterturn.easypoi.excel.annotation.Excel;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

import java.util.Date;

@Getter
@Setter
@Builder
public class EasyPoiStudy {
    /**
     * id:没有加excel,不导出
     */
    private Integer id;
    /**
     * 学生姓名
     * 测试 name 与 width
     */
    @Excel(name = "学生姓名", width = 30)
    private String name;
    /**
     * 学生性别,该字段的取值为 1 / 2
     * 那么我们文件输出的时候,就对应男与女
     * suffix为后面加上一个生,则值就必须为男生/女生
     * 测试 replace 、 suffix
     */
    @Excel(name = "学生性别", replace = {"男_1", "女_2"}, suffix = "生")
    private int sex;

    /**
     * 出生日期
     * 测试 format 、 orderNum(其他默认都是0,这个为2肯定在所有列的后面)
     */
    @Excel(name = "出生日期", orderNum = "2", format = "yyyy-MM-dd", width = 20)
    private Date birthday;

    /**
     * 学生的身高
     * 测试 numFormat
     */
    @Excel(name = "学生的身高", numFormat = "#.0")
    private Double height;

}

(2)执行类

我们定义了两个entity,数据分为为

  • EasyPoiStudy(id=1, name=zhangsan, sex=1, birthday=Tue Jan 11 17:42:17 CST 2022, height=175.0)
  • EasyPoiStudy(id=2, name=lisi, sex=2, birthday=Tue Jan 11 17:42:17 CST 2022, height=173.45)
public class EasyPoiStudyMain {

    public static void main(String[] args) {
        File file = new File("D:\\test.xls");
        if (file.exists()) {
            file.delete();
        }
        try {
            FileOutputStream outputStream = new FileOutputStream(file);
            List<EasyPoiStudy> list = new ArrayList<>();
            EasyPoiStudy study_1 = EasyPoiStudy.builder().id(1).name("zhangsan").sex(1).birthday(new Date()).height(175.0).build();
            EasyPoiStudy study_2 = EasyPoiStudy.builder().id(2).name("lisi").sex(2).birthday(new Date()).height(173.45).build();
            list.add(study_1);
            list.add(study_2);
            ExportParams exportParams = new ExportParams("我的大标题", "我的子标题", "我的sheet");
            Workbook workbook = ExcelExportUtil.exportExcel(exportParams,
                    EasyPoiStudy.class, list);
            workbook.write(outputStream);
            workbook.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

(3)验证

根据entity定义的属性,我们需要验证的点:

  • id是否没导出**(没加Excel的是不是就不导出)**

  • 列名称**(name属性是否生效)**

  • 列的宽度(默认宽度是10,name字段与birthday指定了宽度30与20,width属性是否生效

  • 性别是否是男生/女生**(replace 与 suffix 属性是否生效)**

  • 出生日期的格式**(format属性是否生效)**

  • 学生的身高的格式**(numFormat属性是否生效)**

excel截图:

请添加图片描述

4.2 使用二:利用简单属性导入excel

我们先改一下4.1导出的文件内容,改过后的文件内容截图如下:

请添加图片描述

(1)Entity:还是使用上面的那个,但是但是,(非常要注意,我们一定要有无参构造函数,这边类前面改一点)

@Getter
@Setter
//@Builder
@ToString
@AllArgsConstructor
@NoArgsConstructor
// 或者直接标注@data
//@Data
public class EasyPoiStudy {
}

(2)执行类

package com.study.util;

import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.ExcelImportUtil;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.ImportParams;
import cn.afterturn.easypoi.excel.entity.result.ExcelImportResult;
import com.study.pojo.EasyPoiStudy;
import org.apache.poi.ss.usermodel.Workbook;

import javax.xml.bind.SchemaOutputResolver;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;

public class EasyPoiStudyMain {

    public static void main(String[] args) {
		
        File file = new File("D:\\test.xls");
        ImportParams importParams = new ImportParams();
        // 标题是从文件的哪一行开始的,是第三行,这里是从0开始数的,所以是2
        importParams.setTitleRows(2);
        List<EasyPoiStudy> list = ExcelImportUtil.importExcel(file, EasyPoiStudy.class, importParams);
        System.out.println("list size : " +  list.size());
        list.forEach(System.out::println);
    }

}

(3)看看结果

  • EasyPoiStudy(name=zhangsan, sex=1, birthday=Tue Jan 11 12:00:00 CST 2022, height=175.02)
  • EasyPoiStudy(name=lisi, sex=2, birthday=Tue Jan 11 00:00:00 CST 2022, height=173.4)

在验证的时候遇到了两个问题:

  • 为什么我们定义的replace与suffix生效了,但是format 与 numformat没有生效
  • 为什么在类无无参构造函数的时候执行会报错

说明:我在玩这个的时候,遇到了上面两个问题,我特别好奇,所以我下载了源码,调试了一下,才知道了为什么

1)针对第一个问题的源码

原因:我们获取单元格的时候,需要判断文件中单元格的格式,所以文件中的单元格格式一定要确认一下~

  • 关于numformat的使用,必须单元格的格式是 CellType.NUMERIC

  • 关于format的使用,必须单元格的格式不可以是 CellType.NUMERIC

if(cell instanceof Cell){
    // 获取单元格的值
    result = getCellValue(classFullName, (Cell) cell, entity);
}else{
     result = cell;
}
if (entity != null) {
     result = handlerSuffix(entity.getSuffix(), result);
     result = replaceValue(entity.getReplace(), result);
     result = replaceValue(entity.getReplace(), result);
     if (dictHandler != null && StringUtils.isNoneBlank(entity.getDict())) {
         result = dictHandler.toValue(entity.getDict(), object, entity.getName(), result);
     }
}

/**
     * 获取单元格内的值
     *
     * @param cell
     * @param entity
     * @return
     */
    private Object getCellValue(String classFullName, Cell cell, ExcelImportEntity entity) {
        if (cell == null) {
            return "";
        }
        Object result = null;
        // 判断字段的类型
        if ("class java.util.Date".equals(classFullName)
                || "class java.sql.Date".equals(classFullName)
                || ("class java.sql.Time").equals(classFullName)
                || ("class java.time.Instant").equals(classFullName)
                || ("class java.time.LocalDate").equals(classFullName)
                || ("class java.time.LocalDateTime").equals(classFullName)
                || ("class java.sql.Timestamp").equals(classFullName)) {
            //FIX: 单元格yyyyMMdd数字时候使用 cell.getDateCellValue() 解析出的日期错误
            if (CellType.NUMERIC == cell.getCellType() && DateUtil.isCellDateFormatted(cell)) {
                result = DateUtil.getJavaDate(cell.getNumericCellValue());
            } else {
                String val = "";
                try {
                    val = cell.getStringCellValue();
                } catch (Exception e) {
                    return null;
                }
                // 这里进行numberformat的处理
                result = getDateData(entity, val);
                if (result == null) {
                    return null;
                }
            }
            if (("class java.time.Instant").equals(classFullName)) {
                result = ((Date) result).toInstant();
            } else if (("class java.time.LocalDate").equals(classFullName)) {
                result = ((Date) result).toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
            } else if (("class java.time.LocalDateTime").equals(classFullName)) {
                result = ((Date) result).toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
            } else if (("class java.time.OffsetDateTime").equals(classFullName)) {
                result = ((Date) result).toInstant().atZone(ZoneId.systemDefault()).toOffsetDateTime();
            }  else if (("class java.time.ZonedDateTime").equals(classFullName)) {
                result = ((Date) result).toInstant().atZone(ZoneId.systemDefault());
            }  else if (("class java.sql.Date").equals(classFullName)) {
                result = new java.sql.Date(((Date) result).getTime());
            } else if (("class java.sql.Time").equals(classFullName)) {
                result = new Time(((Date) result).getTime());
            } else if (("class java.sql.Timestamp").equals(classFullName)) {
                result = new Timestamp(((Date) result).getTime());
            }
        } else {
            switch (cell.getCellType()) {
                    // 是文本的时候
                case STRING:
                    result = cell.getRichStringCellValue() == null ? ""
                            : cell.getRichStringCellValue().getString();
                    break;
                   // 是NUMERIC的时候
                case NUMERIC:
                    if (DateUtil.isCellDateFormatted(cell)) {
                        if ("class java.lang.String".equals(classFullName)) {
                            result = formateDate(entity, cell.getDateCellValue());
                        }
                    } else {
                        // 如果是数值的时候,就根据entity来进行解析
                        result = readNumericCell(cell);
                    }
                    break;
                case BOOLEAN:
                    result = Boolean.toString(cell.getBooleanCellValue());
                    break;
                case BLANK:
                    break;
                case ERROR:
                    break;
                case FORMULA:
                    try {
                        result = readNumericCell(cell);
                    } catch (Exception e1) {
                        try {
                            result = cell.getRichStringCellValue() == null ? ""
                                    : cell.getRichStringCellValue().getString();
                        } catch (Exception e2) {
                            throw new RuntimeException("获取公式类型的单元格失败", e2);
                        }
                    }
                    break;
                default:
                    break;
            }
        }
        return result;
    }

2)针对第二个问题的源码如下:

在给对象赋值的时候,需要创建一个对象,一定需要无参构造函数 ,没有就会报错,那可能有人问了,我即使没定义,如果没有定义构造函数,不是会自带一个无参构造函数嘛,怎么会报错呢?

原因:因为在4.1的时候我们定义的entity,我使用了lombok的@Builder注解,这个注解会生成一个全参构造函数,那么就不会自动生成一个无参构造函数了,就需要自己手动加上无参构造函数 ,如果不知道lombok的使用,可以看我另外一篇博客: lombok 的使用

/**
 * 彻底创建一个对象
 *
 * @param clazz
 * @return
 */
public static Object createObject(Class<?> clazz, String targetId) {
    Object obj = null;
    if (clazz.equals(Map.class)) {
            return new LinkedHashMap<String, Object>();
     }
     // clazz.newInstance(); 创建实例,这个必须要有无参构造函数
        obj = clazz.newInstance();
        Field[] fields = getClassFields(clazz);
        for (Field field : fields) {
            if (isNotUserExcelUserThis(null, field, targetId)) {
                continue;
            }
            if (isCollection(field.getType())) {
                ExcelCollection collection = field.getAnnotation(ExcelCollection.class);
                PoiReflectorUtil.fromCache(clazz).setValue(obj, field.getName(),
                        collection.type().newInstance());
            } else if (!isJavaClass(field) && !field.getType().isEnum()) {
                PoiReflectorUtil.fromCache(clazz).setValue(obj, field.getName(),
                        createObject(field.getType(), targetId));
            }
        }
    return obj;
}

总结

Easypoi 总体来说我觉得看它的源码难度不是很大,可能就是它想做的事情太多了,提供的扩展字段有点多,而且很多字段的描述也不是特别清楚,我在上面的学习过程中有的东西还是去看了源码才知道是啥意思、要怎么用~

这里简单说了导入导出的简单用法

传送门

API官网(旧)

官网介绍(新)

源码地址(Git)

EasyPoi开发指南

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值