前言
在实际工作中,经常会需要进行Excel文件的下载导出,并且有时希望通过异步下载来进行实现或者需要下载数据量很大。为防止各个系统重复造轮子,本文通过注解方式来实现Excel的普通、分片生成。
依赖Jar包
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>3.17</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>3.17</version>
</dependency>
实现方案
1、通过自定义注解来定义导出文件的Bean对象。
2、自定义注解可以设定导出字段的标题名称。
3、创建一个通用的Excel导出工具类,采用SXSSFWorkbook实现大数据量的Excel生成
4、满足普通生成和分片生成功能,分片上传通过内存缓存已经生成的ExcelWorkBook,然后后续分片追加,最后完成时删除缓存的文件。
直接上代码
1、自定义注解
/**
* Excel导出字段
* @author yukaiji
* @date 2020-05-20
*/
@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface XlsField {
String xlsHeaderName() default "";
}
2、自定义注解使用方式
public class Test {
@XlsField(xlsHeaderName = "姓名")
String name;
@XlsField(xlsHeaderName = "年龄")
String age;
@XlsField(xlsHeaderName = "性别")
String sex;
}
3、Excel生成工具类
import org.apache.commons.lang3.ArrayUtils;
import org.apache.poi.util.IOUtils;
import org.apache.poi.xssf.streaming.SXSSFCell;
import org.apache.poi.xssf.streaming.SXSSFRow;
import org.apache.poi.xssf.streaming.SXSSFSheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import java.io.*;
import java.lang.reflect.Field;
import java.util.*;
/**
* 通过SXSSFWorkbook实现一个大数据excel生成工具类
* 版本要求excel2007之后版本
* 扩展名为.xlsx
*
* @author yukaiji
* @date 2020-05-20
*/
public class ExcelUtil {
/**
* 用来做分片上传,以文件名称为key,已经生成过的workBook为value
**/
private static Map<String, LocalWorkbook> FILE_BOOK_MAP = new HashMap<>(64);
/**
* 单个Sheet页最大行数
**/
private static final int MAX_ROW_NUM = 1048574;
/**
* 根据自定义注解获取excel表头
**/
private static <T> List<String> genHeader(Class<T> modelClazz) {
Field[] fields = modelClazz.getDeclaredFields();
if (ArrayUtils.isEmpty(fields)) {
return new ArrayList(0);
} else {
List<String> headers = new ArrayList(fields.length);
Field[] arr$ = fields;
int len$ = fields.length;
for (int i$ = 0; i$ < len$; ++i$) {
Field field = arr$[i$];
boolean isPresent = field.isAnnotationPresent(XlsField.class);
if (isPresent) {
String headerInfo = field.getAnnotation(XlsField.class).xlsHeaderName();
headers.add(headerInfo);
}
}
return headers;
}
}
/**
* 创建一个excel文件(非分片)
*
* @param models 数据
* @param fileName 文件名称
* @return 文件
*/
public static <T> File createExcel(List<T> models, String fileName) throws IllegalAccessException {
SXSSFWorkbook workbook = createWorkBook(models, fileName);
File file = new File(fileName);
OutputStream out = null;
try {
if (!file.exists()) {
file.createNewFile();
}
out = new FileOutputStream(file);
workbook.write(out);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
} finally {
IOUtils.closeQuietly(out);
}
return file;
}
public static <T> File multipartCreateExcel(List<T> models, String fileName, boolean isFinish) throws IllegalAccessException {
return multipartCreateExcel(models, fileName, MAX_ROW_NUM, isFinish);
}
/**
* 分片生成excel
*
* @param models 数据
* @param fileName 文件名称
* @param sheetNum 每个Sheet页最大行数
* @param isFinish 是否生成完成(最后一片)
* @return 流,可以直接上传S3
*/
public static <T> File multipartCreateExcel(List<T> models, String fileName, int sheetNum, boolean isFinish) throws IllegalAccessException {
if (sheetNum > MAX_ROW_NUM) {
throw new IllegalAccessException("sheet rows num More than " + MAX_ROW_NUM + " rows ");
}
SXSSFWorkbook workbook = null;
try {
workbook = multipartCreateWorkBook(models, fileName, sheetNum);
} catch (IllegalAccessException e) {
FILE_BOOK_MAP.remove(fileName);
throw e;
}
OutputStream out = null;
File file = new File(fileName);
if (isFinish) {
try {
out = new FileOutputStream(file);
//临时缓冲区
//创建临时文件
workbook.write(out);
} catch (Exception e) {
e.printStackTrace();
} finally {
FILE_BOOK_MAP.remove(fileName);
}
}
return file;
}
/**
* 分片写入SXSSFWorkbook
*
* @param models 数据
* @param fileName 文件名称
* @param sheetRowNum 一个sheet页多少行
* @return SXSSFWorkbook excel文件
*/
private static <T> SXSSFWorkbook multipartCreateWorkBook(List<T> models, String fileName, int sheetRowNum) throws IllegalAccessException {
List<String> header = genHeader(models.get(0).getClass());
Field[] fields = models.get(0).getClass().getDeclaredFields();
SXSSFWorkbook workbook;
SXSSFSheet sheet;
SXSSFRow row;
int rowIndex = 0;
if (!FILE_BOOK_MAP.containsKey(fileName)) {
workbook = new SXSSFWorkbook(1000);
sheet = workbook.createSheet();
row = sheet.createRow(0);
for (int i = 0; i < header.size(); i++) {
SXSSFCell cell = row.createCell(i);
cell.setCellValue(header.get(i));
}
FILE_BOOK_MAP.put(fileName, new LocalWorkbook(workbook, rowIndex));
} else {
workbook = FILE_BOOK_MAP.get(fileName).getSxssfWorkbook();
sheet = workbook.getSheetAt(0);
rowIndex = FILE_BOOK_MAP.get(fileName).getRowIndex();
}
Iterator<T> it = models.iterator();
while (it.hasNext()) {
if (rowIndex == sheetRowNum) {
rowIndex = 0;
sheet = workbook.createSheet();
row = sheet.createRow(0);
for (int i = 0; i < header.size(); i++) {
SXSSFCell cell = row.createCell(i);
cell.setCellValue(header.get(i));
}
FILE_BOOK_MAP.get(fileName).setRowIndex(rowIndex);
}
rowIndex++;
row = sheet.createRow(rowIndex);
T t = (T) it.next();
int cellIndex = 0;
for (Field f : fields) {
SXSSFCell cell = row.createCell(cellIndex);
f.setAccessible(true);
boolean isPresent = f.isAnnotationPresent(XlsField.class);
if (!isPresent) {
continue;
}
String value = Objects.toString(f.get(t));
cell.setCellValue(value);
cellIndex++;
}
}
FILE_BOOK_MAP.get(fileName).setRowIndex(rowIndex);
return workbook;
}
private static <T> SXSSFWorkbook createWorkBook(List<T> models, String fileName) throws IllegalAccessException {
List<String> header = genHeader(models.get(0).getClass());
Field[] fields = models.get(0).getClass().getDeclaredFields();
SXSSFWorkbook workbook = new SXSSFWorkbook(1000);
SXSSFSheet sheet = workbook.createSheet();
SXSSFRow row = sheet.createRow(0);
for (int i = 0; i < header.size(); i++) {
SXSSFCell cell = row.createCell(i);
cell.setCellValue(header.get(i));
}
Iterator<T> it = models.iterator();
int index = 0;
while (it.hasNext()) {
index++;
row = sheet.createRow(index);
T t = (T) it.next();
int cellIndex = 0;
for (Field f : fields) {
SXSSFCell cell = row.createCell(cellIndex);
f.setAccessible(true);
boolean isPresent = f.isAnnotationPresent(XlsField.class);
if (!isPresent) {
continue;
}
String value = Objects.toString(f.get(t));
cell.setCellValue(value);
cellIndex++;
}
}
return workbook;
}
/**
* 分片文件上传文件类
*/
static class LocalWorkbook {
private LocalWorkbook(SXSSFWorkbook sxssfWorkbook, int rowIndex) {
this.sxssfWorkbook = sxssfWorkbook;
this.rowIndex = rowIndex;
this.totalRowNum = 0;
}
/**
* 未完成的workBook
**/
private SXSSFWorkbook sxssfWorkbook;
/**
* 当前sheet页row指针
**/
private int rowIndex;
/**
* 文件整体的行数
**/
private int totalRowNum;
public SXSSFWorkbook getSxssfWorkbook() {
return sxssfWorkbook;
}
public void setSxssfWorkbook(SXSSFWorkbook sxssfWorkbook) {
this.sxssfWorkbook = sxssfWorkbook;
}
public int getRowIndex() {
return rowIndex;
}
public void setRowIndex(int rowIndex) {
this.rowIndex = rowIndex;
}
public int getTotalRowNum() {
return totalRowNum;
}
public void setTotalRowNum(int totalRowNum) {
this.totalRowNum = totalRowNum;
}
}
}
4、百万数据量测试
public static void main(String[] args) throws IllegalAccessException {
String fileName = "testexcel.xlsx";
List<Test> list = new ArrayList<>(1234345);
for (int i = 0; i < 1234345; i++) {
list.add(new Test(String.valueOf(i), String.valueOf(i), String.valueOf(i)));
}
// 按照200000分片
List<List<Test>> ss = Lists.partition(list, 200000);
File file = null;
for (int i = 0; i < ss.size(); i++) {
file = ExcelUtil.multipartCreateExcel(ss.get(i), fileName, 100000, i == ss.size() - 1);
}
}
百万数据量的Excel生成大概几秒钟的时间。
总结
就简简单单通过poi包来实现文件的生成,但是只支持excel2007之后的版本,其中通过自定义注解获取要生成的列和表头,生成方式通过传入一个List列表方式。