查看原文
其他

实现一个基于注解的 Excel 万能导出模板

康熙 终码一生 2023-07-06
点击“终码一生”,关注,置顶公众号
每日技术干货,第一时间送达!

耗时8个月联合打造 《 2023年Java高薪课程 》,更新了 102G 视频累计更新时长 500+ 个小时,需要的小伙伴可以了解下,一次购买,持续更新,无需2次付费。

前言

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

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

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

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

功能简介

  • 能够针对树结构数据导出复杂表头(多少层数据都可以)
  • 针对数据对应关系分为一对一数据关系;一对多数据关系。
  • 可选择导出多个Sheet。(针对一对多的数据关系)。
  • @Cell注解 增加Groups属性(导出时选择需要导出的Group,会自动分组,实现同一实体类不同的自定义列)
  • 增加@Style注解(设置对齐方式,填充方式,前景色,边框及边框颜色)
  • 增加@Font注解(设置字体样式,字体颜色,字体名称)
  • @Sheet注解增加了password 属性,可以给Sheet页面设置密码。
  • @Title注解增加heightInPoints 属性,给标题设置行高。
  • 导出大数据量分页(适应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:

@Datapublic 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:

@Datapublic 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:

@Datapublic 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:

@Datapublic 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:

@Datapublic 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:

@Datapublic 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 */@Overridepublic 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 == nullcontinue;                //得到泛型类型的类名                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 == nullcontinue;            //得到泛型类型的类名            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 */@Overridepublic 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 == nullcontinue;                //得到泛型类型的类名                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 == nullcontinue;                //得到泛型类型的类名                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注解,

  • 解析里面的format 属性,对时间进行时间格式化。
  • readConverterExp 属性,对类型的转换。
  • 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


END

 

【福利】2023 高薪课程,全面来袭(视频+笔记+源码)


【福利】2023 高薪课程,全面来袭(视频+笔记+源码)


PS:防止找不到本篇文章,可以收藏点赞,方便翻阅查找哦。



往期推荐



IntelliJ IDEA 2023.1 正式发布,看看又多了那些神仙功能...

Docker翻脸,不再开源!

这才是企业级的 oss-spring-boot-starter

为什么魂斗罗只有 128KB 却可以实现那么长的剧情

GPT-4 Copilot X震撼来袭!AI写代码效率10倍提升,动嘴写代码不再是梦!

Java 20正式发布!听说超神了?

全网最全的权限系统设计方案,不接受反驳!


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存