自己动手实现一个Excel导出公共组件!!!!文章主要分为以下几个部分来说明:
1、明确目标
2、技术预研
3、技术方案选型与方案设计
4、技术实施
下面是实现一个Excel导出模块的目标:
通过自定义注解解析需要导出的Excel格式
支持自定义日期显示格式
支持是否显示自增列
支持约定的属性的枚举转换(比如YWG->已完工)
少量数据直接导出(5000条内)
对于大数据量支持分页导出(支持全量数据导出,百万级)
超大规模数据导出(千万级的数据量)
目标明确后就可以开始进行进行技术预研,对比了当前比较流行的三种开源Excel导出技术的特点:
JXL
效率低,操作简单
能够修饰单元格属性,格式支持不如POI强大
FastExcel
采用纯java开发的excel文件读写组件,支持Excel 97-2003文件格式
内存消耗小
只读取字符信息,诸如颜色/字体等属性都不支持
POI
效率高,操作相对复杂
支持公式,宏,图像,图表
支持修饰单元格属性;支持字体,数字,日期操作
3.8版本的POI出来了SXSSFWorkbook,可以支持大数据量的操作,但只支持xlsx格式
Excel导出方式的调研:
直接导出.xls(单表支持65532行)或者.xlsx(单表支持1048576行)后缀的excel文件
导出.txt文件
导出.csv文件
导出.xml文件
技术选型:
综合之前的开源技术分析以及导出方式分析,结合自己的业务场景,直接导出Excel是比较好的选择(用户可以直接打开)。开源技术中,JXL可以直接排除,因为效率低;FastExcel其实还不错,内存消耗又小,但是不支持复杂的操作,扩展性不佳;而POI不仅效率高,且支持各种操作(便于未来未知的扩展需求,比如需要导出表格、图像呢?),同时也支持大数据量的操作,非常合适。Excel2007已经普及了,不存在打开的障碍。
技术方案设计:
采用POI作为操作Excel的第三方库
自定义注解,有列号,列名,日期格式化,枚举类型,自增序列
注解解析,解析Excel导出的列和对应的字段
数据解析,根据注解解析结果去解析每一行数据
导出
若数据量少(<5000条),直接全量导出
若数据量大(范围[5000,1048576)),需要分批解析数据然后导出
若数据量超大,超过了Excel2007的表行数限制(1048576),采用导出多份Excel或者多张sheet的方式
定义一个自定义注解,主要包含以下几个属性(序列号,列名,日期格式,枚举名称,是否自增)
package com.shulin.winter.annotions;
import java.lang.annotation.*;
/**
* Excel导出注解
* Created by shulin on 16/12/25.
*/
@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExlOut {
/**
* 列序号,比如第1列是姓名,第2列是电话号码,第3列是地址
*
* @return
*/
int colSeq();
/**
* 列名
*
* @return
*/
String colName();
/**
* 日期类型格式化
*
* @return
*/
String dateFormat() default "yyyy-MM-dd HH:mm:SS";
/**
* 枚举的名称
*
* @return
*/
String enumName() default "";
/**
* 是否有自增序列
*
* @return
*/
boolean autoIncrement() default false;
}
有了这个注解,接下来就是定义注解的解析工作,我们需要将一个使用了这个注解的类的注解表示的相关导出信息解析出来,比如需要导出哪几列,谁前谁后,列名是什么,是否有自增序列,是否需要进行枚举转化等。
在此之前我们先定义一个解析结果类来保存注解解析后的结果:
package com.shulin.winter.annotions;
import lombok.Getter;
import lombok.Setter;
/**
* 解析注解后的对象
* Created by shulin on 16/12/25.
*/
@Setter
@Getter
public class ExlOutParseResult implements Comparable {
private int colSeq; //序列
private String colName; //列名
private String fieldName; //属性名称
private Class fieldType; //属性类型
private String formatStr; //日期格式
private Boolean autoIncrement; //是否自增,全局属性
private String enumName; //枚举名
private String enumMethod; //枚举方法
@Override
public int compareTo(Object o) {
ExlOutParseResult tmp = (ExlOutParseResult) o;
if (colSeq < tmp.colSeq) {
return -1;
} else if (colSeq > tmp.colSeq) {
return 1;
} else {
return 0;
}
}
}
接下来就是对一个被注解的类进行解析的过程了:
package com.shulin.winter.annotions;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
/**
* Created by shulin on 16/12/25.
*/
@Component
@Slf4j
public class ExlOutParser {
/**
* 解析注解
*
* @param clazz
* @return 返回的结果实际是对每一列(字段)的相关属性的包装,然后会按照seqno排序
* @throws Exception
*/
public static List<ExlOutParseResult> parseExlOut(Class clazz) throws InstantiationException, IllegalAccessException {
Object obj = clazz.newInstance();
List<ExlOutParseResult> exlOutParseResultList = new ArrayList<>();
//解析类注解,类注解是可以没有的,如果有,就是自增序列号
if (obj.getClass().isAnnotationPresent(ExlOut.class)) {
ExlOut exlOut = (ExlOut) obj.getClass().getAnnotation(ExlOut.class);
ExlOutParseResult exlOutParseResult = new ExlOutParseResult();
exlOutParseResult.setAutoIncrement(exlOut.autoIncrement());
exlOutParseResult.setColSeq(exlOut.colSeq());
exlOutParseResult.setColName(exlOut.colName());
exlOutParseResultList.add(exlOutParseResult);
}
//解析属性注解
Field[] fields = obj.getClass().getDeclaredFields();
for (Field field : fields) {
Annotation tmp = field.getAnnotation(ExlOut.class);
if (tmp != null) {
ExlOut exlOut = (ExlOut) tmp;
ExlOutParseResult exlOutParseResult = new ExlOutParseResult();
exlOutParseResult.setColName(exlOut.colName());
exlOutParseResult.setColSeq(exlOut.colSeq());
exlOutParseResult.setFieldName(field.getName());
exlOutParseResult.setFieldType(field.getType());
exlOutParseResult.setFormatStr(exlOut.dateFormat());
exlOutParseResult.setAutoIncrement(exlOut.autoIncrement());
exlOutParseResult.setEnumName(exlOut.enumName());
exlOutParseResultList.add(exlOutParseResult);
}
}
return exlOutParseResultList;
}
}
有了对注解的解析结果,我们知道了每一列的相关信息,就可以根据这个结果对每一行数据进行解析了,需要一个解析的工具类:
package com.shulin.winter.exl;
import com.shulin.winter.annotions.EnumDataType;
import com.shulin.winter.annotions.ExlOutParseResult;
import com.shulin.winter.annotions.ExlOutParser;
import com.shulin.winter.common.DateUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.springframework.util.CollectionUtils;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Date;
import java.util.List;
/**
* Created by shulin on 17/1/3.
*/
public class ExlOutUtil {
/**
* 内存中保留 1000 条数据,以免内存溢出,其余写入硬盘
* @return
*/
public static SXSSFWorkbook initSXSSFWorkbook(){
return new SXSSFWorkbook(1000);
}
/**
* 导出工具类
*
* @param sh Excel中的表
* @param rowNum 从哪一行开始写入数据,rowNum==0表示输出列名,分页使用
* @param beanList 导出对象集合
* @param exportBeanClz 导出对象Clazz
* @throws NoSuchFieldException
* @throws IllegalAccessException
* @throws ClassNotFoundException
* @throws IOException
*/
public static void exportExcel(Sheet sh, int rowNum, List> beanList, Class exportBeanClz) throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException, IOException {
List<ExlOutParseResult> exlOutParseResultList = null;
try {
exlOutParseResultList = ExlOutParser.parseExlOut(exportBeanClz);
} catch (Exception e) {
throw new RuntimeException("Excel导出对象注解解析失败!");
}
if (CollectionUtils.isEmpty(exlOutParseResultList)) {
throw new RuntimeException("没有需要导出的列!");
}
if (rowNum == 0) {
//创建Excel的标题
int size = exlOutParseResultList.size();
Row row = sh.createRow(0); // 创建第一行对象
for (int i = 0; i < size; i++) {
row.createCell(exlOutParseResultList.get(i).getColSeq()).setCellValue(exlOutParseResultList.get(i).getColName());
}
}
//具体内容赋值
if (rowNum < 1) {
rowNum = 1;
}
int autoSeq = rowNum - 1;
if (!CollectionUtils.isEmpty(beanList)) {
if (beanList.size() > 5000) {
throw new RuntimeException("导出数据大小一次不能超过5000行!");
}
for (Object bean : beanList) {
Row tmp = sh.createRow(rowNum);
for (ExlOutParseResult exlOutParseResult : exlOutParseResultList) {
Cell cell = tmp.createCell(exlOutParseResult.getColSeq());
String result = null;
if (exlOutParseResult.getAutoIncrement()) {
result = String.valueOf(autoSeq);
autoSeq++;
} else {
Field field = null;
try {
field = bean.getClass().getDeclaredField(exlOutParseResult.getFieldName());
} catch (NoSuchFieldException e) {
throw new NoSuchFieldException();
}
field.setAccessible(true);
Object val = null;
try {
val = field.get(bean);
} catch (IllegalAccessException e) {
throw new IllegalAccessException();
}
//解析日期格式
if (exlOutParseResult.getFieldType().equals(Date.class)) {
result = DateUtil.convertDateToStr((Date) val, exlOutParseResult.getFormatStr());
}
//解析枚举
else if (!StringUtils.isEmpty(exlOutParseResult.getEnumName())) {
String enumName = exlOutParseResult.getEnumName();
Class> cls = null;
try {
cls = Class.forName(enumName);
} catch (ClassNotFoundException e) {
throw e;
}
if (cls.isEnum()) {
for (Enum enu : (Enum[]) cls.getEnumConstants()) {
EnumDataType dataType = (EnumDataType) enu;
String s = dataType.getEnumValue(val);
if (s != null) {
result = s;
break;
}
}
}
}
//其他类型直接转成String输出
else {
result = String.valueOf(val);
}
}
cell.setCellValue(result);
}
rowNum++;
}
}
}
}
至此一个Excel导出模块的公共组件完成了,在此基础上可以进行扩展!!!!