欢迎来技术小站阅读,持续更新优质技术文章:
前言
上次给大家分享了一个自己开发的基于注解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;}}
效果图:
在导出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 == 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 */@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 == 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注解,
解析里面的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作者:木得未来
来源:http://blog.csdn.net/qq_366224
暂无评论数据