使用Springboot+poi上傳并處理百萬級數(shù)據(jù)EXCEL
1 Excel上傳
針對Excel的上傳,采用的是比較常規(guī)的方法,其實(shí)和文件上傳是相同的。具體源碼如下:
@PostMapping(value = "", consumes = "multipart/*", headers = "content-type=multipart/form-data")
public Map<String, Object> addBlacklist(
@RequestParam("file") MultipartFile multipartFile, HttpServletRequest request
) {
//判斷上傳內(nèi)容是否符合要求
String fileName = multipartFile.getOriginalFilename();
if (!fileName.matches("^.+\\.(?i)(xls)$") && !fileName.matches("^.+\\.(?i)(xlsx)$")) {
return returnError(0,"上傳的文件格式不正確");
}
String file = saveFile(multipartFile, request);
int result = 0;
try {
result = blacklistServcice.addBlackLists(file);
} catch (Exception e) {
e.printStackTrace();
}
return returnData(result);
}
private String saveFile(MultipartFile multipartFile, HttpServletRequest request) {
String path;
String fileName = multipartFile.getOriginalFilename();
// 判斷文件類型
String realPath = request.getSession().getServletContext().getRealPath("/");
String trueFileName = fileName;
// 設(shè)置存放Excel文件的路徑
path = realPath + trueFileName;
File file = new File(path);
if (file.exists() && file.isFile()) {
file.delete();
}
try {
multipartFile.transferTo(new File(path));
} catch (IOException e) {
e.printStackTrace();
}
return path;
}
上面的源碼我們可以看見有一個(gè)saveFile方法,這個(gè)方法是將文件存在服務(wù)器本地,這樣方便后續(xù)文件內(nèi)容的讀取,用不著一次讀取所有的內(nèi)容從而導(dǎo)致消耗大量的內(nèi)存。當(dāng)然這里大家如果有更好的方法希望能留言告知哈。
2 Excel處理工具源碼
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.xssf.eventusermodel.XSSFReader;
import org.apache.poi.xssf.model.SharedStringsTable;
import org.apache.poi.xssf.usermodel.XSSFRichTextString;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;
import java.io.InputStream;
import java.sql.SQLException;
import java.util.*;
/**
* XSSF and SAX (Event API)
*/
public abstract class XxlsAbstract extends DefaultHandler {
private SharedStringsTable sst;
private String lastContents;
private int sheetIndex = -1;
private List<String> rowlist = new ArrayList<>();
public List<Map<String, Object>> dataMap = new LinkedList<>(); //即將進(jìn)行批量插入的數(shù)據(jù)
public int willSaveAmount; //將要插入的數(shù)據(jù)量
public int totalSavedAmount; //總共插入了多少數(shù)據(jù)
private int curRow = 0; //當(dāng)前行
private int curCol = 0; //當(dāng)前列索引
private int preCol = 0; //上一列列索引
private int titleRow = 0; //標(biāo)題行,一般情況下為0
public int rowsize = 0; //列數(shù)
//excel記錄行操作方法,以sheet索引,行索引和行元素列表為參數(shù),對sheet的一行元素進(jìn)行操作,元素為String類型
public abstract void optRows(int sheetIndex, int curRow, List<String> rowlist) throws SQLException;
//只遍歷一個(gè)sheet,其中sheetId為要遍歷的sheet索引,從1開始,1-3
/**
* @param filename
* @param sheetId sheetId為要遍歷的sheet索引,從1開始,1-3
* @throws Exception
*/
public void processOneSheet(String filename, int sheetId) throws Exception {
OPCPackage pkg = OPCPackage.open(filename);
XSSFReader r = new XSSFReader(pkg);
SharedStringsTable sst = r.getSharedStringsTable();
XMLReader parser = fetchSheetParser(sst);
// rId2 found by processing the Workbook
// 根據(jù) rId# 或 rSheet# 查找sheet
InputStream sheet2 = r.getSheet("rId" + sheetId);
sheetIndex++;
InputSource sheetSource = new InputSource(sheet2);
parser.parse(sheetSource);
sheet2.close();
}
public XMLReader fetchSheetParser(SharedStringsTable sst)
throws SAXException {
XMLReader parser = XMLReaderFactory.createXMLReader();
this.sst = sst;
parser.setContentHandler(this);
return parser;
}
public void endElement(String uri, String localName, String name) {
// 根據(jù)SST的索引值的到單元格的真正要存儲的字符串
try {
int idx = Integer.parseInt(lastContents);
lastContents = new XSSFRichTextString(sst.getEntryAt(idx))
.toString();
} catch (Exception e) {
}
// v => 單元格的值,如果單元格是字符串則v標(biāo)簽的值為該字符串在SST中的索引
// 將單元格內(nèi)容加入rowlist中,在這之前先去掉字符串前后的空白符
if (name.equals("v")) {
String value = lastContents.trim();
value = value.equals("") ? " " : value;
int cols = curCol - preCol;
if (cols > 1) {
for (int i = 0; i < cols - 1; i++) {
rowlist.add(preCol, "");
}
}
preCol = curCol;
rowlist.add(curCol - 1, value);
} else {
//如果標(biāo)簽名稱為 row ,這說明已到行尾,調(diào)用 optRows() 方法
if (name.equals("row")) {
int tmpCols = rowlist.size();
if (curRow > this.titleRow && tmpCols < this.rowsize) {
for (int i = 0; i < this.rowsize - tmpCols; i++) {
rowlist.add(rowlist.size(), "");
}
}
try {
optRows(sheetIndex, curRow, rowlist);
} catch (SQLException e) {
e.printStackTrace();
}
if (curRow == this.titleRow) {
this.rowsize = rowlist.size();
}
rowlist.clear();
curRow++;
curCol = 0;
preCol = 0;
}
}
}
}
3 解析成功后的數(shù)據(jù)處理
首先我們將源碼展示出來,然后再具體說明
public int addBlackLists(String file) throws ExecutionException, InterruptedException {
ArrayList<Future<Integer>> resultList = new ArrayList<>();
XxlsAbstract xxlsAbstract = new XxlsAbstract() {
//針對數(shù)據(jù)的具體處理
@Override
public void optRows(int sheetIndex, int curRow, List<String> rowlist) {
/**
* 判斷即將插入的數(shù)據(jù)是否已經(jīng)到達(dá)8000,如果到達(dá)8000,
* 進(jìn)行數(shù)據(jù)插入
*/
if (this.willSaveAmount == 5000) {
//插入數(shù)據(jù)
List<Map<String, Object>> list = new LinkedList<>(this.dataMap);
Callable<Integer> callable = () -> {
int count = blacklistMasterDao.addBlackLists(list);
blacklistRecordMasterDao.addBlackListRecords(list);
return count;
};
this.willSaveAmount = 0;
this.dataMap = new LinkedList<>();
Future<Integer> future = executor.submit(callable);
resultList.add(future);
}
//匯總數(shù)據(jù)
Map<String, Object> map = new HashMap<>();
map.put("uid", rowlist.get(0));
map.put("createTime", rowlist.get(1));
map.put("regGame", rowlist.get(2));
map.put("banGame", rowlist.get(2));
this.dataMap.add(map);
this.willSaveAmount++;
this.totalSavedAmount++;
}
};
try {
xxlsAbstract.processOneSheet(file, 1);
} catch (Exception e) {
e.printStackTrace();
}
//針對沒有存入的數(shù)據(jù)進(jìn)行處理
if(xxlsAbstract.willSaveAmount != 0){
List<Map<String, Object>> list = new LinkedList<>(xxlsAbstract.dataMap);
Callable<Integer> callable = () -> {
int count = blacklistMasterDao.addBlackLists(list);
blacklistRecordMasterDao.addBlackListRecords(list);
return count;
};
Future<Integer> future = executor.submit(callable);
resultList.add(future);
}
executor.shutdown();
int total = 0;
for (Future<Integer> future : resultList) {
while (true) {
if (future.isDone() && !future.isCancelled()) {
int sum = future.get();
total += sum;
break;
} else {
Thread.sleep(100);
}
}
}
return total;
}
針對上面的源碼,我們可以發(fā)現(xiàn),我們需要將讀取到的EXCEL數(shù)據(jù)插入到數(shù)據(jù)庫中,這里為了減小數(shù)據(jù)庫的IO和提高插入的效率,我們采用5000一批的批量插入(注意:如果數(shù)據(jù)量過大會(huì)導(dǎo)致組成的SQL語句無法執(zhí)行)。
這里需要獲取到一個(gè)最終執(zhí)行成功的插入結(jié)果,并且插入執(zhí)行很慢。所有采用了Java多線程的Future模式,采用異步的方式最終來獲取J執(zhí)行結(jié)果。
通過上面的實(shí)現(xiàn),樓主測試得到最終一百萬條數(shù)據(jù)需要四分鐘左右的時(shí)間就可以搞定。如果大家有更好的方法,歡迎留言。
補(bǔ)充知識:Java API SXSSFWorkbook導(dǎo)出Excel大批量數(shù)據(jù)(百萬級)解決導(dǎo)出超時(shí)
之前使用簡單的HSSFWorkbook,導(dǎo)出的數(shù)據(jù)不能超過

后來改成SXSSFWorkbook之后可以導(dǎo)出更多,但是
而且我之前的代碼是一次性查出所有數(shù)據(jù),幾十萬條,直接就超時(shí)了。
之前的代碼是一次性查出所有的結(jié)果,list里面存了幾十萬條數(shù)據(jù)。因?yàn)楣δ茉O(shè)計(jì)的問題,我這一個(gè)接口要同時(shí)處理三個(gè)功能:

再加上查詢SQL的效率問題,導(dǎo)致請求超時(shí)。
現(xiàn)在為了做到處更大量的數(shù)據(jù)只能選擇優(yōu)化。優(yōu)化查詢的sql這里就不講了,只講導(dǎo)出功能的優(yōu)化。
其實(shí)就是分批次處理查詢結(jié)果:

這樣做的好處是查詢速度變快,封裝速度也變快,整體速度變快就不會(huì)出現(xiàn)超時(shí),而且,每次分頁查出的結(jié)果放到list中不會(huì)出現(xiàn)占用JVM內(nèi)存過大的情況。避免出現(xiàn)內(nèi)存溢出導(dǎo)致系統(tǒng)崩潰。
再次優(yōu)化:
上面這樣做雖然可以導(dǎo)出,但是代碼看起來不美觀:

這樣看起來就簡潔很多了。
經(jīng)驗(yàn)證,查詢加封裝EXCEL7000條數(shù)據(jù)處理只需要1秒



以上這篇使用Springboot+poi上傳并處理百萬級數(shù)據(jù)EXCEL就是小編分享給大家的全部內(nèi)容了,希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
springboot定時(shí)任務(wù)SchedulingConfigurer異步多線程實(shí)現(xiàn)方式
這篇文章主要介紹了springboot定時(shí)任務(wù)SchedulingConfigurer異步多線程實(shí)現(xiàn)方式,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-04-04
Spring項(xiàng)目里將SQL語句寫在.sql文件中的方法
這篇文章主要介紹了Spring項(xiàng)目里如何將SQL語句寫在.sql文件中的方法,文中給出了詳細(xì)的介紹和示例代碼,相信對大家的學(xué)習(xí)或者工作具有一定的參考借鑒價(jià)值,有需要的朋友們下面來一起看看吧。2017-01-01
淺談如何優(yōu)雅地停止Spring Boot應(yīng)用
這篇文章主要介紹了淺談如何優(yōu)雅地停止Spring Boot應(yīng)用,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05
Java中float類型的范圍及其與十六進(jìn)制的轉(zhuǎn)換例子
這篇文章主要介紹了Java中float類型的范圍及其與十六進(jìn)制的轉(zhuǎn)換例子,是Java入門學(xué)習(xí)中的基礎(chǔ)知識,需要的朋友可以參考下2015-10-10
通過springboot+mybatis+druid配置動(dòng)態(tài)數(shù)據(jù)源
這篇文章主要介紹了通過springboot+mybatis+druid配置動(dòng)態(tài)數(shù)據(jù)源,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,,需要的朋友可以參考下2019-06-06
ElasticSearch學(xué)習(xí)之Es索引Api操作
這篇文章主要為大家介紹了ElasticSearch學(xué)習(xí)之Es索引Api操作詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01

