在项目开发过程导出Excel为常用功能,之前的一篇Java导出超大Excel文件,防止内存溢出已经解决了Excel写入层面时的内存问题,但数据库查询层面,仍存在由于默认的mybatis查询是将所有数据都查询到本地内存,因此仍有可能会导致内存溢出,因此本文再详细介绍记录通过mybatis的ResultHander进行流式查询读取来完全解决excel的大量数据导出内存溢出问题。
一.先批量插入测试数据
1.建表,包含2个字段username,age
CREATE TABLE `t_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '姓名',
`age` int(11) NOT NULL COMMENT '年龄',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uk_code_key`(`username`, `age`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
2.UserMapper.xml
<insert id="batchInsert">
insert into t_user(username,age)
values
<foreach collection="list" item="item" separator=",">
(#{item.username},#{item.age})
</foreach>
</insert>
3.UserMapper.java
int batchInsert(List<User> list);
4.UserService.java & UserServiceImpl.java
int batchInsert(List<User> list);
5.UserController.java中编写测试代码,写入100万行数据
//1.插入1000101行测试数据
//http://localhost:8080/test/user/batchInsert
@RequestMapping(value="/batchInsert", method = RequestMethod.GET)
public String batchInsert(){
logger.info("method starting...");
long startTime = System.currentTimeMillis();
//每次批量插入2000条记录,提高插入效率
int batchSize = 2000;
List<User> list = new LinkedList<User>();
for(int i=0;i<1000101;i++){
User user = new User();
user.setUsername("name"+ i);
user.setAge(18);
list.add(user);
if(list.size()>0 && list.size() % batchSize == 0) {
userService.batchInsert(list);
logger.info("has batchInsert size: {}", i);
list.clear();//清除list
}
}
long endTime = System.currentTimeMillis();
logger.info("method finished,total spend time: {} ms.",(endTime-startTime));
return "batchInsert";
}
二、ResultHandler流式查询导出
ResultHandler接口可以用于进行流式查询(
即一行一行从数据库中读取处理,因此不会占用本地内存),本文的核心就是通过调用mapper的方法,传入一个ResultHandler,然后在实现的方法中读取数据,然后一行一行处理。
1.在UserMapper.xml中配置
其中的resultSetType为FORWARD_ONLY,fetchSize为-2147483648
<sql id="listSql">
select id,username,age from t_user
</sql>
<select id="export" parameterType="user" resultType="user" resultSetType="FORWARD_ONLY" fetchSize="-2147483648">
<include refid="listSql" />
</select>
2.UserMapper.java中编写方法
/**导出,mapper的方法需要是void返回,并且参数中含ResultHandler(流式查询遍历的条件),这里我没加参数,可以加上你的条件参数*/
public void export(ResultHandler<User> resultHandler);
重要注意点:要满足流式查询,需要方法返回值为void,并且方法中有ResultHandler类型的参数。在mybatis源码中的MapperMethod.java中也能看到对应的代码判断如下:
3.自定义一个ExcelResultHandler,提供给所有导出的代码共用
此ResultHandler实现了excel的导出,并遍历mapper查询数据,一行一行写入excel,节省内存,可以在导出的业务代码进行使用,具有通用性,只需要new出对象然后调用相应的方法。
package cn.gzsendi.modules.framework.utils;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import javax.servlet.http.HttpServletResponse;
import org.apache.ibatis.session.ResultContext;
import org.apache.ibatis.session.ResultHandler;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import cn.gzsendi.modules.framework.reflect.Reflector;
import cn.gzsendi.modules.framework.reflect.reflectasm.MethodAccessor;
public abstract class ExcelResultHandler<T> implements ResultHandler<T>{
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private AtomicInteger currentRowNumber = new AtomicInteger(0);//记录当前excel行号,从0开始
private Sheet sheet = null;
private List<String> headerArray ; //excel表头
private List<String> fieldArray ; //对应的字段
//定义totalCellNumber变量,
private int totalCellNumber;
//定义导出成zip格式的还是原始的xlsx格式
private boolean isExportZip = true;
private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//定义要导出的excel文件名,不带xlsx后缀,默认为uuID,也可以通过构造函数传进来进行改变。
private String exportFileName = UUID.randomUUID().toString().replace("-", "");
public ExcelResultHandler(List<String> headerArray,List<String> fieldArray){
this.headerArray = headerArray;
this.fieldArray = fieldArray;
this.totalCellNumber = headerArray.size();
}
public ExcelResultHandler(List<String> headerArray,List<String> fieldArray,boolean isExportZip){
this(headerArray,fieldArray);
this.isExportZip = isExportZip;
}
public ExcelResultHandler(List<String> headerArray,List<String> fieldArray,String exportFileName){
this(headerArray,fieldArray);
this.exportFileName = exportFileName;
}
public ExcelResultHandler(List<String> headerArray,List<String> fieldArray,String exportFileName,boolean isExportZip){
this(headerArray,fieldArray,exportFileName);
this.isExportZip = isExportZip;
}
//出象方法,提供给子类进行实现,遍历写入数据到excel
public abstract void tryFetchDataAndWriteToExcel();
public void handleResult(ResultContext<? extends T> resultContext) {
//获取数据,并回调ExportExcelUtils中的方法进行数据写入到excel,固定写法即可,不需要修改
Object aRowData = resultContext.getResultObject();
callBackWriteRowdataToExcel(aRowData);
}
/**导出*/
public void startExportExcel() {
HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
ZipOutputStream zos = null;
OutputStream os = null;
try {
logger.info("--------->>>>写入Excel开始.." );
//写入文件
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment;filename=" + new String((exportFileName+".zip").replaceAll(" ", "").getBytes("utf-8"),"iso8859-1"));
os = new BufferedOutputStream(response.getOutputStream());
//如果设置成了导出成Zip,格式加上三行以下代码进行Zip的处理
if(isExportZip){
zos = new ZipOutputStream(os);
ZipEntry zipEntry = new ZipEntry(new String((exportFileName+".xlsx").replaceAll(" ", "")));
zos.putNextEntry(zipEntry);
}
SXSSFWorkbook wb = new SXSSFWorkbook();//默认100行,超100行将写入临时文件
wb.setCompressTempFiles(false); //是否压缩临时文件,否则写入速度更快,但更占磁盘,但程序最后是会将临时文件删掉的
sheet = wb.createSheet("Sheet 1");
//写入表头,Rows从0开始.
Row row = sheet.createRow(0);
for (int cellNumber = 0; cellNumber < totalCellNumber; cellNumber++) {
Cell cell = row.createCell(cellNumber);
cell.setCellValue(headerArray.get(cellNumber)); //写入表头数据
}
//写入数据
/****************************/
//调用具体的实现子类的代码,尝试获取数据进行遍历并写入excel
tryFetchDataAndWriteToExcel();
//最后打印一下最终写入的行数
logger.info("--------->>>> write to excel size now is {}", currentRowNumber.get() );
//Write excel to a file
if(isExportZip){
wb.write(zos);
}else{
wb.write(os);
}
if (wb != null) {
wb.dispose();// 删除临时文件,很重要,否则磁盘可能会被写满
}
wb.close();
/****************************/
logger.info("--------->>>>全部数据写入Excel完成.." );
} catch (Exception e) {
logger.error("error",e);
} finally {
//关闭资源
if(isExportZip){
try {if(zos!=null) zos.close();} catch (IOException e1) {logger.error("error",e1); }
}else{
try {if(os!=null) os.close();} catch (IOException e1) {logger.error("error",e1); }
}
}
}
//写入一行数据到excel中,提供给ResultHandler中遍历时进行回调调用
@SuppressWarnings("rawtypes")
public void callBackWriteRowdataToExcel(Object aRowData) {
//反射获取值并设置到excel的中cell列中
MethodAccessor methodAccessor = Reflector.getMethodAccessor(aRowData.getClass());
//先将行号增加
currentRowNumber.incrementAndGet();
//创建excel中新的一行
Row row = sheet.createRow(currentRowNumber.get());
for (int cellNumber = 0; cellNumber < totalCellNumber; cellNumber++) {
//aRowData为map时,要特殊处理进行获取。不能通过methodAccessor反射调用.
Object value = null;
if(aRowData instanceof Map){
value = ((Map)aRowData).get(fieldArray.get(cellNumber));
}else {
value = methodAccessor.getFieldValue(aRowData, fieldArray.get(cellNumber));
}
Cell cell = row.createCell(cellNumber);
//date类型默认转换string格式化日期
if (value!=null && value instanceof Date){
cell.setCellValue(sdf.format(value));//
}else {
cell.setCellValue(value==null?"":value.toString());//写入数据
}
}
//每写入5000条就打印一下
if(currentRowNumber.get() % 5000 == 0 ){
logger.info("--------->>>> write to excel size now is {}", currentRowNumber.get() );
}
}
}
4.在UserServiceImpl实现类中进行ExcelResultHandler的调用并写入excel,如下:
/**导出*/
public void export() {
//定义导出的的表头,以及每个表头字段对应的对象变量名
List<String> headerArray = Arrays.asList("姓名","年龄");
List<String> fieldArray = Arrays.asList("username","age");
//定义要导出的excel的文件名,不带"xlsx"后缀。
String exportExcelFileName = "文件测试";
//每次导出new一个handler对象,将headerArray,fieldArray,exportExcelFileName传递进去。
ExcelResultHandler<User> handler = new ExcelResultHandler<User>(headerArray,fieldArray,exportExcelFileName) {
public void tryFetchDataAndWriteToExcel() {
//这里的this,指的就是ExcelResultHandler<User> handler这个对象,在这里写mapper调用获取数据的调用
userMapper.export(this);
}
};
//真正调用excel的导出开始,在方法中exportExcel会调用写excel表头,
//然后调用tryFetchDataAndWriteToExcel,进行驱动调用userMapper的方法,然后遍历结果集,一条一条写入excel,最后关闭盯应的流资源。
handler.startExportExcel();
}
说明:
UserServiceImpl类的导出代码中,只需要new一个ExcelResultHandler,然后实现其抽象方法tryFetchDataAndWriteToExcel(),在tryFetchDataAndWriteToExcel方法中进行mapper的方法调用,传递一个ResultHander对象,如上面代码中的userMapper.export(this),
new完ExcelResultHandler对象时,导出还没有开始,执行handler.startExportExcel()才进行真正的导出功能,先拿到输出流,然后设置好excel的表头,并写入表头,然后调用tryFetchDataAndWriteToExcel方法,在tryFetchDataAndWriteToExcel方法中会调用mybatis的mapper的方法,在mybatis的mapper的方法中遍历所有的数据,然后一行一行写入excel,最终关闭资源流等,如下:`
public void handleResult(ResultContext<? extends T> resultContext) {
//获取数据,并回调ExportExcelUtils中的方法进行数据写入到excel,固定写法即可,不需要修改
Object aRowData = resultContext.getResultObject();
callBackWriteRowdataToExcel(aRowData);
}
4.测试结果截图如下:
输入http://localhost:8080/test/user/export进行导出测试
三、源码demo下载
详细源码附件如下:可直接下载进行测试
github: github.com/jxlhljh/spr… gitee: gitee.com/jxlhljh/spr…