基于注解的Excel导出万能模板

前言

上次给大家分享了一个自己开发的基于注解Excel导出的小工具,不知道有没有人用,是针对复杂数据导出多个Sheet页的,那也是第一个版本。

后来小编发现了很多问题:

  1. 无法满足多复杂数据多行表头
  2. 有那种一对一的数据也没有办法显示,而且得需要去做连表查询。开发太费劲。
  3. 无法设置自己的样式或者企业规定的样式或字体。
  4. 列宽无法自适应。
  5. 遇到数据库里面 类似:type 字段的值一般都是 1,2,3,4: 1:未支付,2:支付中,3:已支付 等类似的,无法直接导出,希望能够查询列表中就是这些值,只需要配置便可以导出对应的类型的中文。
  6. 遇到时间,需要自定义时间格式化。

今天给大家推出的算是一个完整版本的基于注解导出Excel的工具,花费了小编整整一周的时间来写这个,就是为了正取能把功能写到位。

功能简介

  1. 能够针对树结构数据导出复杂表头(多少层数据都可以
  2. 针对数据对应关系分为一对一数据关系;一对多数据关系。
  3. 可选择导出多个Sheet 。(针对一对多的数据关系)。
  4. @Cell注解 增加Groups属性(导出时选择需要导出的Group,会自动分组,实现同一实体类不同的自定义列)
  5. 增加@Style注解(设置对齐方式,填充方式,前景色,边框及边框颜色)
  6. 增加@Font注解(设置字体样式,字体颜色,字体名称)
  7. @Sheet注解增加了password 属性,可以给Sheet页面设置密码。
  8. @Title注解增加heightInPoints 属性,给标题设置行高。
  9. 导出大数据量分页(适应xls格式的文件,无法导出超大数据。大概6万条数据)

功能展示

接下来就给大家展示一下插件的功能吧。

准备

pom.xml 中加入我们的依赖包。(友情提示:加入依赖包后,就不要在本地依赖POI相关的依赖了,如果存在可能会导致jar包冲突)

<dependency>
  <groupId>com.gitee.lwpwork</groupId>
  <artifactId>excel</artifactId>
  <version>0.0.2-RELEASE</version>
</dependency>

1. 一对多数据关系, 生成多个Sheet,并附加Sheet的密码。

实体类Shop配置:

@Data
@Sheet(name = "吹雪恒集团" ,password = "lwp")
@Title(value = "吹雪恒集团报表统计",heightInPoints = 30)
public class Shop implements ExcelAble {
    @Cell(value = "店名")
    private String name;
 
    @Cell(value = "收入",groups = Clerk.GroupB.class)
    private Integer income;

    //这里实体上面需要添加@Sheet注解
    private List<Clerk> clerks ;

    public Shop() {
    }

    public Shop(String name, Integer income) {
        this.name = name;
        this.income = income;
    }
}

实体类Clerk配置:

@Sheet(name = "{}的员工报表",isField = true,value = {"name"})//这是设置是否关联父节点的字段,来拼接Sheet的名字 name中{}是占位符。 isField表示是否开启 父节点字段。
@Title("员工统计报表")
public class Clerk implements ExcelAble {

    @Cell(value = "姓名")
    private String name;

    @Cell(value = "年龄")
    private String age;

    @Cell(value = "收益")
    private Integer income;

    public Clerk() {
    }

    public Clerk(String name, String age, Integer income) {
        this.name = name;
        this.age = age;
        this.income = income;
    }
}

我们的数据结构是:一个Shop->多个Clerk实体

测试代码:

public static void main(String[] args) throws Exception {
        Long start = System.currentTimeMillis();
        List<Shop> shops = init();//初始化数据列表。
        HSSFWorkbook wb = ExcelUtil.exportExcel(shops);//生成Excel的HSSFWorkbook 对象。
        ExcelUtil.createExcelFile(wb,"/Shop.xls");//生成文件
        System.out.println(System.currentTimeMillis()-start);
}

效果图:
在这里插入图片描述
在这里插入图片描述

2. 一对一数据关系,复杂表头。

Shop:

@Data
@Sheet(name = "吹雪恒集团")
@Title(value = "吹雪恒集团报表统计",heightInPoints = 30)
public class Shop implements ExcelAble {
    @Cell(value = "店名")
    private String name;
    @Cell(value = "收入")
    private Integer income;
    @Cell(value = "店长")
    private Manager manager;

    private List<Clerk> clerks ;

    public Shop() {
    }

    public Shop(String name, Integer income) {
        this.name = name;
        this.income = income;
    }
}

Manager:

@Data
public class Manager implements ExcelAble {

    @Cell(value = "店长姓名")
    private String name;

    @Cell(value = "性别")
    private String sex;

    @Cell(value = "工作经验")
    private String jobExperience;

    public Manager(String name, String sex, String jobExperience) {
        this.name = name;
        this.sex = sex;
        this.jobExperience = jobExperience;
    }

    public Manager() {
    }
}

数据结构: Manager是Shop的子集,那么我们看看导出后的效果是什么样子的。

效果图:
在这里插入图片描述

3. 一对多数据关系和一对一数据关系,复杂表头。

Shop:

@Data
@Sheet(name = "吹雪恒集团")
@Title(value = "吹雪恒集团报表统计",heightInPoints = 30)
public class Shop implements ExcelAble {
    @Cell(value = "店名")
    private String name;
    @Cell(value = "收入")
    private Integer income;
    @Cell(value = "店长")
    private Manager manager;
    @Cell(value = "员工")
    private List<Clerk> clerks ;

    public Shop() {
    }
    public Shop(String name, Integer income) {
        this.name = name;
        this.income = income;
    }
}

这次的导出增加了Clerk这张表。

Clerk:

@Data
public class Clerk implements ExcelAble {

    @Cell(value = "姓名")
    private String name;

    @Cell(value = "年龄")
    private String age;

    @Cell(value = "收益")
    private Integer income;
    public Clerk() {
    }

    public Clerk(String name, String age, Integer income) {
        this.name = name;
        this.age = age;
        this.income = income;
    }
}

效果图:
在这里插入图片描述

4. 分组表头的使用。

在导出Excel的时候,往往实体类的字段要比导出的Excel字段多,并且在不同的业务情境中,导出的列是不同的。那么我们怎么才能使用同一个实体类来实现不同的Excel导出呢 ? 接下来给大家演示一下。

Shop:

@Data
@Sheet(name = "吹雪恒集团")
@Title(value = "吹雪恒集团报表统计",heightInPoints = 30)
public class Shop implements ExcelAble {
    @Cell(value = "店名", groups = {Clerk.GroupB.class, Clerk.GroupA.class})
    private String name;
    @Cell(value = "收入",groups = {Clerk.GroupB.class, Clerk.GroupA.class})
    private Integer income;
    @Cell(value = "店长",groups = Clerk.GroupA.class)
    private Manager manager;
    @Cell(value = "员工", groups = Clerk.GroupB.class)
    private List<Clerk> clerks ;

    public Shop() {
    }

    public Shop(String name, Integer income) {
        this.name = name;
        this.income = income;
    }
}

看一下Shop的子集Manager

Manager:

@Data
public class Manager implements ExcelAble {

    @Cell(value = "店长姓名",groups = Clerk.GroupA.class)
    private String name;

    @Cell(value = "性别",groups = Clerk.GroupA.class)
    private String sex;

    @Cell(value = "工作经验",groups = Clerk.GroupA.class)
    private String jobExperience;

    public Manager(String name, String sex, String jobExperience) {
        this.name = name;
        this.sex = sex;
        this.jobExperience = jobExperience;
    }

    public Manager() {
    }
}

然后再看一下,Shop子集Clerk

Clerk:

@Data
public class Clerk implements ExcelAble {

    @Cell(value = "姓名",groups = {GroupA.class,GroupB.class})
    private String name;

    @Cell(value = "年龄" ,groups = GroupB.class)
    private String age;

    @Cell(value = "收益" ,groups = GroupA.class)
    private Integer income;

    public Clerk() {
    }

    interface GroupA{}  //A分组
    interface GroupB{}  //B分组  这个可以写在任何地方。

    public Clerk(String name, String age, Integer income) {
        this.name = name;
        this.age = age;
        this.income = income;
    }
}

我们先使用Clerk.GroupA分组测试:

 public static void main(String[] args) throws Exception {

        Long start = System.currentTimeMillis();
        List<Shop> shops = init();
        HSSFWorkbook wb = ExcelUtil.exportExcel(shops, Clerk.GroupA.class);//使用Clerk.GroupA.class 分组
        ExcelUtil.createExcelFile(wb,"/Shop.xls");
        System.out.println(System.currentTimeMillis()-start);
    }

效果图:
在这里插入图片描述

我们再看一下Clerk.GroupB分组测试:

public static void main(String[] args) throws Exception {

        Long start = System.currentTimeMillis();
        List<Shop> shops = init();
        HSSFWorkbook wb = ExcelUtil.exportExcel(shops, Clerk.GroupB.class);//,使用Clerk.GroupB.class 分组
        ExcelUtil.createExcelFile(wb,"/Shop.xls");
        System.out.println(System.currentTimeMillis()-start);
    }

效果图:

在这里插入图片描述

5. Cell注解时间格式化。

@Data
@Sheet(name = "吹雪恒集团")
@Title(value = "吹雪恒集团报表统计",heightInPoints = 30)
public class Shop implements ExcelAble {
    @Cell(value = "店名")
    private String name;
    @Cell(value = "收入")
    private Integer income;
    @Cell(value = "店长")
    private Manager manager;
    @Cell(value = "员工")
    private List<Clerk> clerks ;
    @Cell(value = "创建时间",format = "yyyy-MM-dd HH:mm:ss")
    private Date createDate;

    public Shop() {
    }

    public Shop(String name, Integer income) {
        this.name = name;
        this.income = income;
    }
}

效果图:

在这里插入图片描述

6. Cell注解类型解析。

@Data
@Sheet(name = "吹雪恒集团")
@Title(value = "吹雪恒集团报表统计",heightInPoints = 30)
public class Shop implements ExcelAble {
    @Cell(value = "店名")
    private String name;
    @Cell(value = "收入")
    private Integer income;
    @Cell(value = "店长")
    private Manager manager;
    @Cell(value = "员工")
    private List<Clerk> clerks ;
    @Cell(value = "创建时间",format = "yyyy-MM-dd HH:mm:ss")
    private Date createDate;
    @Cell(value = "店铺类型",readConverterExp = "1=超市,2=商场,3=餐饮" )
    private Integer shopType;
    public Shop() {
    }

    public Shop(String name, Integer income) {
        this.name = name;
        this.income = income;
    }
}

效果图:

在这里插入图片描述

7. 设置样式(对齐方式、边框、边框颜色、前景色、填充方式)【全局设置、局部设置】

Shop:

@Data
@Sheet(name = "吹雪恒集团")
@Title(value = "吹雪恒集团报表统计",heightInPoints = 30)
public class Shop implements ExcelAble {
    @Style(border = BorderStyles.BORDER_DASH_DOT,color = ExcelColors.AQUA)
    @Cell(value = "店名")
    private String name;
    @Style(backgroundColor = ExcelColors.YELLOW,fillPattern = FillPatternStyles.THIN_HORZ_BANDS)
    @Cell(value = "收入")
    private Integer income;
    @Cell(value = "店长")
    private Manager manager;
    @Cell(value = "员工")
    private List<Clerk> clerks ;
    @Style(valign = ValignStyles.VERTICAL_TOP,halign = HalignStyles.ALIGN_RIGHT)//垂直靠上,水平右对齐
    @Cell(value = "创建时间",format = "yyyy-MM-dd HH:mm:ss")
    private Date createDate;
    @Cell(value = "店铺类型",readConverterExp = "1=超市,2=商场,3=餐饮" )
    private Integer shopType;


    public Shop() {
    }

    public Shop(String name, Integer income) {
        this.name = name;
        this.income = income;
    }

}

Clerk: Clerk是进行全局设置的样式

@Data
@Style(backgroundColor = ExcelColors.RED)
public class Clerk implements ExcelAble {

    @Cell(value = "姓名",groups = {GroupA.class,GroupB.class})
    private String name;

    @Cell(value = "年龄" ,groups = GroupB.class)
    private String age;

    @Cell(value = "收益" ,groups = GroupA.class)
    private Integer income;

    public Clerk() {
    }

    interface GroupA{}
    interface GroupB{}

    public Clerk(String name, String age, Integer income) {
        this.name = name;
        this.age = age;
        this.income = income;
    }
}

效果图:

在这里插入图片描述

8. 设置字体(字体大小、字体名字、字体颜色)【全局设置、局部设置】

Shop: 局部设置列

@Data
@Sheet(name = "吹雪恒集团")
@Title(value = "吹雪恒集团报表统计",heightInPoints = 30)
public class Shop implements ExcelAble {
    @Font(fontHeightInPoints = 12,fontColor = ExcelColors.RED ,fontName = "华文琥珀")
    @Cell(value = "店名")
    private String name;
    @Font(fontHeightInPoints = 14,fontColor = ExcelColors.BLUE , fontName = "Bradley Hand ITC")
    @Cell(value = "收入")
    private Integer income;
    @Cell(value = "店长")
    private Manager manager;
    @Cell(value = "员工")
    private List<Clerk> clerks ;
    @Style(valign = ValignStyles.VERTICAL_TOP,halign = HalignStyles.ALIGN_RIGHT)//垂直靠上,水平右对齐
    @Cell(value = "创建时间",format = "yyyy-MM-dd HH:mm:ss")
    private Date createDate;

    @Cell(value = "店铺类型",readConverterExp = "1=超市,2=商场,3=餐饮" )
    private Integer shopType;


    public Shop() {
    }

    public Shop(String name, Integer income) {
        this.name = name;
        this.income = income;
    }

}

Clerk:全局设置所有列:

@Data
@Font(fontHeightInPoints = 13,fontColor = ExcelColors.GREEN)
public class Clerk implements ExcelAble {

    @Cell(value = "姓名",groups = {GroupA.class,GroupB.class})
    private String name;

    @Cell(value = "年龄" ,groups = GroupB.class)
    private String age;

    @Cell(value = "收益" ,groups = GroupA.class)
    private Integer income;

    public Clerk() {
    }

    interface GroupA{}
    interface GroupB{}

    public Clerk(String name, String age, Integer income) {
        this.name = name;
        this.age = age;
        this.income = income;
    }
}

效果图:

在这里插入图片描述

9. 动态title、动态headers【表头】

先说明一下动态title和动态headers 是什么,就是配置我们全部都做到位了,但是业务需要根据不同的时间或者前端输入名字来生成对应的title,或者前端选择导出Excel的列,这时候,只靠配置是做不到的了。那么就需要动态title和动态headers来搞定了。

Shop:

@Data
@Sheet(name = "吹雪恒集团")
@Title(value = "吹雪恒集团报表统计",heightInPoints = 30)
public class Shop implements ExcelAble {
    @Cell(value = "店名")
    private String name;
    @Cell(value = "收入")
    private Integer income;
    @Cell(value = "店长")
    private Manager manager;
    @Cell(value = "员工")
    private List<Clerk> clerks ;
    @Cell(value = "创建时间",format = "yyyy-MM-dd HH:mm:ss")
    private Date createDate;

    @Cell(value = "店铺类型",readConverterExp = "1=超市,2=商场,3=餐饮" )
    private Integer shopType;


    public Shop() {
    }

    public Shop(String name, Integer income) {
        this.name = name;
        this.income = income;
    }

}

Manager:

@Data
public class Manager implements ExcelAble {

    @Cell(value = "店长姓名",groups = Clerk.GroupA.class)
    private String name;

    @Cell(value = "性别",groups = Clerk.GroupA.class)
    private String sex;

    @Cell(value = "工作经验",groups = Clerk.GroupA.class)
    private String jobExperience;

    public Manager(String name, String sex, String jobExperience) {
        this.name = name;
        this.sex = sex;
        this.jobExperience = jobExperience;
    }

    public Manager() {
    }
}

Clerk:

@Data
public class Clerk implements ExcelAble {

    @Cell(value = "姓名",groups = {GroupA.class,GroupB.class})
    private String name;

    @Cell(value = "年龄" ,groups = GroupB.class)
    private String age;

    @Cell(value = "收益" ,groups = GroupA.class)
    private Integer income;

    public Clerk() {
    }

    interface GroupA{}
    interface GroupB{}

    public Clerk(String name, String age, Integer income) {
        this.name = name;
        this.age = age;
        this.income = income;
    }
}

测试代码:

public static void main(String[] args) throws Exception {

        Long start = System.currentTimeMillis();
        List<Shop> shops = init();
        //模拟前端传过来的headers
        String[] headers = new String[]{"name","income","manager","clerks","createDate","shopType",
                "manager.name","manager.jobExperience","clerks.name","clerks.income"};

        //模拟前端传过来的title
        String title = "吹雪恒集团报表统计【动态title】";
        HSSFWorkbook wb = ExcelUtil.exportExcel(shops,headers,title);//,
        ExcelUtil.createExcelFile(wb,"/Shop.xls");
        System.out.println(System.currentTimeMillis()-start);
    }

效果图:

在这里插入图片描述

源码分析

这个Excel插件主要运用的技术就是反射,和自定义注解,还有就是POI的API的使用。

下面我们从方法入口开始讲解,由于代码过多,所以我们这里只讲解主线部分,那些细枝末节的方法便一带而过了。

public static void main(String[] args) throws Exception {
        Long start = System.currentTimeMillis();
        List<Shop> shops = init();//初始化数据列表。
        HSSFWorkbook wb = ExcelUtil.exportExcel(shops);//生成Excel的HSSFWorkbook 对象。(最主要的方法便是这个方法。也是导出Excel的核心)
        ExcelUtil.createExcelFile(wb,"/Shop.xls");//生成文件
        System.out.println(System.currentTimeMillis()-start);
}

最主要的方法便是HSSFWorkbook wb = ExcelUtil.exportExcel(shops);方法 ,接下来我们看一下,这个方法到底干了些什么。

/**
     * 生成Excel文件对象: 1.创建一个HSSFWorkbook,对应一个Excel文件
     * 2.第二步,在workbook中添加一个sheet,对应Excel文件中的sheet 3.在sheet中添加表头第0行,创建单元格,并设置值表头
     * 设置表头居中,注意老版本poi对Excel的行数列数有限制 4.插入数据
     *
     * @param data
     *            需要生成Excel的数据列表
     * @param <T>
     * @return
     */
     //只导出数据列表,没有任何限制
    public static <T> HSSFWorkbook exportExcel(List<T> data) {
        return exportExcel(data,(Class<?>) null);
    }

    //导出数据列表,并按照Group分组来导出对应的分组列。
    public static <T> HSSFWorkbook exportExcel(List<T> data,Class<?> group) {
        return exportExcel(data,group,(String[]) null);
    }

    //导出Excel列表,并按照Group分组来导出,并且可以有外部控制,导出的分组的这些列中,有哪些可以导出。headers便是 能够导出的列。
    public static <T> HSSFWorkbook exportExcel(List<T> data,Class<?> group,List<String> headers) {
        return exportExcel(data,group,(String[])headers.toArray());
    }

    //导出Excel列表,并按照外部传入的头部,来控制显示列表。
    public static <T> HSSFWorkbook exportExcel(List<T> data,List<String> headers) {
        return exportExcel(data,null,(String[])headers.toArray());
    }

    //导出Excel列表,并按照外部传入的头部,来控制显示列表。(传入参数的数据类型的变动)
    public static <T> HSSFWorkbook exportExcel(List<T> data,String [] headers) {
        return exportExcel(data,null,headers);
    }

    /**
     * 生成Excel文件对象: 1.创建一个HSSFWorkbook,对应一个Excel文件
     * 2.第二步,在workbook中添加一个sheet,对应Excel文件中的sheet 3.在sheet中添加表头第0行,创建单元格,并设置值表头
     * 设置表头居中,注意老版本poi对Excel的行数列数有限制 4.插入数据
     *
     * @param data
     *              需要生成的数据列表
     * @param group
     *              生成列的分组
     * @param headers
     *              生成列的再次筛选控制(只有使用@Cell注解标注的字段,
     *              并且headers中存在这个字段,才会显示。
     *              如果headers 为null。认为没有筛选过滤条件。显示@Cell注解的所有字段。)
     * @param <T>
     * @return
     */
     //真正做事情的方法。一会也是重点研究的一个方法。
    public static <T> HSSFWorkbook exportExcel(List<T> data , Class<?> group , String [] headers){
        DataExcelResolver excelResolver = new DataExcelResolver(group,headers);
        //检验数据
        excelResolver.checkExcel(data);
        // 第一步,创建一个HSSFWorkbook,对应一个Excel文件
        HSSFWorkbook wb = new HSSFWorkbook();
        // 第二步,生成Sheet集合,和Sheet对应的数据
        Map<HSSFSheet, List> sheetMap = excelResolver.sheetResolver(wb, data,null);
        for (HSSFSheet sheet : sheetMap.keySet()) {
            // 第三步,在sheet中添加标题,注意老版本poi对Excel的行数列数有限制
            excelResolver.titleResolver(sheet, sheetMap.get(sheet), wb,headers);
            // 第四步,在标题下面添加表头
            excelResolver.headerResolver(sheet, sheetMap.get(sheet), wb);
            // 第五步,插入数据
            excelResolver.dataResolver(sheet,sheetMap.get(sheet), wb);
            // 第六步,数据列宽自适应
            int index = excelResolver.countParticleCell(sheetMap.get(sheet).get(0).getClass(),headers);
            //由于后期测试,这种POI的列宽自适应耗时很长,10000条大概1分钟,所以放弃了这个自适应。而是使用自己写的自适应列宽。
            /*for (int i = 0; i < index; i++) {
                sheet.autoSizeColumn(i);
            }*/
            // 第七步,处理中文列宽自适应。
            setSizeColumn(sheet,index);
        }
        return wb;
    }

我们会看到这个ExcelUtil 类里面有这么多的重载方法,这里是为了方便对外开发接口,而构造的各种导出入口。真正的导出逻辑在exportExcel(List data , Class<?> group , String [] headers)这个方法中。

我们这里主要讲解的就是第二步、第三步、第四步、和第五步。这也是Excel导出的核心和接续数据的核心。

excelResolver.sheetResolver(wb, data,null):这个方法的作用就是解析树结构的数据,然后把所有的被@Sheet注解标注的数据都按照@Sheet的配置规则生成名字,并创建Sheet页。然后读者应该也可以看到,该方法的返回值是Map<HSSFSheet, List> ,这个就是可以根据Sheet来找到对应的数据列表。然后后面的几步就是把数据列表生成Excel数据。

/**
     * 解析数据列表,讲数据和Sheet分离开,
     * 每个Sheet对应一个数据列表
     * @param wb
     * @param dataList
     * @param objData
     * @return
     */
    public Map<HSSFSheet, List> sheetResolver(HSSFWorkbook wb, List<?> dataList,Object objData){
        Map<HSSFSheet, List> res = new HashMap<>();// 结果集合
        // 遍历数据列表解析data中的Sheet注解,并获取名称和对应的data列表
        if (dataList == null || dataList.size() == 0) {
            //throw new NotHasDataRunTimeException("没有数据,无法导出Excel");
            return null;
        }
        //获取数据类型
        Object data = dataList.get(0);
        Class clazz = data.getClass();
        //获取Sheet的注解。
        Sheet sheetAnnotation = (Sheet) clazz.getAnnotation(Sheet.class);
        String sheetName = null;
        //初始化Sheet的注解解析器
        SheetAnnotationResolver<Sheet> sheetAnnotationResolver = new SheetAnnotationResolver();
        if (sheetAnnotation == null) {
            return null;
        } else {
            // 获取SheetName数据,解析Sheet注解,并获取SheetName。
            sheetName = (String) sheetAnnotationResolver.resolve(sheetAnnotation, objData);
        }
        //创建Excel中的Sheet页
        HSSFSheet sheetParent = wb.createSheet(sheetName);
        获取Sheet注解中配置的密码。并设置密码。
        String pwd = sheetAnnotation.password();
        if (pwd != null && !pwd.equals("")) {//加密码
            sheetParent.protectSheet(pwd);
        }
        //将解析好的数据加入到map集合中。key为Sheet对象,value为Sheet的数据列表。
        res.put(sheetParent, dataList);
        //遍历该类型中的所有字段,查找是否有列表。
        //如果有,则递归解析子集数据。
        Field[] fields = clazz.getDeclaredFields();
        int index = 0;
        for (Field field : fields) {// 遍历data里面的所有字段,判断 数据模型中是否有 其他列表
            if (Collection.class.isAssignableFrom(field.getType())) {//该字段是集合,需要检测是否被Sheet标记
                for (Object item : dataList) {// datas中的有多少条数据就有多少个sheet
                    Map<HSSFSheet, List> itemRes = null;// 结果集合
                    Method method;
                    List itmeData = null;
                    try {
                        if (field.getType().equals("boolean")) {// 基本变量
                            method = clazz.getMethod(ExcelUtil.getBooleanPrefix(field.getName()));
                        } else {
                            method = clazz.getMethod("get" + ExcelUtil.getMethodName(field.getName()));
                        }
                        itmeData = (List) method.invoke(item);
                    } catch (NoSuchMethodException e) {
                        e.printStackTrace();
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    } catch (InvocationTargetException e) {
                        e.printStackTrace();
                    }
                    itemRes = sheetResolver(wb,itmeData,item);
                    if (itemRes != null) {
                        res.putAll(itemRes);
                    }

                }
            }
        }
        return res;
    }

最后,我们得到了所有的Sheet页面。

那么接下来我们就应该毫不留情的去遍历所有的sheet页面,并把sheet页面对应的数据装载的sheet面中。接下来我们查看一下,如何装载这些数据的吧。并如何保证多级表头(可无限扩展)的。
// 第三步,在sheet中添加标题,注意老版本poi对Excel的行数列数有限制
excelResolver.titleResolver(sheet, sheetMap.get(sheet), wb,headers):

/**
     * 标题,行高占用两行
     *
     * @param sheet
     * @param dataList
     * @param wb
     */
    public void titleResolver(HSSFSheet sheet, List<?> dataList, HSSFWorkbook wb,String[] headers) {
        //int index = this.lastRowIndex(sheet);//获取行索引, 标题的行索引由手动定义。
        Object obj = dataList.get(0);
        Class clazz = obj.getClass();
        Title title = (Title) clazz.getAnnotation(Title.class); //获取Title
        HSSFRow row = sheet.createRow(0);//第0行为Title
        short height = title.heightInPoints();
        row.setHeightInPoints(height);
        HSSFCell cell = row.createCell(0);//创建一列

        HSSFCellStyle style = wb.createCellStyle();
        style.setAlignment(CellStyle.ALIGN_CENTER);
        style.setVerticalAlignment(CellStyle.VERTICAL_CENTER);
        cell.setCellValue(title.value());//填充title数据。
        cell.setCellStyle(style);
        int index = this.countParticleCell(clazz,headers);//获取title的宽度(占用几个单元格。)
        sheet.addMergedRegion(new CellRangeAddress(//合并单元格。
                0, // 起始行
                0, // 结束行
                0, // 起始列
                index-1  // 结束列
        ));

    }

生成标题的这个功能的是比较简单的。也只给标题设置了行高。和通用的对齐方式。

看完小编生成标题,肯定会有读者有这样的疑问,那就是,小编怎么知道这个标题应该占用多少个单元格呢 ? 这里小编写了countParticleCell 方法,是给到数据列表的一个Class类型获取到这个数据类型会占用的总列数,这个类是生成title的父类,那么我们来看一下这个方法吧。

countParticleCell(clazz,headers);

    /**
     * 传入一个数据Class,
     * 返回一个装在该数据需要的列数。
     *
     * @param clazz 字段类型/字段如果是集合,则是泛型的类型。
     * @return
     */
    @Override
    public int countParticleCell(Class<?> clazz,String[] headers) {//
        Field[] fields = clazz.getDeclaredFields();
        //字段需要占用Excel的列数。
        int cellCount = 0;
        //验证
        //是否有字段
        if (fields==null||fields.length==0) {
            return 0;
        }

        //被@Cell注解标注↓↓↓↓↓
        if (ExcelAble.class.isAssignableFrom(clazz)) {//是多属性字段(字段是实例对象)
            for (Field field :
                    fields) {
                //1.验证字段是否被@Cell注解标注
                Cell cell = field.getAnnotation(Cell.class);
                if (cell == null) {//没有被@Cell注解标注
                    continue;
                }
                HeaderExcelResolver header = new HeaderExcelResolver(group,headers);
                if (!header.verifyField(field)) {
                    continue;
                }
                Class<?> genericType = null;
                if (ExcelAble.class.isAssignableFrom(field.getType())) {
                    genericType = field.getType();
                } else if (Collection.class.isAssignableFrom(field.getType())) {
                    Type type = field.getGenericType();

                    if (type == null) continue;
                    //得到泛型类型的类名
                    if (type instanceof ParameterizedType) {
                        ParameterizedType parameterizedType = (ParameterizedType) type;
                        //得到泛型里的class类型对象
                        genericType = (Class<?>) parameterizedType.getActualTypeArguments()[0];
                    }
                } else {
                    cellCount ++;
                }
                if (genericType != null) {
                    cellCount += countParticleCell(genericType,headers);
                }

            }
        } else {
            cellCount = 1;
        }
        return cellCount;
    }

实现思路:遍历Class的所有属性,符合可显示字段的规则的,cellCount+1,遇到符合规则的字段的数据类型为ExcelAble,或者Collection类型的。那么就递归执行,cellCount+递归执行的返回值。
最终便能得到标题的总单元格数。

这里,我们就把所有的Title给生成好了。那么我们继续往下。下面我们应该是生成表头了。
我们看一下

// 第四步,在标题下面添加表头

excelResolver.headerResolver(sheet, sheetMap.get(sheet), wb):

/**
     * @param sheet
     * @param dataList
     * @param wb
     */
    public void headerResolver(HSSFSheet sheet, List<?> dataList, HSSFWorkbook wb) {
        Object data = dataList.get(0);
        Class clazz = data.getClass();//获取填充对象的Class对象,进行反射
        headerResolver(sheet,clazz,wb,0,lastRowIndex(sheet)-1);
    }

    /**
     * 解析表头
     * 1.获取最后一个需要添加的行
     * 2.创建行
     * 遍历字段;
     * 3.验证字段:
     *      1>验证字段是否被@Cell注解。
     *      2>验证是否有Group,有Group,值生成对应的Group
     *      3>验证是否有header,有header,值是否有该字段。
     *      以上三种验证都通过才能算通过,该字段才能显示。
     * 4.添加数据
     *       1> 获取最后一个需要添加的列
     *       2> 添加数据
     * 5.写入Style
     *
     * 6.验证是否是子集列表,或子集实体
     *    1> 子集递归解析。
     *    2> 不是自己准备继续。
     * 7.获取该字段占用多少单元格
     * 8.合并单元格。
     * 9.如果是子集列表或者实体,递归解析。
     * 遇到子集怎么办?遇到自己列表,则递归调用生成列。
     * @param sheet
     * @param clazz
     * @param wb
     * @param parentInsertCellIndex 父级节点所在的列的坐标。
     * @param parentRowIndex 父级节点所在行的坐标。
     */
    public void headerResolver(HSSFSheet sheet, Class<?> clazz, HSSFWorkbook wb,int parentInsertCellIndex,int parentRowIndex) {
        Field[] fields = clazz.getDeclaredFields();//获取全部的字段
        int insertRowIndex = parentRowIndex + 1;//lastRowIndex(sheet);
        HSSFRow row = sheet.getRow(insertRowIndex);//sheet.createRow(insertRowIndex);
        if (row == null) {
            row = sheet.createRow(insertRowIndex);
        }
        row.setHeightInPoints(18);
        int lastFeildCellSize = 1;
        HSSFCellStyle style = wb.createCellStyle();
        style.setAlignment(CellStyle.ALIGN_CENTER);
        style.setVerticalAlignment(CellStyle.VERTICAL_CENTER);
        //遍历字段
        for (Field field :
                fields) {
            //3.验证字段:
            if (!verifyField(field)) {//验证没有通过
                continue;
            }
            //4.添加数据:1> 获取最后一个需要添加的列
            int insertCellIndex = lastCellIndex(row)+lastFeildCellSize-1;
            //2> 添加数据
            if (insertCellIndex == 0) {
                insertCellIndex += parentInsertCellIndex;
            }
            HSSFCell cell = row.createCell(insertCellIndex);
            Cell cellAnnotation = field.getAnnotation(Cell.class);
            String cellValue = cellAnnotation.value();//获取表头
            cell.setCellValue(cellValue);//添加数据。
            //5.写入Style
            cell.setCellStyle(style);//设置样式,水平垂直居中。
            //6.验证是否是子集列表,或子集实体
            Class genericType = null;
            if (ExcelAble.class.isAssignableFrom(field.getType())) { //子集实体
                genericType = field.getType();
            } else if (Collection.class.isAssignableFrom(field.getType())) { //子集列表。
                Type type = field.getGenericType();
                if (type == null) continue;
                //得到泛型类型的类名
                if (type instanceof ParameterizedType) {
                    ParameterizedType parameterizedType = (ParameterizedType) type;
                    //得到泛型里的class类型对象
                    genericType = (Class<?>) parameterizedType.getActualTypeArguments()[0];
                }
            }
            //7.获取该字段占用多少单元格
            int fieldRowSize = 1;
            if (genericType == null) {
                fieldRowSize = countParticleRow(clazz,headers); //获取行数。
            }
            //获取字段需要的列数
            int fieldCellSize = 1;
            if (genericType == null) {
                fieldCellSize = countParticleCell(field.getType(),headers);
            } else {
                fieldCellSize = countParticleCell(genericType,headers);

            }
            lastFeildCellSize = fieldCellSize;
            //8.合并单元格。
            sheet.addMergedRegion(new CellRangeAddress(//合并单元格。
                    insertRowIndex, // 起始行
                    insertRowIndex+fieldRowSize-1, // 结束行
                    insertCellIndex, // 起始列
                    insertCellIndex+fieldCellSize-1  // 结束列
            ));
            //9.如果是子集列表或者实体,递归解析。
            if (genericType != null) {//拥有子集
                headerResolver(sheet, genericType, wb,(insertCellIndex),insertRowIndex);
            }
        }
    }

这里合并单元格,遇到一个坑,那就是合并单元格。未创建的虽然这个单元格占用了很多行或者列,但是为创建的的行,虽然占用,但是行依然是不存在的。所以如果要是获取最后一行的索引,这时候是错误的,所以方法引用了一个父级行索引,和父级列索引。

这里我们因为需要合并表头的单元格,我们依然会用到获取数据的总列数。当然我们也需要获取这个类型的表头需要占用多少行。countParticleCell(field.getType(),headers):

/**
     * 传入一个数据Class,
     * 返回一个装在该数据需要的列数。
     *
     * @param clazz 字段类型/字段如果是集合,则是泛型的类型。
     * @return
     */
    @Override
    public int countParticleCell(Class<?> clazz,String[] headers) {//
        Field[] fields = clazz.getDeclaredFields();
        //字段需要占用Excel的列数。
        int cellCount = 0;
        //验证
        //是否有字段
        if (fields==null||fields.length==0) {
            return 0;
        }
        //被@Cell注解标注↓↓↓↓↓
        if (ExcelAble.class.isAssignableFrom(clazz)) {//是多属性字段(字段是实例对象)
            for (Field field :
                    fields) {
                //1.验证字段是否被@Cell注解标注
                Cell cell = field.getAnnotation(Cell.class);
                if (cell == null) {//没有被@Cell注解标注
                    continue;
                }
                HeaderExcelResolver header = new HeaderExcelResolver(group,headers);
                if (!header.verifyField(field)) {
                    continue;
                }
                Class<?> genericType = null;
                if (ExcelAble.class.isAssignableFrom(field.getType())) {
                    genericType = field.getType();
                } else if (Collection.class.isAssignableFrom(field.getType())) {
                    Type type = field.getGenericType();

                    if (type == null) continue;
                    //得到泛型类型的类名
                    if (type instanceof ParameterizedType) {
                        ParameterizedType parameterizedType = (ParameterizedType) type;
                        //得到泛型里的class类型对象
                        genericType = (Class<?>) parameterizedType.getActualTypeArguments()[0];
                    }
                } else {
                    cellCount ++;
                }
                if (genericType != null) {
                    cellCount += countParticleCell(genericType,headers);
                }

            }
        } else {
            cellCount = 1;
        }
        return cellCount;
    }

这个实现思路还是利用反射、注解、递归。

头部设置好后,我们就可以去加载数据啦。这里我们使用的是 excelResolver.dataResolver(sheet,sheetMap.get(sheet), wb);

 /**
     * 解析数据插入数据。
     * @param sheet
     * @param dataList
     * @param wb
     */
    public void dataResolver(HSSFSheet sheet, List<?> dataList, HSSFWorkbook wb) {
        dataResolver(sheet, dataList, wb,lastRowIndex(sheet),0);
    }

    public void dataResolver(HSSFSheet sheet, List<?> dataList, HSSFWorkbook wb,int insertLastRowIndex,int insertLastCellIndex) {
        for (Object data :
                dataList) {
            HSSFRow row ;
            row = sheet.getRow(insertLastRowIndex);
            if (row == null) {
                row = sheet.createRow(insertLastRowIndex);
            }
            insertLastRowIndex  = dataResolver(sheet, data, wb,row,insertLastCellIndex,countParticleValRow(data,headers));
        }
    }


    public int dataResolver(HSSFSheet sheet, Object data, HSSFWorkbook wb, HSSFRow row,int insertLastCellIndex,int fieldRowSize) {
        if (data == null) {
            throw new CellDataIsNullException("data can’t be Null");
        }
        Class clazz = data.getClass();
        Field[] fields = clazz.getDeclaredFields();//获取所有字段
        Style classStyle = (Style) clazz.getAnnotation(Style.class);
        Style style = classStyle;
        Font classFont = (Font) clazz.getAnnotation(Font.class);
        Font font = classFont;
        //int insertCellIndex = lastCellIndex(row);
        //遍历字段,解析每个字段的数据,创建每行数据。
        //int fieldRowSize = countParticleValRow(data);
        for (Field field :
                fields) {
            Cell cell = field.getAnnotation(Cell.class);
            if (classStyle == null ) {//获取style注解
                style = field.getAnnotation(Style.class);
            } else {
                Style feildStyle = field.getAnnotation(Style.class);
                if ( feildStyle != null) {
                    style = feildStyle;
                }
            }
            if (classFont == null) {
                font = field.getAnnotation(Font.class);
            } else {
                Font feildFont = field.getAnnotation(Font.class);
                if (feildFont != null) {
                    font = feildFont;
                }
            }

            if (!verifyField(field)) {//验证没有通过
                continue;
            }
            //验证通过。
            //解析数据值
            Object value = getValue(data, field);
            if (Collection.class.isAssignableFrom(field.getType())) {//是列表集合
                Class<?> genericType = null;
                if (value == null || ((List) value).size() == 0) {
                    value = new ArrayList<>();
                    Type type = field.getGenericType();
                    if (type == null) continue;
                    //得到泛型类型的类名
                    if (type instanceof ParameterizedType) {
                        ParameterizedType parameterizedType = (ParameterizedType) type;
                        //得到泛型里的class类型对象
                        genericType = (Class<?>) parameterizedType.getActualTypeArguments()[0];
                        Object obj = null;
                        try {
                            obj = genericType.newInstance();
                            ((List) value).add(obj);
                        } catch (InstantiationException e) {
                            e.printStackTrace();
                        } catch (IllegalAccessException e) {
                            e.printStackTrace();
                        }
                    }
                }
                dataResolver(sheet, (List<?>) value, wb, row.getRowNum(),lastCellIndex(row));
            } else if (ExcelAble.class.isAssignableFrom(field.getType())) {//是对象。
                if (value == null) {
                    try {
                        value = field.getType().newInstance();
                    } catch (InstantiationException e) {
                        e.printStackTrace();
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
                dataResolver(sheet,value,wb,row,lastCellIndex(row),fieldRowSize);
            } else {//普通字段。
                HSSFCell dataCell = null;
                int lastCellIndex = lastCellIndex(row);
                if (lastCellIndex < insertLastCellIndex) {
                    lastCellIndex = insertLastCellIndex;
                }
                dataCell = row.createCell(lastCellIndex);//创建列
                String excelVal = getExcelValue(cell, value);//对Cell注解一个全面的解析,得到的最终的value值。
                HSSFCellStyle cellStyle = getStyle(wb, style, font);
                //合并单元格。
                sheet.addMergedRegion(new CellRangeAddress(//合并单元格。
                        row.getRowNum(), // 起始行
                        row.getRowNum()+fieldRowSize-1, // 结束行
                        lastCellIndex, // 起始列
                        lastCellIndex  // 结束列
                ));
                dataCell.setCellValue(excelVal);//给列设置值。
                if (cellStyle != null) {
                    dataCell.setCellStyle(cellStyle);//给列设置样式
                }

            }
        }
        return (row.getRowNum()+fieldRowSize);

    }

这里我们需要做的是解析Cell注解,

  1. 解析里面的format 属性,对时间进行时间格式化。
  2. readConverterExp 属性,对类型的转换。
  3. defaultValue 属性,对空值时的默认值的显示。

主要写数据String excelVal = getExcelValue(cell, value); 这个方法。

    /**
     * 将java数据改为Excel数据。
     * String format() default "yyyy-MM-dd";  时间格式化
     * String defaultValue() default "";   值为null时,的默认字符串
     * String readConverterExp() default ""; 读取内容转表达式 (如: 0=男,1=女,2=未知)
     * @param cell
     * @param javaValue
     * @return
     */
    public String getExcelValue(Cell cell,Object javaValue){
        String val = null;
        if (javaValue == null) {//javaVal = null 显示默认数据
            val = cell.defaultValue();
        } else {
            CellAnnotationResolver<Cell> cellCellAnnotationResolver = new CellAnnotationResolver<>();
            if (javaValue instanceof Date) {//时间类型的数据,需要时间格式化
                SimpleDateFormat format = new SimpleDateFormat(cell.format());
                String dateStr = format.format((Date) javaValue);
                val = dateStr;
            } else if (javaValue instanceof Boolean && isReadConverterExp(cell)) {//boolean类型的。true = 1,false = 0;
                Map<String, Object> converter = (Map<String, Object>) cellCellAnnotationResolver.resolve(cell,cellCellAnnotationResolver.READ_CONVERTER_EXP);
                val = (String) converter.get(getConverterKey((Boolean) javaValue));
            } else if (javaValue instanceof Number && isReadConverterExp(cell)) {// 数字类型
                Map<String, Object> converter = (Map<String, Object>) cellCellAnnotationResolver.resolve(cell,cellCellAnnotationResolver.READ_CONVERTER_EXP);
                val = (String) converter.get(getConverterKey((Number) javaValue));
            } else if (javaValue instanceof String && isReadConverterExp(cell)) {//String类型
                Map<String, Object> converter = (Map<String, Object>) cellCellAnnotationResolver.resolve(cell,cellCellAnnotationResolver.READ_CONVERTER_EXP);
                val = (String) converter.get((String) javaValue);
            } else {//不需要 做类型转换的。
                val = javaValue.toString();
            }
        }
        return val;
    }

解析对应数据类型的格式,最终都以字符串的形式写入Excel。
说到这里,excel数据导出基本已经结束了。

还有最后一步,自适应列宽:
POI给的自适应列宽性能很差,10000条数据生成时间大概需要1分钟的时间。这对导出Excel,是在是太慢了。所以小编自己写了一个算法。来计算列宽。

实现原理便是计算所有数据每列最长的那条数据,在根据公式 换算出列宽,最终定义每列的列宽。 这是实现的基本原理,下面我们看看细节吧。

private static void setSizeColumn(HSSFSheet sheet, int size) {
        int[] columnWidths = new int[size];//定义所有列最大列宽的数组
        for (int rowNum = sheet.getLastRowNum() - 1; rowNum >= 0; rowNum--) {
            HSSFRow currentRow;
            //当前行未被使用过
            if (sheet.getRow(rowNum) == null) {
                currentRow = sheet.createRow(rowNum);
            } else {
                currentRow = sheet.getRow(rowNum);
            }
            //遍历该行的所有列。计算每列的列宽,与最大列宽数组对比。
            for (int columnNum = 0; columnNum < size; columnNum++) {
                int columnWidth = sheet.getColumnWidth(columnNum) ;
                if (currentRow.getCell(columnNum) != null) {
                    HSSFCell currentCell = currentRow.getCell(columnNum);
                    if (currentCell.getCellType() == HSSFCell.CELL_TYPE_STRING) {
                        MegedRegionCellResult megedRegionCellResult = isMergedRegionCell(sheet, rowNum, columnNum);
                        int length = currentCell.getStringCellValue().length() * 256 * 9 / 4;
                        if (megedRegionCellResult.isMerged()) {//是否合并单元格
                            int sum = 0;
                            for (int i = 0; i < megedRegionCellResult.getCellMergedLength(); i++) {
                                sum += columnWidths[columnNum + i];//计算合并单元格现宽度
                            }
                            if (isChangeColumnWidth(length,sum)) {//需要自适应宽度,但是合并单元格,是多个单元格的所以得按比例分给不同的单元格。
                                for (int i = 0; i < megedRegionCellResult.getCellMergedLength(); i++) {
                                    int lengthRate = length/sum -1;
                                    //three += columnWidths[columnNum + i];
                                    length = columnWidths[columnNum + i] +lengthRate * columnWidths[columnNum + i];
                                    if (length > 15000) {
                                        length = 15000;
                                    }
                                    if (columnWidths[columnNum+i] < length) {
                                        columnWidths[columnNum+i] = length;
                                    }
                                }
                            }
                        }else {//正常情况下。
                            if (columnWidth < length) {
                                columnWidth = length;
                            }
                            if (columnWidth > 15000) {
                                columnWidth = 15000;
                            }
                            if (columnWidths[columnNum] < columnWidth) {
                                columnWidths[columnNum] = columnWidth;
                            }
                        }
                    }
                }
            }
        }
        //遍历初始化好的单元格列宽。并初始化列宽。
        for (int i = 0; i < size; i++) {
            sheet.setColumnWidth(i, columnWidths[i]);
        }
    }

这里用到了一个是初始化单元格结果的 方法。

isMergedRegionCell(sheet, rowNum, columnNum):

/**
     * 获取MegedRegionCell结果集。
     * 只判断列有没有合并,(自适应列宽,跟行没有关系,所以就忽略了。)
     * @param sheet  当前Sheet页
     * @param row    当前行
     * @param column 当前列
     * @return
     */
    private static MegedRegionCellResult isMergedRegionCell(HSSFSheet sheet, int row, int column) {
        int sheetMergeCount = sheet.getNumMergedRegions();
        for (int i = 0; i < sheetMergeCount; i++) {
            CellRangeAddress range = sheet.getMergedRegion(i);
            int firstColumn = range.getFirstColumn();
            int lastColumn = range.getLastColumn();
            int firstRow = range.getFirstRow();
            int lastRow = range.getLastRow();
            if (row >= firstRow && row <= lastRow) {//任意。
                if (column >= firstColumn && column < lastColumn) {//插入数据的时候往往都是插入到合并单元格的最左边的那个单元格。
                    //return new Result(true, firstRow + 1, lastRow + 1, firstColumn + 1, lastColumn + 1);
                    return new MegedRegionCellResult(true, lastColumn - firstColumn + 1, lastRow - firstRow + 1, column, row);
                }
            }
        }
        return new MegedRegionCellResult(false);
    }

这样我们的整个Excel导出的代码主线算是结束了。如果想深入了解的可以下载源码进行查看。当然,如果发现bug或不足的地方也可以提交修改申请的。

源码地址

https://gitee.com/lwpwork/excel

  • 13
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 21
    评论
自己封装的excel导出/导入,可以根据注解导出excel.本项目一共有13个类,里面还包含了一个反射工具,一个编码工具,10分值了。下面是测试代码 public class Test { public static void main(String[] arg) throws FileNotFoundException, IOException{ testBean(); testMap(); } public static void testBean() throws FileNotFoundException, IOException{ List l = new ArrayList(); for(int i=0;i<100;i++){ l.add(new MyBean()); } //很轻松,只需要二句话就能导出excel BeanExport be = ExportExcel.BeanExport(MyBean.class); be.createBeanSheet("1月份", "1月份人员信息").addData(l); be.createBeanSheet("2月份","2月份人员信息").addData(l); be.writeFile("E:/test/bean人员信息8.xlsx"); } //如果不想用注解,还能根据MAP导出. public static void testMap () throws FileNotFoundException, IOException{ List l = new ArrayList(); l.add(new MapHeader("姓名","name",5000)); l.add(new MapHeader("年龄","age",4000)); l.add(new MapHeader("生日","birthdate",3000)); l.add(new MapHeader("地址","address",5000)); l.add(new MapHeader("双精度","d",4000)); l.add(new MapHeader("float","f",6000)); List<Map> lm = new ArrayList<Map>(); for(int i=0;i<100;i++){ Map map = new HashMap(); map.put("name","闪电球"); map.put("age",100); map.put("birthdate",new Date()); map.put("address","北京市广东省AAA号123楼!"); map.put("d",22.222d); map.put("f",295.22f); lm.add(map); } MapExport me = ExportExcel.mapExport(l); me.createMapSheel("1月份","广东省人员信息").addData(lm); me.createMapSheel("2月份", "北京市人员信息").addData(lm); me.writeFile("E:/test/map人员信息9.xlsx"); } }
评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值