Java導出多個excel表打包到zip文件中供客戶端另存為窗口下載實現(xiàn)方法
一、業(yè)務(wù)背景
業(yè)務(wù)需求:從數(shù)據(jù)庫查詢多個list集合信息封裝excel,每個excel都有2個sheet頁,填充不同的信息,最后將所有excel打包成zip文件,以流的形式返回給客戶端,供客戶端另存為窗口下載。
- 只發(fā)出一次請求
- 每個excel表中到數(shù)據(jù)記錄不能超過2條
- excel文件或者zip包不會上傳服務(wù)器,而是查詢后直接封裝excel,然后把多個excel封裝成zip包直接返回
之前看過其他人的方案大概有2種:
- 方案1:打包成zip包后上傳到服務(wù)器某個路徑下,然后在讀取該路徑的zip文件,以流的形式返回給客戶端。
- 方案2:不上傳服務(wù)器,而是查詢后直接封裝excel,然后把多個excel封裝成zip包直接返回。(本人采用的就是第二種方案)
最終的效果,如圖
二、實現(xiàn)思路
- 設(shè)置HttpServletResponse的參數(shù),比如header、contentType
- 新建一個Workbook對象并置為空,同時初始化相關(guān)對象,比如List、File等
- 從數(shù)據(jù)庫查詢多條list
- 以其中為主的一個list計算分頁數(shù)量
- 循環(huán)遍歷list開始
- 初始化新的Workbook對象,并設(shè)置相應(yīng)的Title
- 將list拆分成多個段,分別寫到的Workbook對象中
- 將Workbook對象填充到List<XSSFWorkbook>中
- 重復步驟6至步驟8直至寫完所有數(shù)據(jù)
- 最后統(tǒng)一封裝zip壓縮包并導出,調(diào)用downFileByStream方法
- 初始化ZipOutputStream對象
- 循環(huán)遍歷List<XSSFWorkbook>將每一個wb寫入ZipOutputStream對象中,并將內(nèi)存流寫入Zip文件,即:將每一個excel封裝到zip包中
- 關(guān)閉ZipOutputStream
二、準備工作
1.準備data模板.xlsx
2.引入poi相關(guān)依賴,用于操作excel
pom.xml
<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>
3.針對WorkBook+ZIP壓縮輸入/輸出流,相關(guān)方法知識點要有所了解
Apache POI包中的HSSFWorkbook、XSSFWorkbook、SXSSFWorkbook的區(qū)別如下:
ZipOutputStream類的常用方法如下表所示:
方法 | 返回值 | 說明 |
---|---|---|
putNextEntry(ZipEntry e) | void | 開始寫一個新的ZipEntry,并將流內(nèi)的位置移至此entry所指數(shù)據(jù)的開頭 |
write(byte[] b, int off, int len) | void | 將字節(jié)數(shù)組寫入當前ZIP條目數(shù)據(jù) |
finish() | void | 完成寫入ZIP輸出流的內(nèi)容,無須關(guān)閉它所配合的OutputStream |
setComment(String comment) | void | 可設(shè)置此ZIP文件的注釋文字 |
ZipInputStream類的常用方法如下表所示:
方法 | 返回值 | 說明 |
---|---|---|
read(byte[] b, int off, int len) | int | 讀取目標b數(shù)組內(nèi)off偏移量的位置,長度是len字節(jié) |
available() | int | 判斷是否已讀完目前entry所指定的數(shù)據(jù)。已讀完返回0,否則返回1 |
closeEntry() | void | 關(guān)閉當前ZIP條目并定位流以讀取下一個條目 |
skip(long n) | long | 跳過當前ZIP條目中指定的字節(jié)數(shù) |
getNextEntry() | ZipEntry | 讀取下一個ZipEntry,并將流內(nèi)的位置移至該entry所指數(shù)據(jù)的開頭 |
createZipEntry(String name) | ZipEntry | 以指定的name參數(shù)新建一個ZipEntry對象 |
Workbook類提供的方法
方法 | 返回值 | 說明 |
---|---|---|
getNumberOfSheets() | int | 獲得工作薄(Workbook)中工作表(Sheet)的個數(shù) |
getSheets() | Sheet[] | 返回工作?。╓orkbook)中工作表(Sheet)對象數(shù)組 |
getVersion() | String | 返回正在使用的API的版本號,好像是沒什么太大的作用。 |
getName() | String | 獲取Sheet的名稱 |
getColumns() | int | 獲取Sheet表中所包含的總列數(shù) |
getColumn(int column) | Cell[] | 獲取某一列的所有單元格,返回的是單元格對象數(shù)組 |
getRows() | int | 獲取Sheet表中所包含的總行數(shù) |
getRow(int row) | Cell[] | 獲取某一行的所有單元格,返回的是單元格對象數(shù)組 |
getCell(int column, int row) | Cell | 獲取指定單元格的對象引用,需要注意的是它的兩個參數(shù),第一個是列數(shù),第二個是行數(shù),這與通常的行、列組合有些不同。 |
write() | 寫入Exel工作表 | |
close() | 關(guān)閉Excel工作薄對象 | |
getPhysicalNumberOfCells() | int | 獲取該行的總列數(shù) |
getSheetAt(int index) | Sheet | 根據(jù)索引index獲取對應(yīng)的sheet頁 |
getBodyStyle(Workbook wb) | CellStyle | 設(shè)置excel中比如第一行Title樣式 |
setCellStyle(CellStyle var1) | void | 跟getBodyStyle()方法搭配設(shè)置樣式 |
setCellValue(String var1) | void | 設(shè)置值 |
getStringCellValue() | String | 獲取對應(yīng)列的值 |
三、完整的項目代碼
ExportUtil工具類
package com.excel.utils; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.http.HttpServletResponse; import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.OutputStream; import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; /** * 導出【用戶、組、隸屬關(guān)系】工具 * @Author 211145187 * @Date 2023/4/13 16:29 **/ public class ExportUtil { private static Logger logger = LoggerFactory.getLogger(ExportUtil.class); /** * 自定義獲取分頁總頁數(shù)的方法 * @param count 查詢集合數(shù)量 * @param pageSize 配置文件中設(shè)置的單文件存儲最大條數(shù) * @return 總頁數(shù) */ public static Integer getPageCount(Integer count, Integer pageSize){ Integer pageCount = 0; if(count.equals(0)){ return pageCount; } pageCount = count/pageSize; if(count % pageSize != 0){ pageCount++; } return pageCount; } /** * 自定義List分頁工具 * @param list 待分頁的list數(shù)據(jù) * @param pageNum 頁碼 * @param pageSize 頁容量 * @param pageCount 總頁數(shù) * @return 分頁后的list數(shù)據(jù) */ public static <T> List<T> getPageList(List<T> list, Integer pageNum, Integer pageSize, Integer pageCount){ /**開始索引*/ int beginIndex = 0; /**結(jié)束索引*/ int endIndex = 0; Integer compare = pageNum.compareTo(pageCount); if(!compare.equals(0)){ beginIndex = (pageNum - 1) * pageSize; endIndex = beginIndex + pageSize; }else{ beginIndex = (pageNum - 1) * pageSize; endIndex = list.size(); } List pageList = list.subList(beginIndex, endIndex); return pageList; } /** * HSSFWorkbook轉(zhuǎn)file * @param wb wb * @param name 文件名稱 * @return File */ public static File xssfWorkbookToFile(Workbook wb, String name) { File toFile = new File(name); try { OutputStream os = new FileOutputStream(toFile); wb.write(os); os.close(); } catch (Exception e) { e.printStackTrace(); } return toFile; } /** * 直接下載zip包 * @param response response * @param excels wb集合 */ public static void downFileByStream(HttpServletResponse response, List<XSSFWorkbook> excels){ try { OutputStream toClient = new BufferedOutputStream(response.getOutputStream()); ZipOutputStream zipOutputStream = new ZipOutputStream(toClient); for(int i=0; i<excels.size(); i++){ ByteArrayOutputStream baos = new ByteArrayOutputStream(); // 將Workbook寫入內(nèi)存流 excels.get(i).write(baos); ZipEntry zipEntry = new ZipEntry("data" + i + ".xlsx"); zipOutputStream.putNextEntry(zipEntry); // 將內(nèi)存流寫入Zip文件 zipOutputStream.write(baos.toByteArray()); } zipOutputStream.closeEntry(); zipOutputStream.flush(); zipOutputStream.close(); }catch (Exception e){ logger.error("downFileByStream==========fail:{}", e.getMessage()); } } }
application.properties
server.port=8001 #導出excel配置,單文件存儲最大數(shù)量 export.num=2
OperateExcelController
package com.excel.controller; import com.excel.bean.Score; import com.excel.bean.Teacher; import com.excel.utils.ExportUtil; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellStyle; import org.apache.poi.ss.usermodel.FillPatternType; import org.apache.poi.ss.usermodel.IndexedColors; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.xssf.usermodel.XSSFCell; import org.apache.poi.xssf.usermodel.XSSFRow; import org.apache.poi.xssf.usermodel.XSSFSheet; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.RequestMapping; import javax.servlet.http.HttpServletResponse; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; /** * 測試3.17版本操作Excel * @Author 211145187 * @Date 2022/2/22 19:43 **/ @RequestMapping("/excel") @Controller public class OperateExcelController { private static Logger logger = LoggerFactory.getLogger(OperateExcelController.class); @Value("${export.num:10000}") private Integer exportLimitNum; //構(gòu)建教師集合數(shù)據(jù) public List<Teacher> buildTeacherList1() { List<Teacher> teacherList = new ArrayList<>(); Teacher teacher1 = new Teacher(); teacher1.setName("周杰倫"); teacher1.setClasses("三年二班"); teacher1.setCollege("魔法學院"); teacher1.setAlias("Jay Chou"); teacherList.add(teacher1); Teacher teacher2 = new Teacher(); teacher2.setName("陳奕迅"); teacher2.setClasses("三年二班"); teacher2.setCollege("魔法學院"); teacher2.setAlias("Eason"); teacherList.add(teacher2); Teacher teacher3 = new Teacher(); teacher3.setName("林俊杰"); teacher3.setClasses("三年二班"); teacher3.setCollege("魔法學院"); teacher3.setAlias("Eason"); teacherList.add(teacher3); Teacher teacher4 = new Teacher(); teacher4.setName("張杰"); teacher4.setClasses("三年二班"); teacher4.setCollege("魔法學院"); teacher4.setAlias("Eason"); teacherList.add(teacher4); return teacherList; } //構(gòu)建分數(shù)集合數(shù)據(jù) public List<Score> buildScoreList1() { List<Score> scoreList = new ArrayList<>(); Score score1 = new Score(); score1.setName("流川楓"); score1.setClasses("三年二班"); score1.setWriteScore("6"); score1.setComputerScore("4"); scoreList.add(score1); Score score2 = new Score(); score2.setName("櫻木花道"); score2.setClasses("三年二班"); score2.setWriteScore("6"); score2.setComputerScore("4"); scoreList.add(score2); Score score3 = new Score(); score3.setName("大猩猩"); score3.setClasses("三年二班"); score3.setWriteScore("6"); score3.setComputerScore("4"); scoreList.add(score3); Score score4 = new Score(); score4.setName("三井"); score4.setClasses("三年二班"); score4.setWriteScore("6"); score4.setComputerScore("4"); scoreList.add(score4); return scoreList; } //方法5:java導出多個Excel為zip包 @RequestMapping("/exportMultipleExcelToZip") public void exportMultipleExcelToZip(HttpServletResponse response) throws IOException { response.setHeader("Content-disposition", "attachment; filename=" + "test.zip"); response.setContentType("application/zip; charset=utf-8"); //創(chuàng)建HSSFWorkbook對象(excel的文檔對象) XSSFWorkbook wb = null; List<Teacher> teacherList = new ArrayList<>(); //構(gòu)建sheet頁集合 List<Score> scoreList = new ArrayList<>(); File templateFile = new File("C:\\Users\\211145187\\Desktop\\data模板.xlsx"); //.....省略部分代碼 List<Teacher> buildTeacherList = buildTeacherList1(); List<Score> buildScoreList = buildScoreList1(); Integer pageLimitSize = exportLimitNum; //計算list的分頁數(shù)量 Integer pageCount = ExportUtil.getPageCount(buildTeacherList.size(), pageLimitSize); List<XSSFWorkbook> excels = new ArrayList<>(); try { for(Integer pageNum = 1; pageNum < pageCount + 1; pageNum++) { //注意:每次循環(huán)遍歷前都需要初始化新的wb對象 //注意情況1:如果是初始化wb空對象然后手動添加title,下方三行代碼不會報錯 // wb = new XSSFWorkbook(); // buildScoreSheetTitle(wb); // buildTeacherSheetTitle(wb); //注意情況2:如果是初始化wb對象,并且以流的形式初始化,那么io流必須放在里面才行,如果放在for循環(huán)外面會報“Stream Closed”錯誤 InputStream io = new FileInputStream(templateFile); wb = new XSSFWorkbook(io); teacherList = ExportUtil.getPageList(buildTeacherList, pageNum, pageLimitSize, pageCount); scoreList = ExportUtil.getPageList(buildScoreList, pageNum, pageLimitSize, pageCount); buildScoreSheetParams(wb, scoreList); buildTeacherSheetParams(wb, teacherList); excels.add(wb); } //最后統(tǒng)一封裝zip壓縮包并導出 ExportUtil.downFileByStream(response, excels); } catch (Exception e) { logger.error("IOException:", e); } } /** * 填充教師頁信息 * @param wb wb * @param bodyData bodyData */ private void buildTeacherSheetParams(Workbook wb, List<Teacher> bodyData){ int teacherColumnCount = wb.getSheetAt(1).getRow(0).getPhysicalNumberOfCells(); Sheet sheet = wb.getSheetAt(1); // build data for(int j=0; j<bodyData.size(); j++){ Teacher itm = bodyData.get(j); Row rowData = sheet.createRow(j+1); for(int k=0;k<teacherColumnCount; k++){ Cell cell = rowData.createCell(k); cell.setCellValue(getValueByTeacher(k, itm)); } } } private String getValueByTeacher(int columnIndex,Teacher itm){ String cellValue; switch (columnIndex){ case 0:cellValue = itm.getName(); break; case 1:cellValue = itm.getClasses()+""; break; case 2:cellValue = itm.getCollege(); break; case 3:cellValue = itm.getAlias(); break; default:cellValue=""; break; } return cellValue; } /** * 填充分數(shù)頁信息 * @param wb wb * @param bodyData bodyData */ private void buildScoreSheetParams(Workbook wb, List<Score> bodyData){ int scoreColumnCount = wb.getSheetAt(0).getRow(0).getPhysicalNumberOfCells(); Sheet sheet = wb.getSheetAt(0); // build data for(int j=0; j<bodyData.size(); j++){ Score itm = bodyData.get(j); Row rowData = sheet.createRow(j+1); for(int k=0;k<scoreColumnCount; k++){ Cell cell = rowData.createCell(k); cell.setCellValue(getValueByScore(k, itm)); } } } private String getValueByScore(int columnIndex,Score itm){ String cellValue; switch (columnIndex){ case 0:cellValue = itm.getName(); break; case 1:cellValue = itm.getClasses()+""; break; case 2:cellValue = itm.getWriteScore(); break; case 3:cellValue = itm.getComputerScore(); break; default:cellValue=""; break; } return cellValue; } /** * 構(gòu)建分數(shù)表excel的標頭 * @Author 211145187 * @Date 2022/2/22 20:20 * @Param wb wb **/ private void buildScoreSheetTitle(XSSFWorkbook wb) { //建立新的sheet對象(excel的表單) XSSFSheet sheet=wb.createSheet("成績表"); XSSFRow row=sheet.createRow(0); //創(chuàng)建單元格并設(shè)置單元格內(nèi)容 XSSFCell cell0 = row.createCell(0); cell0.setCellValue("姓名"); cell0.setCellStyle(getHeadStyle(wb)); XSSFCell cell1 = row.createCell(1); cell1.setCellValue("班級"); cell1.setCellStyle(getHeadStyle(wb)); XSSFCell cell2 = row.createCell(2); cell2.setCellValue("筆試成績"); cell2.setCellStyle(getHeadStyle(wb)); XSSFCell cell3 = row.createCell(3); cell3.setCellValue("機試成績"); cell3.setCellStyle(getHeadStyle(wb)); } /** * 構(gòu)建教師表excel的標頭 * @Author 211145187 * @Date 2022/2/22 20:20 * @Param wb wb **/ private void buildTeacherSheetTitle(XSSFWorkbook wb) { //建立新的sheet對象(excel的表單) XSSFSheet sheet=wb.createSheet("教師表"); XSSFRow row=sheet.createRow(0); //創(chuàng)建單元格并設(shè)置單元格內(nèi)容 XSSFCell cell0 = row.createCell(0); cell0.setCellValue("姓名"); cell0.setCellStyle(getHeadStyle(wb)); XSSFCell cell1 = row.createCell(1); cell1.setCellValue("班級"); cell1.setCellStyle(getHeadStyle(wb)); XSSFCell cell2 = row.createCell(2); cell2.setCellValue("所屬學院"); cell2.setCellStyle(getHeadStyle(wb)); XSSFCell cell3 = row.createCell(3); cell3.setCellValue("別名"); cell3.setCellStyle(getHeadStyle(wb)); } /** * 設(shè)置樣式 * @Author 211145187 * @Date 2022/2/22 20:15 * @Param wb wb * @Return CellStyle **/ private CellStyle getHeadStyle(Workbook wb) { CellStyle cellStyle = wb.createCellStyle(); //用于設(shè)置前景顏色 cellStyle.setFillForegroundColor(IndexedColors.SKY_BLUE.getIndex()); /** * setFillPattern用于設(shè)置單元格填充樣式 * 注意: * 1)setFillPattern必須設(shè)置否則光設(shè)置setFillForegroundColor無效 * 2)3.10.1版本支持short類型參數(shù),而3.17版本支持FillPatternType類型參數(shù) */ cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); return cellStyle; } }
四、可能遇到的問題
錯誤場景1:java.io.IOException: Stream closed
原因分析:
我這里導致流關(guān)閉會有2種場景觸發(fā):場景1:工具類方法downFileByStream()中的for循環(huán)執(zhí)行write()方法。
場景2:wb = new XSSFWorkbook(io);通過IO流初始化wb對象時。
解決方案:
針對場景1中的解決方案就是:Workbook的write()方法最終會關(guān)閉它寫入的輸出流。如果只是一次性全部輸出那就不會碰到這種問題,但是現(xiàn)在的場景是工具類方法downFileByStream()中的for循環(huán)執(zhí)行write()方法,所以解決方案就是用一個ByteArrayOutputStream去接收存儲當前Workbook的內(nèi)容,這樣哪怕Workbook關(guān)閉了,但是內(nèi)容已經(jīng)提前存入了ByteArrayOutputStream中,只要讀取了就可以繼續(xù)使用。因此才有這樣的如下寫法:
/** * 直接下載zip包 * @param response response * @param excels wb集合 */ public static void downFileByStream(HttpServletResponse response, List<XSSFWorkbook> excels){ try { OutputStream toClient = new BufferedOutputStream(response.getOutputStream()); ZipOutputStream zipOutputStream = new ZipOutputStream(toClient); for(int i=0; i<excels.size(); i++){ ByteArrayOutputStream baos = new ByteArrayOutputStream(); // 將Workbook寫入內(nèi)存流 excels.get(i).write(baos); ZipEntry zipEntry = new ZipEntry("data" + i + ".xlsx"); zipOutputStream.putNextEntry(zipEntry); // 將內(nèi)存流寫入Zip文件 zipOutputStream.write(baos.toByteArray()); } zipOutputStream.closeEntry(); zipOutputStream.flush(); zipOutputStream.close(); }catch (Exception e){ logger.error("downFileByStream==========fail:{}", e.getMessage()); } }
針對場景2中的解決方案就是:把IO流初始化提到for循環(huán)里面,如實有了如下寫法:
for(Integer pageNum = 1; pageNum < pageCount + 1; pageNum++) { //注意:每次循環(huán)遍歷前都需要初始化新的wb對象 //注意情況1:如果是初始化wb空對象然后手動添加title,下方三行代碼不會報錯 // wb = new XSSFWorkbook(); // buildScoreSheetTitle(wb); // buildTeacherSheetTitle(wb); //注意情況2:如果是初始化wb對象,并且以流的形式初始化,那么io流必須放在里面才行,如果放在for循環(huán)外面會報“Stream Closed”錯誤 InputStream io = new FileInputStream(templateFile); wb = new XSSFWorkbook(io); ... }
錯誤場景2:調(diào)用接口沒有另存為彈窗,但是F12查看接口結(jié)果返回一堆亂碼
錯誤原因分析:可能是返回結(jié)果HttpServletResponse設(shè)置的setContentType格式不對。
正確的應(yīng)該設(shè)置為如下,一定要設(shè)置為application/zip:
response.setHeader("Content-disposition", "attachment; filename=" + "test.zip"); response.setContentType("application/zip; charset=utf-8");
總結(jié)
到此這篇關(guān)于Java導出多個excel表打包到zip文件中供客戶端另存為窗口下載實現(xiàn)的文章就介紹到這了,更多相關(guān)Java導出多excel表打包到zip文件內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
關(guān)于Long和Integer相互轉(zhuǎn)換方式
這篇文章主要介紹了關(guān)于Long和Integer相互轉(zhuǎn)換方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-08-08淺談springmvc的DispatcherServlet分析
本篇文章主要介紹了淺談springmvc的DispatcherServlet分析,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-09-09Java版數(shù)據(jù)結(jié)構(gòu)插入數(shù)據(jù)時遇到的結(jié)點為空的問題詳解
這篇文章主要介紹了Java版數(shù)據(jù)結(jié)構(gòu)插入數(shù)據(jù)時遇到的結(jié)點為空的問題及解決辦法,需要的朋友們可以學習下。2019-09-09JVM(Java虛擬機)簡介(動力節(jié)點Java學院整理)
Java虛擬機(Jvm)是可運行Java代碼的假想計算機。Java虛擬機包括一套字節(jié)碼指令集、一組寄存器、一個棧、一個垃圾回收堆和一個存儲方法域。對java jvm 虛擬機感興趣的朋友通過本文一起學習吧2017-04-04Spring Task定時任務(wù)每天零點執(zhí)行一次的操作
這篇文章主要介紹了Spring Task定時任務(wù)每天零點執(zhí)行一次的操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-09-09