SpringBoot整合POI实现Excel导入(百万级数据处理方式)
目录
- 1、pom
- 1、基本excel导入
- 2、大量数据excel导入
1、pom
引入pom包:
<dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>4.0.0</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-excelant</artifactId> <version>4.0.0</version> </dependency>
1、基本excel导入
导入模板样式如下:
解析器封装:
为了做到统一的excel解析处理,进行通用的解析器封装,ExcelHandler
,放了满足一边解析数据一边存库的需要,通过Consumer
接口实现回调操作,保证解析到一定数据量后就可以进行后续操作。
代码如下:
import org.apache.poi.ss.usermodel.*;import org.apache.poi.xssf.usermodel.XSSFWorkbook;import java.io.InputStream;import java.lang.reflect.Constructor;import java.util.ArrayList;import java.util.List;import java.util.function.Consumer;/** * @Author: LiHuaZhi * @Date: 2022/1/16 14:58 * @Description: **/public class ExcelHandler<T> { /** * 批量处理梳理 */ public Integer batchNum; /** * 开始解析的行号,第一行时,startRow为0 */ public Integer startRow; /** * 封装的entity对象 */ public Class<?> entity; public Constructor<?> constructor; /** * 传入解析数据的service对象 */ public Consumer<List<T>> uploadService; /** * 接收解析对象值 */ public List<T> list = new ArrayList<>(); public ExcelHandler(Consumer<List<T>> uploadService, Class<?> entity, Integer startRow, Integer batchNum) { this.uploadService = uploadService; this.entity = entity; this.startRow = startRow; this.batchNum = batchNum == null ? 1000 : batchNum; } /** * @param inputStream 数据流 * @throws Exception */ public void handlerData(InputStream inputStream) throws Exception { // 创建构造器 this.constructor = entity.getDeclaredConstructor(new Class[]{List.class}); // 创建表格对象 XSSFWorkbook workbook = new XSSFWorkbook(inputStream); //有多少个sheet int sheets = workbook.getNumberOfSheets(); for (int i = 0; i < sheets; i++) { Sheet sheet = workbook.getSheetAt(i); //获取多少行 int rows = sheet.getPhysicalNumberOfRows(); //注意:第0为表头,第1行开始解析数据 for (int currentRow = 0; currentRow < rows; currentRow++) { parseCellData(sheet, currentRow); } } // 所有数据解析完成后 saveData(list); } /** * 一行解析完成后 */ private void endRow(List<Object> values, int currentRow) throws Exception { if (currentRow < startRow) { // 加载表头数据 System.out.println("解析头部数据:" + values); } else { System.out.println("解析表格数据:" + values); T data = (T) constructor.newInstance(values); list.add(data); } if (list.size() >= batchNum) { // 回调接口,处理数据 saveData(list); } } /** * 获取表格数据 * * @param sheet * @param currentRow * @return */ private void parseCellData(Sheet sheet, int currentRow) throws Exception { //获得第行号 Row row = sheet.getRow(currentRow); // 解析正文数据 List<Object> values = new ArrayList<>(); for (int cellNum = 0; cellNum < row.getLastCellNum(); cellNum++) { Cell cell = row.getCell(cellNum); Object value = getCellValue(cell); values.add(value); } endRow(values, currentRow); } /** * 解析数据类型 * * @param cell * @return */ private Object getCellValue(Cell cell) { //1.获取到单元格的属性类型 CellType cellType = cell.getCellType(); //2.根据单元格数据类型获取数据 Object value = null; switch (cellType) { case STRING: value = cell.getStringCellValue(); break; case BOOLEAN: value = cell.getBooleanCellValue(); break; case NUMERIC: if (DateUtil.isCellDateFormatted(cell)) { //日期格式 value = cell.getDateCellValue(); } else { //数字,poi解析的数值都是double类型 value = cell.getNumericCellValue(); } break; case FORMULA: //公式 value = cell.getCellFormula(); break; default: break; } return value; } /** * 回调保存数据 * * @param dataList */ private void saveData(List<T> dataList) { if (dataList.size() > 0) { uploadService.accept(dataList); dataList.clear(); } }}
实体类:
为了方便数据的统一解析,在解析时不对实体具体字段进行赋值操作,而是在实体类中根据字段类型进行相应的转换并且赋值;
@Datapublic class User { private String name; private Integer agx; private String gender; private String address; private Double height; // 进行解析后的类型转换 public User(List<Object> values) { this.name = values.get(0) == null ? "" : values.get(0).toString(); this.agx = values.get(1) == null ? 0 : new Double(values.get(1).toString()).intValue(); this.gender = values.get(2) == null ? "" : values.get(0).toString(); this.address = values.get(3) == null ? "" : values.get(0).toString(); this.height = values.get(4) == null ? 0 : Double.parseDouble(values.get(4).toString()); }}
Controller:
@PostMapping("upload") @ResponseBody public String upload(@RequestParam("file") MultipartFile file) throws Exception { try { if (file == null) { return "文件为空"; } InputStream inputStream = file.getInputStream(); // 注册事件处理器 Consumer<List<User>> consumer = uploadData -> importData2(uploadData); // 传入参数进行解析 new ExcelHandler(consumer, User.class, 1, null).handlerData(inputStream); } catch (IOException e) { e.printStackTrace(); return "解析失败"; } return "操作成功"; }// 接收Handler回传的数据 public void importData2(List<User> list) { System.out.println("解析数量:" + list.size()); }
2、大量数据excel导入
针对于数据过多时,可以使用采用大量数据excel
导入方式,比如上万级别的数据,实测中导入百万数据
大约耗时20秒
;
导入模板样式如下,并且数据量为百万
:
解析器封装:
为了做到统一的excel解析处理,进行通用的解析器封装,SheetHandler
以及XssfSheetHandler
,放了满足一边解析数据一边存库的需要,通过Consumer
接口实现回调操作,保证解析到一定数据量后就可以进行后续操作。
代码如下:
- SheetHandler
import org.apache.poi.xssf.eventusermodel.XSSFSheetXMLHandler;import org.apache.poi.xssf.usermodel.XSSFComment;import java.lang.reflect.Constructor;import java.util.ArrayList;import java.util.List;import java.util.function.Consumer;/** * 自定义的事件处理器 * 处理每一行数据读取 * 实现接口 * * @author LiHuaZhi */public class SheetHandler<T> implements XSSFSheetXMLHandler.SheetContentsHandler { /** * 批量处理梳理 */ public Integer batchNum; /** * 开始解析的行号,第一行时,startRow为0 */ public Integer startRow; /** * 封装的entity对象 */ public Class<?> entity; /** * 当前解析的行号 */ private Integer currentRow; /** * 传入解析数据的service对象 */ public Consumer<List<T>> uploadService; /** * 临时解析对象的构造器 */ Constructor<?> constructor; /** * 接收解析对象值 */ public List<T> list = new ArrayList<>(); /** * 解析头部数据 */ public List<String> headList = new ArrayList<>(); /** * 解析单元格 */ public List<String> valueList = new ArrayList<>(); /** * 解析的列号,默认0为第一列 */ private Integer cellNum = 0; public SheetHandler(Integer batchNum, Integer startRow, Consumer<List<T>> uploadService, Class<?> entity) throws Exception { constructor = entity.getDeclaredConstructor(new Class[]{List.class}); this.batchNum = batchNum == null ? 1000 : batchNum; this.startRow = startRow; this.uploadService = uploadService; this.entity = entity; } /** * 当开始解析某一行的时候触发 * i:行索引 */ @Override public void startRow(int row) { currentRow = row; } /** * 当结束解析某一行的时候触发 * i:行索引 */ @Override public void endRow(int row) { try { cellNum = 0; if (headList.size() > 0) { System.out.println("解析头部数据:" + headList); } if (valueList.size() > 0) { System.out.println("解析表格数据:" + valueList); T data = (T) constructor.newInstance(valueList); list.add(data); } if (list.size() >= batchNum) { // 回调接口,处理数据 saveData(list); } } catch (Exception e) { e.printStackTrace(); } finally { headList.clear(); valueList.clear(); } } /** * 对行中的每一个表格进行处理 * cellReference: 单元格名称 * value:数据 * xssfComment:批注 */ @Override public void cell(String cellReference, String value, XSSFComment xssfComment) { try { if (currentRow < startRow) { // 加载表头数据 headList.add(value); } else { // 获取表格数据 valueList.add(value); } cellNum++; } catch (Exception e) { e.printStackTrace(); } } /** * 解析完sheet,多个sheet会回调多次 */ @Override public void endSheet() { System.out.println("解析完成"); saveData(list); } private void saveData(List<T> dataList) { if (dataList.size() > 0) { uploadService.accept(dataList); dataList.clear(); } }}
- XssfSheetHandler
import org.apache.poi.openxml4j.opc.OPCPackage;import org.apache.poi.xssf.eventusermodel.XSSFReader;import org.apache.poi.xssf.eventusermodel.XSSFSheetXMLHandler;import org.apache.poi.xssf.model.SharedStringsTable;import org.apache.poi.xssf.model.StylesTable;import org.xml.sax.InputSource;import org.xml.sax.XMLReader;import org.xml.sax.helpers.XMLReaderFactory;import java.io.InputStream;import java.util.function.Consumer;/** * @Author: LiHuaZhi * @Date: 2022/1/16 14:57 * @Description: **/public class XssfSheetHandler { /** * @param inputStream 数据流 * @param consumer 自定义回调 * @param entity 解析数据实体 * @param batchNum 批处理数量 * @param startRow excel解析正文行号 * @throws Exception */ public static void handlerData(InputStream inputStream, Consumer consumer, Class<?> entity, Integer batchNum, Integer startRow) throws Exception { //1.根据excel报表获取OPCPackage OPCPackage opcPackage = OPCPackage.open(inputStream); //2.创建XSSFReader XSSFReader reader = new XSSFReader(opcPackage); //3.获取SharedStringTable对象 SharedStringsTable table = reader.getSharedStringsTable(); //4.获取styleTable对象 StylesTable stylesTable = reader.getStylesTable(); //5.创建Sax的xmlReader对象 XMLReader xmlReader = XMLReaderFactory.createXMLReader(); XSSFSheetXMLHandler xmlHandler = new XSSFSheetXMLHandler(stylesTable, table, new SheetHandler(batchNum, startRow, consumer, entity), false); xmlReader.setContentHandler(xmlHandler); //7.逐行读取 XSSFReader.SheetIterator sheetIterator = (XSSFReader.SheetIterator) reader.getSheetsData(); while (sheetIterator.hasNext()) { //每一个sheet的流数据 InputStream stream = sheetIterator.next(); InputSource is = new InputSource(stream); xmlReader.parse(is); } }}
实体类:
为了方便数据的统一解析,在解析时不对实体具体字段进行赋值操作,而是在实体类中根据字段类型进行相应的转换并且赋值;
@Datapublic class User { private String name; private Integer agx; private String gender; private String address; private Double height; // 进行解析后的类型转换 public User(List<Object> values) { this.name = values.get(0) == null ? "" : values.get(0).toString(); this.agx = values.get(1) == null ? 0 : new Double(values.get(1).toString()).intValue(); this.gender = values.get(2) == null ? "" : values.get(0).toString(); this.address = values.get(3) == null ? "" : values.get(0).toString(); this.height = values.get(4) == null ? 0 : Double.parseDouble(values.get(4).toString()); }}
Controller:
/** * 数据导入 - 百万数据导入 - 大约20S,平均50000条/秒,当数据千条后使用会有更好的效果 * * @param file * @return */ @PostMapping("upload/bw") @ResponseBody public String uploadBaiWan(@RequestParam("file") MultipartFile file) throws Exception { InputStream inputStream = file.getInputStream(); // 注册事件处理器 Consumer<List<User>> consumer = uploadData -> importData(uploadData); XssfSheetHandler.handlerData(inputStream, consumer, User.class, null, 1); return "操作成功"; } // 接收Handler回传的数据 public void importData(List<User> list) { System.out.println("解析数量:" + list.size()); }