Java Excel数据导入方案实现及性能对比 --基于Apache poi框架

环境准备:

       jdk1.8、spring boot、swagger2、poi 3.17、EasyExcel 1.1.2-beta5 ……

目录:

       基于XSSF的UserModel 数据导入实现

       基于XSSF的 EventModel 数据导入实现

       基于阿里EasyExcel的数据导入实现(推荐使用)

       三种方案性能对比

       关于阿里EasyExcel的简单介绍

demo下载:

        https://download.csdn.net/download/weixin_42686388/10851053

 

整体介绍:

       项目代码导入到ide,通过spring boot启动,在浏览器输入地址http://localhost:8888/swagger-ui.html即可看到接口文档:

关于数据导出,请查看另外一篇文章:

Java Excel数据导出方案及性能优化策略 --基于Apache poi框架

本文仅实现XSSF模式的Excel数据解析,HSSH模式的就不在重复了,跟XSSF的基本类似;

代码结构:

maven依赖引入:

		<!-- POI -->
		<dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi</artifactId>
            <version>3.17</version>
        </dependency>
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
            <version>3.17</version>
        </dependency>
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml-schemas</artifactId>
            <version>3.17</version>
        </dependency>
		<!-- 阿里easyexcel -->
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>easyexcel</artifactId>
			<version>1.1.2-beta5</version>
		</dependency>

基于XSSF的UserModel 数据导入实现

简单介绍:UserModel实现起来比较简单易懂,适合新手使用,通过文件输入流创建Workbook,一次性把文件读入内存,构建一颗Dom树,并且在POI对Excel的抽象中,每一行,每一个单元格都是一个对象;当文件大,数据量多的时候对内存消耗非常大,极容易内存溢出(亲测中等配置计算机,3M左右文件,10w数据导入内存溢出);但是这种模式下,对一些特殊的数据处理比较好;适合数据量少的导入操作;本例支持将Excel解析为List<map>返回跟List<entity>返回;

controller层实现代码:

    @Autowired
	private ExcelDemoService service;

	@ApiOperation(value="apache poi XSSF import", notes="基于POI UserModel 模式的xlsx文件导入")
	@ApiImplicitParams({
		@ApiImplicitParam(name = "path", value = "数据导入文件路径,本地盘符", dataType = "String", paramType="query", defaultValue="D:\\CodeGenerator\\系统用户信息Easyexcel_1543827647428.xlsx")
	})
	@PostMapping("/importByPoiXSSFUserModel")
	public Object importByPoiXSSFUserModel(String path) {
		try {
			service.importByPoiXSSFUserModel(path);
		} catch (IOException e) {
			e.printStackTrace();
		}
		return Result.ok();
	}

service层代码实现:

    public Object importByPoiXSSFUserModel(String path) throws IOException {
        long begin = System.currentTimeMillis();
        ExcelImportUtils<UserEntity> util = new ExcelImportUtils<>(path);
        List<UserEntity> list = null;
        /*ExcelImportUtils<Map> util = new ExcelImportUtils<>(path);
		List<Map> list = null;*/

        Integer total = util.getRecordCount();
        int limit = 1000;
        int loop = total / limit;	//计算总数据量可以分几个批次执行
        if (total % limit > 0) {
            loop++;
        }
        //分批次执行
        for (int i = 0; i < loop; i++) {	
            int index = i * limit + 1; //加1 跳过标题行
            //bean方式返回数据
            list = util.importByPoiXSSFUserModel(index, limit, this.getHeadMap(), true, UserEntity.class);
            //map方式返回数据
            //list = util.importByPoiXSSFUserModel(index, limit, this.getHeadMap(), false, Map.class);

            //分批次写入数据库,或者后续其他的处理
            userMapper.batchInsert(list);
            list.clear();
            list = null;
        }
        //关闭文件输入流
        util.closeInputStream();
        long end = System.currentTimeMillis();
        logger.error("耗时:==>" + (end-begin) + " 毫秒;");
        logger.error("耗时:==>" + (end-begin)/1000 + " 秒");
        return null;
    }

util关键代码实现:

	/**
	 * 
	 * @param index 分批次执行,读取数据行
	 * @param limit 分批次执行,每个批次读取的记录条数
	 * @param headMap Excel表头跟实体类属性的关联map
	 * @param isBean 根据需要,true返回List<bean>, false返回List<Map>
	 * @param clazz 
	 * @return list
	 * @throws IOException
	 */
	@SuppressWarnings({ "unchecked", "deprecation" })
	public List<T> importByPoiXSSFUserModel(int index, int limit, Map<String, String> headMap, boolean isBean, Class<T> clazz) throws IOException {
		//标题行, 处理Excel中文标题行与实体类属性的对应关系
		XSSFRow fistRow =  sheet.getRow(0);	
		Map<Integer, String> columnMap = Maps.newHashMap();
		short minColIx = fistRow.getFirstCellNum();
		short maxColIx = fistRow.getLastCellNum();
		for (short colIx = minColIx; colIx < maxColIx; colIx++) {
			XSSFCell cell = fistRow.getCell(colIx);
			if (cell == null) {
				continue;
			}
			String head = cell.getStringCellValue();
			if(headMap.containsKey(head)) {
				columnMap.put((int) colIx, headMap.get(head));
			}
		}
		List result = Lists.newArrayList();
		//遍历行,读取结果集
		XSSFRow row = null;
		for (int j = 0; j < limit; j++) {	
			if(index > total) {
				break;
			}
			row = sheet.getRow(index);
			if(null == row) {
				continue;
			}

			if(isBean) {	//结果集返回实体类的集合
				T obj =  this.getResultByBean(columnMap, row, clazz);
				//logger.error(obj.toString());
				result.add(obj);
			} else {	//结果集返回Map的集合
				Map<String, String> rowMap = Maps.newHashMap();
				for (Entry<Integer, String> entry : columnMap.entrySet()) {	//遍历列
					Cell cell = row.getCell(entry.getKey());
					String property = entry.getValue();
					if(property.contains("Time")) {	//处理日期
						if(cell.getCellType() == Cell.CELL_TYPE_STRING) {	//判断日期字段的格式是时间还是字符串格式
							rowMap.put(property, cell.getStringCellValue());
						} else {
							Date value = cell.getDateCellValue();
							rowMap.put(property, DateUtil.format(value, DateUtil.PATTERN_DEFAULT));
						}
					} else {
						cell.setCellType(CellType.STRING);
						rowMap.put(property, cell.getStringCellValue());
					}
				}
				//logger.error(rowMap.toString());
				result.add(rowMap);
			}
			index ++;
		}
		return result;
	}

	/**
	 * 基于反射机制,将数据反射到对应的实体类
	 * 参考map2Bean方法
	 * @param columnMap
	 * @param row
	 * @param clazz
	 * @return
	 */
	@SuppressWarnings("deprecation")
	public T getResultByBean(Map<Integer, String> columnMap, XSSFRow row, Class<T> clazz) {
		T ob = null;
		try {
			ob = clazz.newInstance();//实例化对象
			for (Entry<Integer, String> entry : columnMap.entrySet()) {	//遍历列
				String key = entry.getValue();
				//获取实体属性
				Field property= clazz.getDeclaredField(key);

				//获取属性对应的set方法
				String setMethodName = "set" + key.substring(0, 1).toUpperCase() + key.substring(1);
				Method method = clazz.getDeclaredMethod(setMethodName, property.getType());
				//获取实体属性类型
				String propertyType =  property.getType().getTypeName();

				Cell cell = row.getCell(entry.getKey());

				//反射赋值
				try {
					if(propertyType.equals(Integer.class.getName())) {
						method.invoke(ob, cell.getNumericCellValue());
					} else if(propertyType.equals(Date.class.getName())) {
						if(cell.getCellType() == Cell.CELL_TYPE_STRING) {	//判断日期字段的格式是时间还是字符串格式
							String value = cell.getStringCellValue();
							//当前仅处理标准格式的日期时间数据,其他格式,需要自行处理
							value = value.replaceAll("/", "-");
							if(value.contains(" ")) {
								method.invoke(ob, DateUtil.format(value, DateUtil.PATTERN_DEFAULT));
							} else {
								method.invoke(ob, DateUtil.format(value, DateUtil.PATTERN_YYYYMMDD));
							}
						} else {
							method.invoke(ob, cell.getDateCellValue());
						}
					} else if(propertyType.equals(Short.class.getName())) {
						method.invoke(ob, cell.getNumericCellValue());

					} else {//默认字符串类型
						method.invoke(ob, cell.getStringCellValue());
					}
				} catch (Exception e) {
					logger.error("数据反射到实体类异常", e);
				}
			}
		}catch(Exception e){
			logger.error("数据反射到实体类异常", e);
			return null;
		}
		return ob;
	}

详细代码,请移步下载链接,下载demo查看。

基于XSSF的 EventModel 数据导入实现

简单介绍:EventModel是POI本身针对数据导入内存溢出问题出的解决方案,使用门槛稍微高一点,它是基于xml解析的,使用处理XML事件驱动的Sax推模型,它逐行扫描文档,一边扫描一边解析;由于应用程序只是在读取数据时检查数据,因此不需要将数据存储在内存中,这对于大型文档的解析是个巨大优势;所以在很大程度上解决了内存溢出的问题;本例支持将Excel解析为List<map>返回,List<entity>返回,自行参考Usermodel的反射案例实现即可;

controller层实现代码:

@ApiOperation(value="apache poi XSSF import", notes="基于POI EventModel 模式的xlsx文件导入")
	@ApiImplicitParams({
		@ApiImplicitParam(name = "path", value = "数据导入文件路径,本地盘符", dataType = "String", paramType="query", defaultValue="D:\\CodeGenerator\\系统用户信息Easyexcel_1543827647428.xlsx")
	})
	@PostMapping("/importByPoiXSSFEventModel")
	public Object importByPoiXSSFEventModel(String path) {
		try {
			service.importByPoiXSSFEventModel(path);
		} catch (IOException e) {
			e.printStackTrace();
		}
		return Result.ok();
	}

service层代码实现:

public Object importByPoiXSSFEventModel(String path) throws IOException {
		long begin = System.currentTimeMillis();
		try {
			ParseXlsxExcel excel = new ParseXlsxExcel(this.getHeadMap()); 
			excel.readOneSheet(path, "sheet"); 
			List<Map<String, String>> list = excel.getDataList();
			System.err.println("总记录条数:==>" + list.size());
			System.err.println("==>" + list.get(0).toString());

			Integer total = list.size();
			int limit = 1000;
			int loop = total / limit;	//计算总数据量可以分几个批次执行
			if (total % limit > 0) {
				loop++;
			}
			//分批次执行
			for (int i = 0; i < loop; i++) {
				Integer fromIndex = i * limit;
				Integer toIndex = (i + 1) * limit;
				if(toIndex > total) {
					toIndex = total;
				}
				//分批次写入数据库,或者后续其他的处理
				//userMapper.batchInsert(list.subList(fromIndex, toIndex));
			}
			list.clear();
			list = null;
		} catch (Exception e) {
			e.printStackTrace();
		}
		long end = System.currentTimeMillis();
		logger.error("耗时:==>" + (end-begin) + " 毫秒;");
		logger.error("耗时:==>" + (end-begin)/1000 + " 秒");
		return null;
	}

解析Handler代码实现:


import java.io.InputStream;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;

import javax.xml.parsers.ParserConfigurationException;

import org.apache.poi.xssf.eventusermodel.XSSFReader;
import org.apache.poi.xssf.eventusermodel.XSSFReader.SheetIterator;
import org.apache.poi.xssf.model.SharedStringsTable;
import org.apache.poi.xssf.usermodel.XSSFRichTextString;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.util.SAXHelper;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;

/**
 * POI事件驱动读取Excel文件的抽象类
 * 该类负责解析文件,将文件的数据转换为可以操作的数据行,基本上不用修改
 * 调用optRows回调函数处理行数据,将行数据转为为Map或者实体类
 * @author yuxue
 * @date 2018-12-05
 */
public abstract class ExcelAbstract extends DefaultHandler {
	
	private SharedStringsTable sst;
	private String lastContents;
	private boolean nextIsString;

	//记录当前执行到的行号
	private int curRow = 0;
	//记录当前执行到的单元格列名称,如 A1
	private String curCellName = "";

	/**
	 * 读取当前行的数据。key是单元格名称如A1,value是单元格中的值。如果单元格式空,则没有数据。
	 */
	private Map<String, String> rowValueMap = new HashMap<>();

	/**
	 * 处理单行数据的回调方法。
	 * 
	 * @param curRow 当前行号
	 * @param rowValueMap 当前行的值
	 * @throws SQLException
	 */
	public abstract void optRows(int curRow, Map<String, String> rowValueMap);

	/**
	 * 通过sheet下标,读取Excel指定sheet页的数据。
	 * @param filePath 文件路径
	 * @param sheetNum sheet页编号.从1开始。
	 * @throws Exception
	 */
	public void readOneSheet(String filePath, int sheetNum) throws Exception {
		if (filePath.endsWith(".xlsx")) { //处理excel2007文件
			OPCPackage pkg = OPCPackage.open(filePath);
			XSSFReader r = new XSSFReader(pkg);
			SharedStringsTable sst = r.getSharedStringsTable();
			XMLReader parser = getSheetParser(sst);

			// 根据 rId# 或 rSheet# 查找sheet
			InputStream sheet2 = r.getSheet("rId" + sheetNum);
			InputSource sheetSource = new InputSource(sheet2);
			parser.parse(sheetSource);
			sheet2.close();
		} else {
			throw new Exception("文件格式错误,文件扩展名只能是xlsx。");
		}
	}

	/**
	 * 通过sheet名称,读取Excel指定sheet页的数据。
	 * @param filePath 文件路径
	 * @param sheetName sheet页名称。
	 * @throws Exception
	 */
	public void readOneSheet(String filePath, String sheetName) throws Exception {
		if (filePath.endsWith(".xlsx")) { //处理excel2007文件
			OPCPackage pkg = OPCPackage.open(filePath);
			XSSFReader r = new XSSFReader(pkg);
			SharedStringsTable sst = r.getSharedStringsTable();

			XMLReader parser = getSheetParser(sst);

			// 根据名称查找sheet
			int sheetNum = 0;
			SheetIterator sheets = (SheetIterator)r.getSheetsData();
			while (sheets.hasNext()) {
				curRow = 0;
				sheetNum++;
				InputStream sheet = sheets.next();
				if(sheets.getSheetName().equals(sheetName)){
					sheet.close();
					break;
				}
				sheet.close();
			}
			
			// 根据 rId# 或 rSheet# 查找sheet
			InputStream sheet = r.getSheet("rId" + sheetNum);
			InputSource sheetSource = new InputSource(sheet);
			parser.parse(sheetSource);
			sheet.close();
			
		} else {
			throw new Exception("文件格式错误,文件扩展名只能是xlsx。");
		}
	}

	@Override
	public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException {
		// c => 单元格
		if (name.equals("c")) {
			// 如果下一个元素是 SST 的索引,则将nextIsString标记为true
			String cellType = attributes.getValue("t");
			if (cellType != null && cellType.equals("s")) {
				nextIsString = true;
			} else {
				nextIsString = false;
			}
		}
		// 置空
		lastContents = "";
		
		//记录当前读取单元格的名称
		String cellName = attributes.getValue("r");
		if (cellName != null && !cellName.isEmpty()) {
			curCellName = cellName;
		}
	}

	@Override
	public void endElement(String uri, String localName, String name) throws SAXException {
		// 根据SST的索引值的到单元格的真正要存储的字符串
		// 这时characters()方法可能会被调用多次
		if (nextIsString) {
			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();
			rowValueMap.put(curCellName, value);
		} else {
			// 如果标签名称为 row ,这说明已到行尾,调用 optRows() 方法
			if (name.equals("row")) {
				optRows(curRow, rowValueMap);
				rowValueMap.clear();
				curRow++;
			}
		}
	}

	public void characters(char[] ch, int start, int length) throws SAXException {
		// 得到单元格内容的值
		lastContents += new String(ch, start, length);
	}

	/**
	 * 获取单个sheet页的xml解析器。
	 * @param sst
	 * @return
	 * @throws SAXException
	 */
	private XMLReader getSheetParser(SharedStringsTable sst) throws SAXException {
		XMLReader parser = null;
		try {
			parser = SAXHelper.newXMLReader();
		} catch (ParserConfigurationException e) {
			e.printStackTrace();
		}
		this.sst = sst;
		parser.setContentHandler(this);
		return parser;
	}

}

回调代码实现:


import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.poi.hssf.usermodel.HSSFDateUtil;

/**
 * optRows回调函数的实现类
 * 负责将行数据转换为指定的数据,比如List<Entity> 或者List<Map>
 * 当前已经实现List<Map>的转换,反射为实体类,有兴趣的可以自己实现,可以参考ExcelImportUtils
 * @author yuxue
 * @date 2018-12-14
 */
public class ParseXlsxExcel extends ExcelAbstract {

	//提取列名称的正则表达式
	private static final String DISTILL_COLUMN_REG = "^([A-Z]{1,})";

	/**
	 * 读取excel的每一行记录。map的key是列号(A、B、C...), value是单元格的值。如果单元格是空,则没有值。
	 * ArrayList 存储上限: Integer.MAX_VALUE 2147483647
	 */
	private List<Map<String, String>> dataList = new ArrayList<>();

	/**
	 * <标题/列,实体属性/key> 关系
	 */
	private  Map<String, String> headMap;

	public ParseXlsxExcel(Map<String, String> headMap){
		this.headMap = headMap;
	}

	/**
	 * 处理行数据的回调方法
	 * 将行数据反射为实体对象、日期数字转换为字符串等操作 也在这里实现
	 */
	@Override
	public void optRows(int curRow, Map<String, String> rowValueMap) {
		if(curRow == 0) { //处理标题行
			this.changeMapKey(rowValueMap);
			return;
		}
		Map<String, String> dataMap = new HashMap<>();
		rowValueMap.forEach((k,v)->dataMap.put(removeNum(k), v));
		dataList.add(dataMap);
	}

	/**
	 * 第一行标题行: {A1=序号, B1=登录名称, C1=姓名, D1=性别, ...}
	 * 配置的标题对应实体属性,或者map的key, headMap:{序号=id, 登录名称=loginName, 姓名=userName, 性别=sex, ...}
	 * 转行为Excel列对应的实体属性或者key: {A=id, B=loginName, C=userName, D=sex, ...}
	 * @param rowValueMap
	 */
	private void changeMapKey(Map<String, String> rowValueMap) {
		rowValueMap.forEach((k, v) -> {
			String key = k.replaceAll("[\\d]", "");	//替换掉所有数字
			headMap.put(key, headMap.get(v.trim()));
			headMap.remove(v.trim());
		});
	}

	/**
	 * 日期数字转换为字符串。
	 * @param dateNum excel中存储日期的数字
	 * @return 格式化后的字符串形式
	 */
	public static String dateNum2Str(String dateNum) {
		Date date = HSSFDateUtil.getJavaDate(Double.parseDouble(dateNum));
		SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
		return formatter.format(date);
	}

	/**
	 * 删除单元格名称中的数字,只保留列号
	 * @param cellName 单元格名称。如:A1  列号。如:A
	 * @return	返回列号对应的实体属性或者key
	 */
	private String removeNum(String cellName) {
		Pattern pattern = Pattern.compile(DISTILL_COLUMN_REG);
		Matcher m = pattern.matcher(cellName);
		if (m.find()) {
			return headMap.get(m.group(1));
		}
		return "";
	}
	
	public List<Map<String, String>> getDataList() {
		return dataList;
	}

}

 

基于阿里EasyExcel的数据导入实现(推荐使用)

简单介绍:EasyExcel是阿里出品的,快速、简单避免OOM的java处理Excel工具;重写了poi对07版Excel的解析,能够原本一个3M的excel用POI sax依然需要100M左右内存降低到KB级别,并且再大的excel不会出现内存溢出,03版依赖POI的sax模式;本例支持将Excel解析为List<entity>返回;

controller层实现代码:

@ApiOperation(value="alibaba easyExcel import", notes="alibaba easyExcel数据导入")
	@ApiImplicitParams({
		@ApiImplicitParam(name = "path", value = "数据导入文件路径,本地盘符", dataType = "String", paramType="query", defaultValue="D:\\CodeGenerator\\系统用户信息Easyexcel_1543827647428.xlsx")
	})
	@PostMapping("/importByalbabaEasyExcel")
	public Object importByalbabaEasyExcel(String path) {
		service.importByalbabaEasyExcel(path);
		return Result.ok();
	}

service层代码实现:

public Object importByalbabaEasyExcel(String path) {
        long begin = System.currentTimeMillis();
        try {
            InputStream inputStream = new BufferedInputStream(new FileInputStream(path));
            //回调函数,用于处理读取Excel生成的数据
            ExcelListener excelListener = new ExcelListener();
            //实体类不支持lombok注解生成的getter和setter方法
            //可能的原因是,导出代码编译在lombok之前,导致获取的class没有setter和setter方法  
            //@注解的先后执行问题,具体问题我就没有去分析了,使用spring boot的@注解, 经常会遇到这种问题
            EasyExcelFactory.readBySax(inputStream, new Sheet(1, 1, UserEntity.class), excelListener);
            
            inputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        long end = System.currentTimeMillis();
        logger.error("耗时:==>" + (end-begin) + " 毫秒;");
        logger.error("耗时:==>" + (end-begin)/1000 + " 秒");
        return null;
    }

实体类:

public class UserEntity extends BaseRowModel implements Serializable {

	private static final long serialVersionUID = 1L;
	
	@ExcelProperty(value = { "序号" }, index = 0) // 阿里 easyExcel数据导出
	private Integer id;

	@ExcelProperty(value = { "登录名称" }, index = 1)
	private String loginName;

	private String loginPasswd;

	@ExcelProperty(value = { "姓名" }, index = 2)
	private String userName;

	@ExcelProperty(value = { "性别" }, index = 3)
	private Integer sex;

	private String salt;

	@ExcelProperty(value = { "状态" }, index = 4)
	private Integer userStatus;

	@ExcelProperty(value = { "创建时间" }, index = 5)
	private Date createTime;
    
......
}

回调Listener代码实现:


import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;

import java.util.ArrayList;
import java.util.List;

public class ExcelListener extends AnalysisEventListener {

    private List<Object>  data = new ArrayList<Object>();

    @Override
    public void invoke(Object object, AnalysisContext context) {
        
        if(data.size()<=100){	//分批次处理数据
            data.add(object);
        }else {
            doSomething();
            data = new ArrayList<Object>();
        }
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
    	System.out.println(context.getCurrentSheet());
        //doSomething();
    }
    
    public void doSomething(){
        /*for (Object o:data) {
            System.err.println(o);
        }*/
    }
}

 

三种方案性能对比

1W数据量对比: 13个字段 ,启动内存占45M左右

方案 耗时 CPU 内存
UserModel 1754 毫秒   38.4%左右  60M左右
EventModel 739 毫秒  12.3%左右  55M左右
EasyExcel 918 毫秒  10%左右  55M左右

10w数据量对比:13个字段 ,启动内存占45M左右

方案 耗时 CPU 内存
UserModel 失败    内存溢出
EventModel 10689ms   17%左右  58M左右
EasyExcel 9753ms   17%左右  58M左右

100W数据量对比:13个字段 ,启动内存占45M左右;文件大小61M,全部都失败了,估计跟我的环境有关

方案 耗时 CPU 内存
UserModel 失败    内存溢出
EventModel 失败     内存溢出
EasyExcel 失败     内存溢出

    User Model的缺点是一次性将文件读入内存,构建一颗Dom树.并且在POI对Excel的抽象中,每一行,每一个单元格都是一个对象.当文件大,数据量多的时候对内存的占用可想而知.

    Event Model使用的方式是边读取边解析,并且不会将这些数据封装成Row,Cell这样的对象.而都只是普通的数字或者是字符串.并且这些解析出来的对象是不需要一直驻留在内存中,而是解析完使用后就可以回收

 

关于阿里EasyExcel简单介绍

    poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。easyexcel重写了poi对07版Excel的解析,能够让原本一个3M的excel用POI sax依然需要100M左右内存降低到KB级别,并且再大的excel不会出现内存溢出,03版依赖POI的sax模式。在上层做了模型转换的封装,让使用者更加简单方便;

    对比三种方案,阿里出品,确实是精品,使用简单,容易上手;当然,具体使用什么方案,还是要根据自己的业务来决策吧。阿里EasyExcel的github官方地址:https://github.com/alibaba/easyexcel

下图是1.1.2-beta5版本对应的API截图:

发布了15 篇原创文章 · 获赞 40 · 访问量 15万+

猜你喜欢

转载自blog.csdn.net/weixin_42686388/article/details/85007814