为了更方便快捷地实现Excel导出数据,之前利用Kendo UI地Excel导出grid数据,非常方便快捷,但是却发现无法导出大量数据,所以需要导出百万级别地数据时只能后台java代码实现。通过查询资料得知,java导出excel的一般使用jxl或POI来实现,但jxl已经不更新,且无法支持Excel 2007版本(sheet的最大行数是1048576),现在一般使用POI导出。网上有很多关于使用POI的例子,这里不再赘述。
为了方便地实现后台开发,而且满足导出百万级别数据,我开发了一个POI 导出Excel的工具类(参考了他人代码)。经过我的测试,72万条数据,查询需要7、8分钟,主要是将数据从数据库传到服务器的网络耗时,POI处理数据大概需要35秒,可以接受。(吐槽下,为啥我优化代码尽量避免不必要的代码重复运行,实际却没有看到运行时间明显减少,反而让代码可读性降低了。)同时也说明,查询时应尽量避免查询不必要的数据,导致数据库传输大量数据耗时。
代码在发布到linux时,POI导出Excel可能会出现java.lang.NoClassDefFoundError: sun/awt/X11GraphicsEnvironment异常。解决方案: 在tomcat配置文件catalina.sh文件中添加 CATALINA_OPTS=”-Djava.awt.headless=true”
package utils;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map.Entry;
import javax.servlet.http.HttpServletResponse;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.FillPatternType;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.IndexedColors;
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 core.exception.ExcelException;
/**
* @ClassName: ExcelPoiUtil
* @Description:使用POI对Excel进行操作
* 现阶段只实现了将List导出Excel至浏览器:listToExcel
* @author: chong.luo
* @version 1.0
*/
public class ExcelPoiUtil {
/**
*Excel 2007最大行数1048576
*/
private static final int SHEET_MAX_SIZE_XLSX=1048576;
/**
* Sheet默认列宽,excel的列宽
*/
private static final int SHEET_DEFAULT_COLUMN_WIDTH=18;
/*
* Sheet默认行高,excel的行高
*/
//private static final float SHEET_DEFAULT_ROW_HEIGHT_POINTS = 16;
/**
* @Description: 导出Excel(导出到浏览器)
* @param list 数据源 (仅支持dto和vo)
* @param fieldMap 类的英文属性和Excel中的中文列名的对应关系
* @param response 使用response可以导出到浏览器
* @param fileName 文件名(建议加上日期后缀)
* @throws ExcelException
* @author: chong.luo
*/
public static void listToExcel (
List list,
LinkedHashMap fieldMap,
HttpServletResponse response,
String fileName
) throws ExcelException{
//设置response头信息
response.reset();
response.setContentType("application/octet-stream;charset=utf-8");
try {
response.setHeader("Content-Disposition", "attachment;filename="
+ new String(fileName.getBytes(),"iso-8859-1") + ".xlsx");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
//创建工作簿并发送到浏览器
try {
OutputStream out=response.getOutputStream();
listToExcel(list, fieldMap, SHEET_MAX_SIZE_XLSX-1, out );
} catch (Exception e) {
e.printStackTrace();
//如果是ExcelException,则直接抛出
if(e instanceof ExcelException){
throw (ExcelException)e;
//否则将其它异常包装成ExcelException再抛出
}else{
throw new ExcelException("导出Excel失败");
}
}
}
/**
* @Description: 导出Excel(可以导出到本地文件系统,也可以导出到浏览器,可自定义工作表大小)
* @param list 数据源 (仅支持dto、vo等)
* @param fieldMap 类的英文属性和Excel中的中文列名的对应关系
* 如果需要的是引用对象的属性,则英文属性使用类似于EL表达式的格式
* 如:list中存放的都是student,student中又有college属性,而我们需要学院名称,则可以这样写
* fieldMap.put("college.collegeName","学院名称")
* @param sheetSize 每个工作表中记录的最大个数
* @param out 导出流
* @throws ExcelException
* @author: chong.luo
*/
public static void listToExcel (
List list ,
LinkedHashMap fieldMap,
int sheetSize,
OutputStream out
) throws ExcelException{
/*if(list.size()==0 || list==null){
throw new ExcelException("数据源中没有任何数据");
}*/
if(sheetSize>SHEET_MAX_SIZE_XLSX-1 || sheetSize<1){
sheetSize=SHEET_MAX_SIZE_XLSX-1;
}
//1.建立操作的SXSSFWorkbook相关对象
SXSSFWorkbook workbook = new SXSSFWorkbook(100);// 内存中只创建100个对象,写临时文件,当超过100条,就将内存中不用的对象释放。
SXSSFSheet sheet = null; // 工作表对象
SXSSFRow row = null; // 行对象
CellStyle headCellStyle = getHeadCellStyle(workbook); //首行单元格样式
CellStyle cellStyle = getCellStyle(workbook); //单元格样式
try{
//2.拆解fieldMap:定义存放英文字段名和中文字段名的数组
String[] enFields=new String[fieldMap.size()];
String[] cnFields=new String[fieldMap.size()];
//填充数组
int count=0;
for(Entry entry:fieldMap.entrySet()){
enFields[count]=entry.getKey();
cnFields[count]=entry.getValue();
count++;
}
//3.建立一个新的Sheet,并填充表头列
int rowNo = 0; // 页行号
sheet = workbook.createSheet();// 建立新的Sheet
sheet.setDefaultColumnWidth(SHEET_DEFAULT_COLUMN_WIDTH); // 设置默认列宽
//sheet.setDefaultRowHeightInPoints(SHEET_DEFAULT_ROW_HEIGHT_POINTS); //设置默认列高
row = sheet.createRow(rowNo);//定义表头
fillHeadRow(row,cnFields,headCellStyle);//将列数据填充至row中
//4.一行一行填充数据列
for(int i = 0,listSize = list.size(); i//3.如果超过Sheet的行数限制,则建立一个新的Sheet,并填充表头列
if (i % sheetSize == 0 && i != 0) { // 建立新的Sheet
rowNo = 0; // 每当新建了工作表就将当前工作表的行号重置为0
sheet = workbook.createSheet();sheet.setDefaultColumnWidth(SHEET_DEFAULT_COLUMN_WIDTH); // 设置默认列宽
//sheet.setDefaultRowHeightInPoints(SHEET_DEFAULT_ROW_HEIGHT_POINTS); //设置默认列高
row = sheet.createRow(rowNo);//定义表头
fillHeadRow(row,cnFields,headCellStyle);
}
row = sheet.createRow(++rowNo); //新建行对象
fillDataRow(row,list.get(i),enFields,cellStyle);//填充数据至行中
}
//5.将SXSSFWorkbook数据写入流中
//System.out.println("-------数据处理完成,当前时间:"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()).toString() +"-------");
workbook.write(out); //写入流中
//System.out.println("-------数据导出完成,当前时间:"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()).toString() +"-------");
}catch (Exception e) {
e.printStackTrace();
//如果是ExcelException,则直接抛出
if(e instanceof ExcelException){
throw (ExcelException)e;
//否则将其它异常包装成ExcelException再抛出
}else{
throw new ExcelException("导出Excel失败");
}
}
}
/**
* @Description: 向首行对象中填充列标题(分离首行填充,以提高代码效率)
* @param row 行对象
* @param cnFields 首行列标题,来源于fieldMap
* @param cellStyle 填充行所用的单元格样式
* @throws Exception
* @author: chong.luo
*/
private static void fillHeadRow(
SXSSFRow row,
String[] cnFields,
CellStyle cellStyle
)throws Exception{
try{
for(int i=0;i//设置单元格的值
cell.setCellStyle(cellStyle); //设置单元格的样式
}
}catch (Exception e) {
e.printStackTrace();
if(e instanceof ExcelException){ //如果是ExcelException,则直接抛出
throw (ExcelException)e;
}else{ //否则将其它异常包装成ExcelException再抛出
throw new ExcelException("导出Excel失败:填充第"+(row.getRowNum()+1)+"行时失败");
}
}
return;
}
/**
* @Description: 向数据行行对象中填充数据
* @param row 行对象
* @param item 列数据,dto或vo
* @param enFields 类的英文属性 ,来源于fieldMap
* @param cellStyle 填充行所用的单元格样式
* @throws Exception
* @author: chong.luo
*/
private static void fillDataRow(
SXSSFRow row,
T item,
String[] enFields,
CellStyle cellStyle
)throws Exception{
try{
// 数据行
for(int i=0;i//时间类型的值需要格式化一下再转字符串
if((objValue instanceof Date) && objValue != null){
String dataValue = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(objValue);
if(dataValue.endsWith("00:00:00")){
dataValue = dataValue.substring(0, 10); //去掉时分秒
}
objValue = dataValue;
}
//设置单元格的值及样式
String fieldValue=objValue==null ? "" : objValue.toString();
SXSSFCell cell = row.createCell(i);
cell.setCellValue(fieldValue); //设置单元格的值
cell.setCellStyle(cellStyle); //设置单元格的样式,不启用以提高导出速度
}
}catch (Exception e) {
e.printStackTrace();
//如果是ExcelException,则直接抛出
if(e instanceof ExcelException){
throw (ExcelException)e;
//否则将其它异常包装成ExcelException再抛出
}else{
throw new ExcelException("导出Excel失败:填充第"+(row.getRowNum()+1)+"行时失败");
}
}
return;
}
/**
* @Description: 获取首行的样式信息
* @param workbook
* @return
* @author: chong.luo
*/
private static CellStyle getHeadCellStyle(SXSSFWorkbook workbook){
CellStyle headCellStyle = workbook.createCellStyle();
headCellStyle.setAlignment(HorizontalAlignment.CENTER); //设置居中
//headCellStyle.setFillBackgroundColor(IndexedColors.LIGHT_ORANGE.index);
headCellStyle.setFillForegroundColor(IndexedColors.GREY_50_PERCENT.index);//设置背景色(前景色)
headCellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); //设置背景色必须的
Font headFont = workbook.createFont(); //字体样式
//headFont.setBold(true); //设置字体加粗
headFont.setFontName("宋体"); //字体
headFont.setColor(IndexedColors.WHITE.index);
headCellStyle.setFont(headFont);
return headCellStyle;
}
/**
* @Description: 获取单元格的样式信息
* @param workbook
* @return
* @author: chong.luo
*/
private static CellStyle getCellStyle(SXSSFWorkbook workbook){
CellStyle style = workbook.createCellStyle(); //注意java.lang.IllegalStateExceptionYou can define up to 64000 style in a .xlsx Workbook
//style.setAlignment(HorizontalAlignment.CENTER); //设置居中
//headCellStyle.setFillBackgroundColor(IndexedColors.LIGHT_ORANGE.index);
//style.setFillForegroundColor(IndexedColors.LIGHT_GREEN.index);//设置背景色(前景色)
//style.setFillPattern(FillPatternType.SOLID_FOREGROUND); //设置背景色必须的
Font font = workbook.createFont(); //字体样式
//font.setBold(true); //设置字体加粗
font.setFontName("宋体"); //字体
style.setFont(font);
return style;
}
/**
* @MethodName : getFieldValueByNameSequence
* @Description :
* 根据带路径或不带路径的属性名获取属性值
* 即接受简单属性名,如userName等,又接受带路径的属性名,如student.department.name等
*
* @param fieldNameSequence 带路径的属性名或简单属性名
* @param o 对象
* @return 属性值
* @throws Exception
*/
private static Object getFieldValueByNameSequence(String fieldNameSequence, Object o) throws Exception{
Object value=null;
//将fieldNameSequence进行拆分
String[] attributes=fieldNameSequence.split("\\.");
if(attributes.length==1){
value=getFieldValueByName(fieldNameSequence, o);
}else{
//根据属性名获取属性对象
Object fieldObj=getFieldValueByName(attributes[0], o);
String subFieldNameSequence=fieldNameSequence.substring(fieldNameSequence.indexOf(".")+1);
value=getFieldValueByNameSequence(subFieldNameSequence, fieldObj);
}
return value;
}
/**
* @MethodName : getFieldValueByName
* @Description : 根据字段名获取字段值
* @param fieldName 字段名
* @param o 对象
* @return 字段值
*/
private static Object getFieldValueByName(String fieldName, Object o) throws Exception{
Object value=null;
Field field=getFieldByName(fieldName, o.getClass());
if(field !=null){
field.setAccessible(true);
value=field.get(o);
}else{
throw new ExcelException(o.getClass().getSimpleName() + "类不存在字段名 "+fieldName);
}
return value;
}
/**
* @MethodName : getFieldByName
* @Description : 根据字段名获取字段
* @param fieldName 字段名
* @param clazz 包含该字段的类
* @return 字段
*/
private static Field getFieldByName(String fieldName, Class> clazz){
//拿到本类的所有字段
Field[] selfFields=clazz.getDeclaredFields();
//如果本类中存在该字段,则返回
for(Field field : selfFields){
if(field.getName().equals(fieldName)){
return field;
}
}
//否则,查看父类中是否存在此字段,如果有则返回
Class> superClazz=clazz.getSuperclass();
if(superClazz!=null && superClazz !=Object.class){
return getFieldByName(fieldName, superClazz);
}
//如果本类和父类都没有,则返回空
return null;
}
}
ExcelException源代码,这个不重要,可自己实现
package core.exception;
@SuppressWarnings("serial")
public class ExcelException extends Exception {
public ExcelException() {
// TODO Auto-generated constructor stub
}
public ExcelException(String message) {
super(message);
// TODO Auto-generated constructor stub
}
public ExcelException(Throwable cause) {
super(cause);
// TODO Auto-generated constructor stub
}
public ExcelException(String message, Throwable cause) {
super(message, cause);
// TODO Auto-generated constructor stub
}
}
@RequestMapping(value = "/bankBranch/exportExcelPoi")
@ResponseBody
public void bankBranchExportExcel(BankBranch dto, HttpServletRequest request, HttpServletResponse response) {
LinkedHashMap fieldMap = new LinkedHashMap<>();
List bankBranchList = bankBranchMapper.selectExport(dto);
fieldMap.put("branchName", "分行名称");
fieldMap.put("branchDescType", "分行类型");
fieldMap.put("bankName", "银行名称");
fieldMap.put("stateName", "国家");
fieldMap.put("provinceName", "省份");
fieldMap.put("cityName", "城市");
String fileName = "银行分行_"+new SimpleDateFormat("yyyyMMdd").format(new Date()).toString();
try {
ExcelPoiUtil.listToExcel(bankBranchList, fieldMap, response, fileName);
} catch (Exception e) {
e.printStackTrace();
}
}