实现一个基于注解的 Excel 万能导出模板
前言
上次给大家分享了一个自己开发的基于注解Excel导出的小工具,不知道有没有人用,是针对复杂数据导出多个Sheet页的,那也是第一个版本。
后来小编发现了很多问题:
无法满足多复杂数据多行表头 有那种一对一的数据也没有办法显示,而且得需要去做连表查询。开发太费劲。 无法设置自己的样式或者企业规定的样式或字体。 列宽无法自适应。 遇到数据库里面 类似:type 字段的值一般都是 1,2,3,4:1:未支付,2:支付中,3:已支付 等类似的,无法直接导出,希望能够查询列表中就是这些值,只需要配置便可以导出对应的类型的中文。 遇到时间,需要自定义时间格式化。
今天给大家推出的算是一个完整版本的基于注解导出Excel的工具,花费了小编整整一周的时间来写这个,就是为了争取能把功能写到位。微信搜索公众号:Java后端编程,回复:java 领取资料 。
功能简介
能够针对树结构数据导出复杂表头(多少层数据都可以) 针对数据对应关系分为一对一数据关系;一对多数据关系。 可选择导出多个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:
@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注解,
解析里面的format 属性,对时间进行时间格式化。 readConverterExp 属性,对类型的转换。 defaultValue 属性,对空值时的默认值的显示。 微信搜索公众号:Java项目精选,回复:java 领取资料 。
主要写数据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
作者:木得未来
来源:blog.csdn.net/qq_36622496/article/
details/100152838
推荐 最近面试BAT,整理一份面试资料《Java面试BATJ通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。 获取方式:点“在看”,关注公众号并回复 Java 领取,更多内容陆续奉上。 PS:因公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列里。 点“在看”支持呀,谢谢啦
微信扫码关注该文公众号作者