使用Springboot+poi上传并处理百万级数据EXCEL
作者:ONROAD0612 发布时间:2021-12-18 17:38:28
1 Excel上传
针对Excel的上传,采用的是比较常规的方法,其实和文件上传是相同的。具体源码如下:
@PostMapping(value = "", consumes = "multipart/*", headers = "content-type=multipart/form-data")
public Map<String, Object> addBlacklist(
@RequestParam("file") MultipartFile multipartFile, HttpServletRequest request
) {
//判断上传内容是否符合要求
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;
// 设置存放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;
}
上面的源码我们可以看见有一个saveFile方法,这个方法是将文件存在服务器本地,这样方便后续文件内容的读取,用不着一次读取所有的内容从而导致消耗大量的内存。当然这里大家如果有更好的方法希望能留言告知哈。
2 Excel处理工具源码
import org.apache.poi.openxml4j.opc.O * ackage;
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<>(); //即将进行批量插入的数据
public int willSaveAmount; //将要插入的数据量
public int totalSavedAmount; //总共插入了多少数据
private int curRow = 0; //当前行
private int curCol = 0; //当前列索引
private int preCol = 0; //上一列列索引
private int titleRow = 0; //标题行,一般情况下为0
public int rowsize = 0; //列数
//excel记录行操作方法,以sheet索引,行索引和行元素列表为参数,对sheet的一行元素进行操作,元素为String类型
public abstract void optRows(int sheetIndex, int curRow, List<String> rowlist) throws SQLException;
//只遍历一个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 {
O * ackage pkg = O * ackage.open(filename);
XSSFReader r = new XSSFReader(pkg);
SharedStringsTable sst = r.getSharedStringsTable();
XMLReader parser = fetchSheetParser(sst);
// rId2 found by processing the Workbook
// 根据 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) {
// 根据SST的索引值的到单元格的真正要存储的字符串
try {
int idx = Integer.parseInt(lastContents);
lastContents = new XSSFRichTextString(sst.getEntryAt(idx))
.toString();
} catch (Exception e) {
}
// v => 单元格的值,如果单元格是字符串则v标签的值为该字符串在SST中的索引
// 将单元格内容加入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 {
//如果标签名称为 row ,这说明已到行尾,调用 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 解析成功后的数据处理
首先我们将源码展示出来,然后再具体说明
public int addBlackLists(String file) throws ExecutionException, InterruptedException {
ArrayList<Future<Integer>> resultList = new ArrayList<>();
XxlsAbstract xxlsAbstract = new XxlsAbstract() {
//针对数据的具体处理
@Override
public void optRows(int sheetIndex, int curRow, List<String> rowlist) {
/**
* 判断即将插入的数据是否已经到达8000,如果到达8000,
* 进行数据插入
*/
if (this.willSaveAmount == 5000) {
//插入数据
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);
}
//汇总数据
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();
}
//针对没有存入的数据进行处理
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;
}
针对上面的源码,我们可以发现,我们需要将读取到的EXCEL数据插入到数据库中,这里为了减小数据库的IO和提高插入的效率,我们采用5000一批的批量插入(注意:如果数据量过大会导致组成的SQL语句无法执行)。
这里需要获取到一个最终执行成功的插入结果,并且插入执行很慢。所有采用了Java多线程的Future模式,采用异步的方式最终来获取J执行结果。
通过上面的实现,楼主测试得到最终一百万条数据需要四分钟左右的时间就可以搞定。如果大家有更好的方法,欢迎留言。
补充知识:Java API SXSSFWorkbook导出Excel大批量数据(百万级)解决导出超时
之前使用简单的HSSFWorkbook,导出的数据不能超过
后来改成SXSSFWorkbook之后可以导出更多,但是
而且我之前的代码是一次性查出所有数据,几十万条,直接就超时了。
之前的代码是一次性查出所有的结果,list里面存了几十万条数据。因为功能设计的问题,我这一个接口要同时处理三个功能:
再加上查询SQL的效率问题,导致请求超时。
现在为了做到处更大量的数据只能选择优化。优化查询的sql这里就不讲了,只讲导出功能的优化。
其实就是分批次处理查询结果:
这样做的好处是查询速度变快,封装速度也变快,整体速度变快就不会出现超时,而且,每次分页查出的结果放到list中不会出现占用JVM内存过大的情况。避免出现内存溢出导致系统崩溃。
再次优化:
上面这样做虽然可以导出,但是代码看起来不美观:
这样看起来就简洁很多了。
经验证,查询加封装EXCEL7000条数据处理只需要1秒
来源:https://blog.csdn.net/ONROAD0612/article/details/81672971


猜你喜欢
- 本文实例讲述了Android开发实现popupWindow弹出窗口自定义布局与位置控制方法。分享给大家供大家参考,具体如下:布局文件:主布局
- 关于静态类型检查和动态类型检查的解释:静态类型检查:基于程序的源代码来验证类型安全的过程;动态类型检查:在程序运行期间验证类型安全的过程;J
- 什么是 Nacos Config在分布式系统中,由于服务数量巨多,为了方便服务 配置文件统一管理,实时更新,所以需要分布式配置中心组件。Sp
- 前言本文主要给大家介绍了关于Android模仿美团顶部滑动菜单的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧。先
- 前言.NET中的委托是一个类,它定义了方法的类型,是一个方法容器。委托把方法当作参数,可以避免在程序中大量使用条件判断语句的情况。项目名为T
- 目录一、值类型和引用类型的区别1、赋值时的区别2、内存分配的区别3、来自继承结构的区别二、总结一、值类型和引用类型的区别.NET的类型可以分
- 引入dll 本次程序中引入的是Spire.Pdf.dll,引入方法如下:【方法1】通过NuGet安装。可以在Visual Stud
- 一、排序与去重日常工作中,总会有一些场景需要对结果集进行一些过滤。比如,与第三方交互后获取的结果集,需要再次排序去重,此时就会根据某个字段来
- 本文实例讲述了Android开发之FloatingActionButton悬浮按钮基本使用、字体、颜色用法。分享给大家供大家参考,具体如下:
- 1. 概述官方JavaDocsApi: javax.swing.JFrameJFrame,窗口。JFrame 是一个可以独立显示的组件,一个
- 本文实例讲述了Android自定义ActionBar的实现方法。分享给大家供大家参考。具体实现方法如下:Android 3.0及以上已经有了
- 1. 多行编辑先来体验一下从xml文件拷贝字段新建实体对象一般我们为了新建多表连接后映射的 ResultMap ,耗费不少时间,那么我们就来
- android多线程断点下载,带进度条和百分比显示,断点下载的临时数据保存到SD卡的文本文档中,建议可以保存到本地数据库中,这样可以提高存取
- 本文实例为大家分享了Java Socket编程实现多人交互聊天室的具体代码,供大家参考,具体内容如下本项目由三个.java文件(
- 目前很多Android app都内置了可以显示web页面的界面,会发现这个界面一般都是由一个叫做WebView的组件渲染出来的,学习该组件可
- using (TransactionScope tr = new TransactionScope()) {  
- 抽象方法与虚方法的区别先说两者最大的区别:抽象方法是需要子类去实现的。虚方法是已经实现了的,可以被子类覆盖,也可以不覆盖,取决于需求。因为抽
- 单源最短路径问题,即在图中求出给定顶点到其它任一顶点的最短路径。在弄清楚如何求算单源最短路径问题之前,必须弄清楚最短路径的最优子
- 前言相信大家在使用spring的项目中,前台传递参数到后台是经常遇到的事, 我们必须熟练掌握一些常用的参数传递方式和注解的使用,本文将给大家
- 使用匿名内部类课使代码更加简洁、紧凑,模块化程度更高。内部类能够访问外部内的一切成员变量和方法,包括私有的,而实现接口或继承类做不到。然而这