爲了更方便快捷地實現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 <T> void listToExcel (
List<T> list,
LinkedHashMap<String,String> 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 <T> void listToExcel (
List<T> list ,
LinkedHashMap<String,String> 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<String,String> 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<listSize; 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 <T> void fillHeadRow(
SXSSFRow row,
String[] cnFields,
CellStyle cellStyle
)throws Exception{
try{
for(int i=0;i<cnFields.length;i++){
SXSSFCell cell = row.createCell(i);
cell.setCellValue(cnFields[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 <T> void fillDataRow(
SXSSFRow row,
T item,
String[] enFields,
CellStyle cellStyle
)throws Exception{
try{
// 數據行
for(int i=0;i<enFields.length;i++){
Object objValue=getFieldValueByNameSequence(enFields[i], item);
//時間類型的值需要格式化一下再轉字符串
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<String,String> fieldMap = new LinkedHashMap<>();
List<BankBranch> 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();
}
}