主體邏輯實現(xiàn)類:
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author
* 高性能并發(fā),低內(nèi)存導出大批量數(shù)據(jù)Excel工具類
* 解決大量數(shù)據(jù)導出可能引起的OOM和速度慢問題
* 若數(shù)據(jù)量少,可以不用本封裝類
* @param <T>
*/
public class TransferDataForExcel<T>{
private final static Logger LOGGER = LoggerFactory.getLogger(TransferDataForExcel.class);
/** 查詢導出數(shù)據(jù)服務 **/
private QueryService<T> queryService;
/** 導出文件名稱 **/
private String fileName;
/** 導出數(shù)據(jù)過程中,最大緩存行數(shù) **/
public final static Integer ROW_ACCESS_WINDOW_SIZE = 5000;
/** 每個sheet頁最多存放行數(shù),最大65536條 **/
public final static Integer MAX_ROW_SHEET = 10000;
/**
* 設置緩存隊列容量
* 因此,最大載入內(nèi)存行數(shù) = QUEUE_CAPACITY * MAX_ROW_SHEET = 10 * 10000 = 100000
**/
public final static Integer QUEUE_CAPACITY = 10;
/** 設置線程池核心線程數(shù) **/
public final static Integer THREAD_NUM = 10;
public TransferDataForExcel(QueryService<T> queryService,String fileName){
this.queryService = queryService;
this.fileName = fileName;
}
public void doExport() throws Exception {
Integer totalNum = queryService.getTotalNum();
LOGGER.info(String.format("當前導出Excel總行數(shù)=%s",totalNum));
//引入SXSSFWorkbook,利用其高效的Excel數(shù)據(jù)處理特性
SXSSFWorkbook sxssfWorkbook = new SXSSFWorkbook(ROW_ACCESS_WINDOW_SIZE);
Integer sheetNum = (totalNum / TransferDataForExcel.MAX_ROW_SHEET) + 1;
LOGGER.info(String.format("當前導出Excel總Sheet頁數(shù)=%s",sheetNum));
List<Sheet> sheetList = new ArrayList<>();
for(int i=0;i<sheetNum;i++){
Sheet sheet = sxssfWorkbook.createSheet();
sheetList.add(sheet);
}
//把任務分片后交給線程池執(zhí)行,每個分頁查詢的結(jié)果保存進一個sheet頁
ArrayBlockingQueue<List<T>> queue = new ArrayBlockingQueue<>(QUEUE_CAPACITY);
ExecutorService producerExecutorService = Executors.newFixedThreadPool(THREAD_NUM);
for (int i = 1; i <= sheetNum; i++){
Producer task = new Producer(queryService,i,queue);
producerExecutorService.submit(task);
}
producerExecutorService.shutdown();
//消費者開始消費阻塞隊列中數(shù)據(jù)
ExecutorService consumerExecutorService = Executors.newFixedThreadPool(THREAD_NUM);
CountDownLatch countDownLatch = new CountDownLatch(sheetList.size());
AtomicInteger count = new AtomicInteger(0);
for(Sheet sheet : sheetList) {
Consumer consumer = new Consumer(queryService,queue,countDownLatch,sheet,totalNum,count);
consumerExecutorService.submit(consumer);
}
countDownLatch.await();
consumerExecutorService.shutdown();
LOGGER.info("消費者處理完成,線程池關(guān)閉");
Http.Response response = Http.Response.current();
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));
response.setHeader("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
try{
sxssfWorkbook.write(response.out);
}catch (Exception e){
LOGGER.error(String.format("sxssfWorkbook寫入輸出流錯誤,原因=%s",e.getMessage()),e);
}
}
}
抽象接口,使用時只要實現(xiàn)這個接口的方法,new出TransferDataForExcel實例,把實現(xiàn)類當參數(shù)傳入即可。
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.xssf.streaming.SXSSFSheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import java.util.List;
public interface QueryService<T> {
/**
* 獲取導出總數(shù)
* @return
*/
public Integer getTotalNum();
/**
* 分頁查詢導出的數(shù)據(jù)
* @param page 起始頁
* @param pageSize 每頁數(shù)量
* @return
*/
public List<T> queryData(Integer page,Integer pageSize);
/**
* 自定義構(gòu)建導出結(jié)果
* @param row 第一行,表頭
* @throws Exception
*/
public void setHeaderForExcel(Row row);
/**
* 自定義構(gòu)建導出結(jié)果
* @param row 當前遍歷到的正在操作的行對象
* @param statisticsDTO 當前行對應的DTO對象
*/
public void setCellForExcel(Row row, T statisticsDTO);
}
導出Excel的數(shù)據(jù)源生產(chǎn)者:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Callable;
public class Producer<T> implements Callable {
private final static Logger LOGGER = LoggerFactory.getLogger(Producer.class);
private QueryService queryService;
private Integer page;
private ArrayBlockingQueue<List<T>> queue;
/**
* 初始化任務類
* @param page
*/
public Producer(QueryService queryService, Integer page, ArrayBlockingQueue<List<T>> queue) {
//對數(shù)據(jù)進行分頁查詢
this.queryService = queryService;
this.page = (page - 1) * TransferDataForExcel.MAX_ROW_SHEET;
this.queue = queue;
}
@Override
public Boolean call(){
try {
while(true){
if(queue.remainingCapacity() > 0){
//最多同時查詢頁數(shù)控制等于隊列容量,避免同時查詢過多結(jié)果載入內(nèi)存引發(fā)OOM
List<T> orderList = queryService.queryData(page,TransferDataForExcel.MAX_ROW_SHEET);
queue.put(orderList);
break;
}else{
Thread.sleep(300);
LOGGER.info(String.format("導出excel隊列空間=%s,生產(chǎn)者put隊列已滿,阻塞等待中",queue.size()));
continue;
}
}
return true;
} catch (InterruptedException e) {
LOGGER.error(String.format("導出excel生產(chǎn)者入隊錯誤,原因=%s",e.getMessage()));
return false;
}
}
}
消費數(shù)據(jù)導入Excel的消費者:
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.streaming.SXSSFSheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 導出excel隊列消費者
* @param <T>
*/
public class Consumer<T> implements Callable{
private final static Logger LOGGER = LoggerFactory.getLogger(Consumer.class);
private QueryService<T> queryService;
private ArrayBlockingQueue<List<T>> queue;
private CountDownLatch countDownLatch;
private AtomicInteger count; //已導入行數(shù)
private Integer total;
private Sheet sheet;
public Consumer(QueryService<T> queryService, ArrayBlockingQueue<List<T>> queue,
CountDownLatch countDownLatch,Sheet sheet, Integer total, AtomicInteger count) {
this.queryService = queryService;
this.queue = queue;
this.countDownLatch = countDownLatch;
this.total = total;
this.count = count;
this.sheet = sheet;
}
@Override
public Boolean call() {
try {
if(queue.remainingCapacity() == queue.size()){
LOGGER.info(String.format("隊列空間=%s,消費者take隊列為空,阻塞等待中",queue.size()));
}
List<T> orderList = queue.take();
//根據(jù)總條數(shù)確定消費者本次寫入數(shù)據(jù)時的sheet和開始行數(shù)
int currentCount = count.intValue();
count.addAndGet(orderList.size());
writeExcel(orderList,sheet);
LOGGER.info(String.format("需寫入excel總行數(shù)=%s,已寫入行數(shù)=%s,當前操作sheet頁=%s",
total,currentCount,sheet.getSheetName()));
countDownLatch.countDown();
LOGGER.info(String.format("消費者已處理任務數(shù)=%s",countDownLatch.getCount()));
return true;
} catch (Exception e) {
countDownLatch.countDown();
LOGGER.error(String.format("導出excel消費者處理錯誤=%s",e.getMessage()),e);
LOGGER.info(String.format("消費者已處理任務數(shù)=%s",countDownLatch.getCount()));
return false;
}
}
private void writeExcel(List<T> orderList,Sheet sheet){
if (CollectionUtils.isEmpty(orderList)){
return;
}
Row headRow = sheet.createRow(0);
queryService.setHeaderForExcel(headRow);
for (int i = 0; i < orderList.size(); i++) {
int beginRowNum = i+1;
Row bodyRow = sheet.createRow(beginRowNum);
T statisticsDTO = orderList.get(i);
queryService.setCellForExcel(bodyRow,statisticsDTO);
}
orderList.clear();
}
}