导读


由于项目需要,需要在页面点击导出按钮,然后根据条件进行查询数据,并导出。最开始的时候想着使用是前端的xlsxJs来实现前端Excel数据导出,后面想了想这种办法行不通,原因很简单,假如后台数据几百万的话,那么前端会瞬间崩掉,这个xlsxJS感觉适合数据量少的导出Excel文件,数据量大的话可能也有,但是现在没时间去尝试了,以后吧。So,这显然是行不通的,网上找了一堆后,有使用poi还是阿里的easyExcel亦或者easypoi,可能看着poi好感度更高吧,就选择它了,正好网上也有现成的,直接拿来使用了。

使用


添加依赖

在pom.xml文件添加以下依赖:

  1. <!-- 文件导出-->
  2. <dependency>
  3. <groupId>org.apache.poi</groupId>
  4. <artifactId>poi</artifactId>
  5. <version>3.17</version>
  6. </dependency>
  7. <dependency>
  8. <groupId>org.apache.poi</groupId>
  9. <artifactId>poi-ooxml</artifactId>
  10. <version>3.17</version>
  11. </dependency>

工具类

在相应的包下添加以下几个文件:

WriteExcelDataDelegated委托类

  1. import org.apache.poi.xssf.streaming.SXSSFSheet;
  2. /**
  3. * @Author admin
  4. * @create 2019/11/19 17:45
  5. * @About EXCEL写数据委托类
  6. */
  7. public interface WriteExcelDataDelegated {
  8. /**
  9. * EXCEL写数据委托类 针对不同的情况自行实现
  10. *
  11. * @param eachSheet 指定SHEET
  12. * @param startRowCount 开始行
  13. * @param endRowCount 结束行
  14. * @param currentPage 分批查询开始页
  15. * @param pageSize 分批查询数据量
  16. * @throws Exception
  17. */
  18. public abstract void writeExcelData(SXSSFSheet eachSheet, Integer startRowCount, Integer endRowCount, Integer currentPage, Integer pageSize) throws Exception;
  19. }

ExcelConstant常量

  1. /**
  2. * @Author admin
  3. * @create 2019/11/19 17:42
  4. * @About EXCEL常量类
  5. */
  6. public class ExcelConstant {
  7. /**
  8. * 每个sheet存储的记录数 10W
  9. */
  10. public static final Integer PER_SHEET_ROW_COUNT = 100000;
  11. /**
  12. * 每次向EXCEL写入的记录数(查询每页数据大小) 2W
  13. */
  14. public static final Integer PER_WRITE_ROW_COUNT = 20000;
  15. /**
  16. * 每个sheet的写入次数 5
  17. */
  18. public static final Integer PER_SHEET_WRITE_COUNT = PER_SHEET_ROW_COUNT / PER_WRITE_ROW_COUNT;
  19. }

PoiUtil导出数据

  1. import com.demos.excel.ExcelConstant;
  2. import com.demos.excel.WriteExcelDataDelegated;
  3. import org.apache.poi.xssf.streaming.SXSSFCell;
  4. import org.apache.poi.xssf.streaming.SXSSFRow;
  5. import org.apache.poi.xssf.streaming.SXSSFSheet;
  6. import org.apache.poi.xssf.streaming.SXSSFWorkbook;
  7. import org.slf4j.Logger;
  8. import org.slf4j.LoggerFactory;
  9. import javax.servlet.http.HttpServletResponse;
  10. import java.io.FileOutputStream;
  11. import java.io.IOException;
  12. import java.io.OutputStream;
  13. import java.util.Date;
  14. /**
  15. * @Author admin
  16. * @create 2019/11/19 17:19
  17. * @About POI导出工具类
  18. */
  19. public class PoiUtil {
  20. private final static Logger logger = LoggerFactory.getLogger(PoiUtil.class);
  21. /**
  22. * 初始化EXCEL(sheet个数和标题)
  23. *
  24. * @param totalRowCount 总记录数
  25. * @param titles 标题集合
  26. * @return XSSFWorkbook对象
  27. */
  28. public static SXSSFWorkbook initExcel(Integer totalRowCount, String[] titles) {
  29. // 在内存当中保持 100 行 , 超过的数据放到硬盘中在内存当中保持 100 行 , 超过的数据放到硬盘中
  30. SXSSFWorkbook wb = new SXSSFWorkbook(100);
  31. Integer sheetCount = ((totalRowCount % ExcelConstant.PER_SHEET_ROW_COUNT == 0) ?
  32. (totalRowCount / ExcelConstant.PER_SHEET_ROW_COUNT) : (totalRowCount / ExcelConstant.PER_SHEET_ROW_COUNT + 1));
  33. // 根据总记录数创建sheet并分配标题
  34. for (int i = 0; i < sheetCount; i++) {
  35. SXSSFSheet sheet = wb.createSheet("sheet" + (i + 1));
  36. SXSSFRow headRow = sheet.createRow(0);
  37. for (int j = 0; j < titles.length; j++) {
  38. SXSSFCell headRowCell = headRow.createCell(j);
  39. headRowCell.setCellValue(titles[j]);
  40. }
  41. }
  42. return wb;
  43. }
  44. /**
  45. * 下载EXCEL到本地指定的文件夹
  46. *
  47. * @param wb EXCEL对象SXSSFWorkbook
  48. * @param exportPath 导出路径
  49. */
  50. public static void downLoadExcelToLocalPath(SXSSFWorkbook wb, String exportPath) {
  51. FileOutputStream fops = null;
  52. try {
  53. fops = new FileOutputStream(exportPath);
  54. wb.write(fops);
  55. } catch (Exception e) {
  56. e.printStackTrace();
  57. } finally {
  58. if (null != wb) {
  59. try {
  60. wb.dispose();
  61. } catch (Exception e) {
  62. e.printStackTrace();
  63. }
  64. }
  65. if (null != fops) {
  66. try {
  67. fops.close();
  68. } catch (Exception e) {
  69. e.printStackTrace();
  70. }
  71. }
  72. }
  73. }
  74. /**
  75. * 下载EXCEL到浏览器
  76. *
  77. * @param wb EXCEL对象XSSFWorkbook
  78. * @param response
  79. * @param fileName 文件名称
  80. * @throws IOException
  81. */
  82. public static void downLoadExcelToWebsite(SXSSFWorkbook wb, HttpServletResponse response, String fileName) throws IOException {
  83. response.setHeader("Content-disposition", "attachment; filename="
  84. + new String((fileName + ".xlsx").getBytes("utf-8"), "ISO8859-1"));//设置下载的文件名
  85. OutputStream outputStream = null;
  86. try {
  87. outputStream = response.getOutputStream();
  88. wb.write(outputStream);
  89. } catch (Exception e) {
  90. e.printStackTrace();
  91. } finally {
  92. if (null != wb) {
  93. try {
  94. wb.dispose();
  95. } catch (Exception e) {
  96. e.printStackTrace();
  97. }
  98. }
  99. if (null != outputStream) {
  100. try {
  101. outputStream.close();
  102. } catch (Exception e) {
  103. e.printStackTrace();
  104. }
  105. }
  106. }
  107. }
  108. /**
  109. * 导出Excel到本地指定路径
  110. *
  111. * @param totalRowCount 总记录数
  112. * @param titles 标题
  113. * @param exportPath 导出路径
  114. * @param writeExcelDataDelegated 向EXCEL写数据/处理格式的委托类 自行实现
  115. * @throws Exception
  116. */
  117. public static final void exportExcelToLocalPath(Integer totalRowCount, String[] titles, String exportPath, WriteExcelDataDelegated writeExcelDataDelegated) throws Exception {
  118. logger.info("开始导出:" + DateUtil.formatDate(new Date(), DateUtil.YYYY_MM_DD_HH_MM_SS));
  119. // 初始化EXCEL
  120. SXSSFWorkbook wb = PoiUtil.initExcel(totalRowCount, titles);
  121. // 调用委托类分批写数据
  122. int sheetCount = wb.getNumberOfSheets();
  123. for (int i = 0; i < sheetCount; i++) {
  124. SXSSFSheet eachSheet = wb.getSheetAt(i);
  125. for (int j = 1; j <= ExcelConstant.PER_SHEET_WRITE_COUNT; j++) {
  126. int currentPage = i * ExcelConstant.PER_SHEET_WRITE_COUNT + j;
  127. int pageSize = ExcelConstant.PER_WRITE_ROW_COUNT;
  128. int startRowCount = (j - 1) * ExcelConstant.PER_WRITE_ROW_COUNT + 1;
  129. int endRowCount = startRowCount + pageSize - 1;
  130. writeExcelDataDelegated.writeExcelData(eachSheet, startRowCount, endRowCount, currentPage, pageSize);
  131. }
  132. }
  133. // 下载EXCEL
  134. PoiUtil.downLoadExcelToLocalPath(wb, exportPath);
  135. logger.info("导出完成:" + DateUtil.formatDate(new Date(), DateUtil.YYYY_MM_DD_HH_MM_SS));
  136. }
  137. /**
  138. * 导出Excel到浏览器
  139. * 亲测可用
  140. * @param response
  141. * @param totalRowCount 总记录数
  142. * @param fileName 文件名称
  143. * @param titles 标题
  144. * @param writeExcelDataDelegated 向EXCEL写数据/处理格式的委托类 自行实现
  145. * @throws Exception
  146. */
  147. public static final void exportExcelToWebsite(HttpServletResponse response, Integer totalRowCount, String fileName, String[] titles, WriteExcelDataDelegated writeExcelDataDelegated) throws Exception {
  148. logger.info("开始导出:" + DateUtil.formatDate(new Date(), DateUtil.YYYY_MM_DD_HH_MM_SS));
  149. // 初始化EXCEL
  150. SXSSFWorkbook wb = PoiUtil.initExcel(totalRowCount, titles);
  151. // 调用委托类分批写数据
  152. //该算法需要优化,后面会查询多余的数据,但是不会在页面显示
  153. int sheetCount = wb.getNumberOfSheets();
  154. for (int i = 0; i < sheetCount; i++) {
  155. SXSSFSheet eachSheet = wb.getSheetAt(i);
  156. for (int j = 1; j <= ExcelConstant.PER_SHEET_WRITE_COUNT; j++) {
  157. int pageSize = ExcelConstant.PER_WRITE_ROW_COUNT;
  158. int currentPage = (i * ExcelConstant.PER_SHEET_WRITE_COUNT + j - 1) * pageSize;
  159. int startRowCount = (j - 1) * ExcelConstant.PER_WRITE_ROW_COUNT + 1;
  160. int endRowCount = startRowCount + pageSize - 1 > ExcelConstant.PER_SHEET_ROW_COUNT ? ExcelConstant.PER_SHEET_ROW_COUNT : startRowCount + pageSize - 1;
  161. writeExcelDataDelegated.writeExcelData(eachSheet, startRowCount, endRowCount, currentPage, pageSize);
  162. }
  163. }
  164. // 下载EXCEL
  165. PoiUtil.downLoadExcelToWebsite(wb, response, fileName);
  166. logger.info("导出完成:" + DateUtil.formatDate(new Date(), DateUtil.YYYY_MM_DD_HH_MM_SS));
  167. }
  168. }

DateUtil日期处理

  1. import java.text.SimpleDateFormat;
  2. import java.util.Date;
  3. /**
  4. * @Author admin
  5. * @create 2019/11/19 17:39
  6. * @About 日期工具类
  7. */
  8. public class DateUtil {
  9. public static final String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss";
  10. /**
  11. * 将日期转换为字符串
  12. *
  13. * @param date DATE日期
  14. * @param format 转换格式
  15. * @return 字符串日期
  16. */
  17. public static String formatDate(Date date, String format) {
  18. SimpleDateFormat simpleDateFormat = new SimpleDateFormat(format);
  19. return simpleDateFormat.format(date);
  20. }
  21. }

具体使用

Controller层

  • 首先查询条数
  1. /**
  2. * 根据参数导出系统表中的字段
  3. * 步骤一 先查询是否有数据集,如果没有,不进行下一步请求
  4. *
  5. * @param archiveErrorSimpleQuery 参数(total,page,size等)
  6. * @return
  7. */
  8. @GetMapping("/getDataJoinExports")
  9. public ResponseMsg getDataJoinExports(ArchiveErrorSimpleQuery archiveErrorSimpleQuery, HttpServletResponse resp) {
  10. ResponseMsg msg = new ResponseMsg();
  11. //权限校验...略
  12. //封装数据
  13. List<ArchivrErrorSimpleExportVO> archivrErrorSimpleExportVOS = null;
  14. try {
  15. //查询总数量
  16. archiveErrorSimpleQuery.setPage((archiveErrorSimpleQuery.getPage() - 1) * archiveErrorSimpleQuery.getSize());
  17. int totalCount = anomalyAnalysisService.countDataJoinByQuery(archiveErrorSimpleQuery);
  18. msg.setTotal(totalCount);
  19. if (totalCount > 0) {
  20. msg.setSuccess(true);
  21. msg.setData("导出系统异常明细数据");
  22. }
  23. } catch (Exception e) {
  24. msg.setMsg(e.getMessage());
  25. }
  26. return msg;
  27. }
  • 如果总条数大于0,再进行数据导出

    1. /**
    2. * 根据参数导出系统表中的字段
    3. * 步骤二 步骤一查询出数据集后,直接查询数据集
    4. *
    5. * @param archiveErrorSimpleQuery 参数(total,page,size等)
    6. * @return
    7. */
    8. @GetMapping("/getDataJoinExportList")
    9. public ResponseMsg getDataJoinExportList(ArchiveErrorSimpleQuery archiveErrorSimpleQuery, HttpServletResponse resp) {
    10. ResponseMsg msg = new ResponseMsg();
    11. //权限校验...略
    12. //封装数据
    13. List<ArchivrErrorSimpleExportVO> archivrErrorSimpleExportVOS = null;
    14. try {
    15. //根据条件查询数据集合
    16. archivrErrorSimpleExportVOS = anomalyAnalysisService.getDataJoinExportList(archiveErrorSimpleQuery,resp);
    17. } catch (Exception e) {
    18. msg.setMsg(e.getMessage());
    19. }
    20. msg.setSuccess(archivrErrorSimpleExportVOS != null);
    21. msg.setData("导出系统异常明细数据");
    22. return msg;
    23. }

Service层

  1. /**
  2. * 满足条件的total总数量
  3. *
  4. * @param archiveErrorSimpleQuery
  5. * @return
  6. */
  7. int countDataJoinByQuery(ArchiveErrorSimpleQuery archiveErrorSimpleQuery);
  8. /**
  9. * 满足条件的列表
  10. * @param archiveErrorSimpleQuery
  11. * @param resp
  12. * @return
  13. */
  14. List<ArchivrErrorSimpleExportVO> getDataJoinExportList(ArchiveErrorSimpleQuery archiveErrorSimpleQuery, HttpServletResponse resp) throws Exception;

Service实现层

  1. @Override
  2. public int countDataJoinByQuery(ArchiveErrorSimpleQuery archiveErrorSimpleQuery) {
  3. return archiveErrorSimpleMapper.countDataJoinByQuery(archiveErrorSimpleQuery);
  4. }
  5. @Override
  6. public List<ArchivrErrorSimpleExportVO> getDataJoinExportList(ArchiveErrorSimpleQuery archiveErrorSimpleQuery, HttpServletResponse resp) throws Exception {
  7. // 总记录数
  8. Integer totalRowCount = archiveErrorSimpleQuery.getTotal();
  9. // 导出EXCEL文件名称
  10. String filaName = "标题透";
  11. String[] titles = {"列1", "列2", "列3", "列4",...};
  12. // 开始导入
  13. PoiUtil.exportExcelToWebsite(resp, totalRowCount, filaName, titles, new WriteExcelDataDelegated() {
  14. @Override
  15. public void writeExcelData(SXSSFSheet eachSheet, Integer startRowCount, Integer endRowCount, Integer currentPage, Integer pageSize) throws Exception {
  16. //绑定数据
  17. archiveErrorSimpleQuery.setPage(currentPage); //设置limit的page
  18. archiveErrorSimpleQuery.setSize(pageSize); //设置limit的size
  19. //查询列表
  20. List<ArchivrErrorSimpleExportVO> exportVOList = archiveErrorSimpleMapper.getDataJoinExportList(archiveErrorSimpleQuery);
  21. if (!CollectionUtils.isEmpty(exportVOList)) {
  22. for (int i = startRowCount; i <= endRowCount; i++) {
  23. SXSSFRow eachDataRow = eachSheet.createRow(i);
  24. if ((i - startRowCount) < exportVOList.size()) {
  25. ArchivrErrorSimpleExportVO eachUserVO = exportVOList.get(i - startRowCount);
  26. eachDataRow.createCell(0).setCellValue(eachUserVO.getType() == null ? "" : eachUserVO.getType());
  27. eachDataRow.createCell(1).setCellValue(eachUserVO.getArchiveId() == null ? "" : eachUserVO.getArchiveId());
  28. eachDataRow.createCell(2).setCellValue(eachUserVO.getBelongSystem() == null ? "" : eachUserVO.getBelongSystem());
  29. eachDataRow.createCell(3).setCellValue(eachUserVO.getErrorCode() == null ? "" : eachUserVO.getErrorCode());
  30. }
  31. }
  32. }
  33. }
  34. });
  35. return null;
  36. }

Mapper层

  1. @Mapper
  2. @Component
  3. public interface demoMapper {
  4. int countDataJoinByQuery(@Param("simpleQuery") ArchiveErrorSimpleQuery simpleQuery);
  5. List<ArchivrErrorSimpleExportVO> getDataJoinExportList(@Param("simpleQuery") ArchiveErrorSimpleQuery simpleQuery);
  6. }

Mapper.xml层

查询列表:

  1. <select id="getDataJoinExportList"
  2. resultType="ExportVO"> //返回的VO类
  3. select
  4. <include refid="aesExportColumn"></include>
  5. from user
  6. <trim prefix="WHERE" prefixOverrides="AND |OR">
  7. <if test="simpleQuery.name!=null and simpleQuery.name!=''">
  8. and name = #{simpleQuery.name}
  9. </if>
  10. <!-- 类似上面的参数条件-->
  11. </trim>
  12. order by id
  13. limit
  14. <if test="simpleQuery.page != null and simpleQuery.page != '' and simpleQuery.page!=0">
  15. #{simpleQuery.page},
  16. </if>
  17. #{simpleQuery.size};
  18. </select>
  19. <select id="countDataJoinByQuery"
  20. resultType="java.lang.Integer">
  21. select
  22. count(id)
  23. from user
  24. <trim prefix="WHERE" prefixOverrides="AND |OR">
  25. <if test="simpleQuery.name!=null and simpleQuery.name!=''">
  26. and name = #{simpleQuery.name}
  27. </if>
  28. <!-- 类似上面的参数条件-->
  29. </trim>
  30. </select>

前端JS

前端一个按钮,点击执行export函数,执行以下JS代码

  1. //导出
  2. $scope.export = function () {
  3. let total = 0;
  4. var req_data = $.extend({}, $scope.form, $scope.page);
  5. //先发送数量查询,如果不为空,再查询列表,并导出
  6. let url = "/anomaly/getDataJoinExportList";//列表查询
  7. $http({
  8. method: "GET",
  9. url: "/anomaly/getDataJoinExports", //数量查询
  10. params: req_data, //$scope.form下的一些参数,前端页面传入
  11. }).then(function successCallback(response) {
  12. if (response.data.success) {
  13. //我是将第一次查询的total总数量,作为参数第二个方法接受,就不需要再次查询总数量了,这指定不安全的,如果需要考虑更安全的方式话,需要再次查询一遍。
  14. total = response.data.total;
  15. window.location.href = (url + "?startTime=" + $scope.form.startTime + "&endTime=" + $scope.form.endTime
  16. + "&type=" + $scope.form.type + "&belongSystem=" + $scope.form.belongSystem + "&total=" + total)
  17. } else {
  18. alert("无数据!")
  19. }
  20. }, function errorCallback() {
  21. })
  22. };

注意 :下载的时候不能使用ajax或者$http的形式发送请求,不然页面会下载不了,出现以下错误:
image.png
解决办法 :就是使用window.location.harf=url的形式发送请求。

效果展示

120w数据,大约需要两分钟,已经很长了,总觉的那个算法循环的地方可以优化,回头看看,再研究下。
image.png

参考