Java中常見的用來操作 Excel 的方式有2種:JXL和POI。JXL只能對 Excel進行操作,且只支持到 Excel 95-2000的版本。而POI是Apache 的開源項目,由Java編寫的跨平台 Java API,可操作 Microsoft Office。借助POI,可以方便的生成數據報表,數據批量上傳,數據備份等工作。
一.簡單使用
1.創建Maven工程導入POI坐標
<!-- poi 相關 --> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>4.0.1</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>4.0.1</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml-schemas</artifactId> <version>4.0.1</version> </dependency>
-
HSSF : 讀寫 Microsoft Excel XLS 格式文檔
-
XSSF : 讀寫 Microsoft Excel OOXML XLSX 格式文檔
-
SXSSF : 讀寫 Microsoft Excel OOXML XLSX 格式文檔
-
HWPF : 讀寫 Microsoft Word DOC 格式文檔
-
HSLF : 讀寫 Microsoft PowerPoint 格式文檔
-
HDGF : 讀 Microsoft Visio 格式文檔
-
HPBF : 讀 Microsoft Publisher 格式文檔
-
HSMF : 讀 Microsoft Outlook 格式文檔
3.操作示例
//1.創建Excel對象 XSSFWorkbook wb = new XSSFWorkbook(); //2.創建Sheet對象 Sheet sheet = wb.createSheet(); //3.創建行對象(索引從0開始) Row nRow = sheet.createRow(0); //4.設置行高和列寬 nRow.setHeightInPoints(26.25f); sheet.setColumnWidth(1,26*256); //(列的索引,列寬*256(理解為固定寫法)) //5.創建單元格對象(索引從0開始) Cell nCell = nRow.createCell(0); //6.設置單元格內容 nCell.setCellValue("dinTalk"); //============================== //7.創建單元格樣式對象 CellStyle style = wb.createCellStyle(); //8.創建字體對象 Font font = wb.createFont(); //9.設置字體和其大小及效果 font.setFontName("黑體"); font.setFontHeightInPoints((short)12); font.setBold(true); //加粗 //10.設置樣式 style.setFont(font); style.setAlignment(HorizontalAlignment.CENTER); //橫向居中 style.setVerticalAlignment(VerticalAlignment.CENTER);//縱向居中 style.setBorderTop(BorderStyle.THIN); //上細線 style.setBorderBottom(BorderStyle.THIN); //下細線 style.setBorderLeft(BorderStyle.THIN); //左細線 style.setBorderRight(BorderStyle.THIN); //右細線 //11.為單元格應用樣式 nCell.setCellStyle(style);
1.模板打印(下載)
我們通過自定義生成 Excel 報表文件很是麻煩,特別是字體、樣式比較復雜的時候。這時候我們可以考慮使用准備好的 Excel 模板,這樣我們只需關注模板中的數據即可。
制作並加載Excel 模板,填充數據響應到瀏覽器(下載)
/** * 下載用戶新增表 * @param inputDate 格式為:2019-01 */ @RequestMapping(value = "/printExcel",name = "下載用戶新增表") public void printExcel(String inputDate) throws Exception { //1.用servletContext對象獲取excel模板的真實路徑 String templatePath = session.getServletContext().getRealPath("/dintalk/xlsprint/dtUSER.xlsx"); //2.讀取excel模板,創建excel對象 XSSFWorkbook wb = new XSSFWorkbook(templatePath); //3.讀取sheet對象 Sheet sheet = wb.getSheetAt(0); //4.定義一些可復用的對象 int rowIndex = 0; //行的索引 int cellIndex = 1; //單元格的索引 Row nRow = null; Cell nCell = null; //5.讀取大標題行 nRow = sheet.getRow(rowIndex++); // 使用后 +1 //6.讀取大標題的單元格 nCell = nRow.getCell(cellIndex); //7.設置大標題的內容 String bigTitle = inputDate.replace("-0","-").replace("-","年")+"月份新增用戶表"; nCell.setCellValue(bigTitle); //8.跳過第二行(模板的小標題,我們要用) rowIndex++; //9.讀取第三行,獲取它的樣式 nRow = sheet.getRow(rowIndex); //讀取行高 float lineHeight = nRow.getHeightInPoints(); //10.獲取第三行的5個單元格中的樣式 CellStyle cs1 = nRow.getCell(cellIndex++).getCellStyle(); CellStyle cs2 = nRow.getCell(cellIndex++).getCellStyle(); CellStyle cs3 = nRow.getCell(cellIndex++).getCellStyle(); CellStyle cs4 = nRow.getCell(cellIndex++).getCellStyle(); CellStyle cs5 = nRow.getCell(cellIndex++).getCellStyle(); //11.通過月份查詢新增用戶列表 List<User> newUserList = UserService.findByAddTime(inputDate); //12.遍歷數據 for(User user : newUserList){ //13.創建數據行 nRow = sheet.createRow(rowIndex++); //16.設置數據行高 nRow.setHeightInPoints(lineHeight); //17.重置cellIndex,從第一列開始寫數據 cellIndex = 1; //18.創建數據單元格,設置單元格內容和樣式 //用戶名 nCell = nRow.createCell(cellIndex++); nCell.setCellStyle(cs1); nCell.setCellValue(user.getUserName()); //性別 nCell = nRow.createCell(cellIndex++); nCell.setCellStyle(cs2); nCell.setCellValue(user.getSex()); //年齡 nCell = nRow.createCell(cellIndex++); nCell.setCellStyle(cs3); nCell.setCellValue(user.getAge()); //手機號 nCell = nRow.createCell(cellIndex++); nCell.setCellStyle(cs4); nCell.setCellValue(user.getPhone()); //郵箱 nCell = nRow.createCell(cellIndex++); nCell.setCellStyle(cs5); nCell.setCellValue(user.getEmail()); } //最后,下載新增用戶表,字節數組的輸出流,它可存可取,帶緩沖區 ByteArrayOutputStream bos = new ByteArrayOutputStream(); wb.write(bos); //將工作簿寫到輸出流中 new DownloadUtil().download(bos,response,bigTitle+".xlsx"); bos.close(); wb.close(); }
在添加數據時,通過批量導入可大大減少人力。但是批量導入需要代碼解析固定格式的模板,因此我們最好給用戶提供模板下載功能。我們同樣以導入用戶表為例:
統一 excel 模板格式
/** * 批量導入用戶 * @param companyId * @param file * @return */ @RequestMapping("/import") public String ImportExcel(String companyId, MultipartFile file) throws IOException { //定義一個list 集合保存從excel解析的用戶 List<User> list = new ArrayList<>(); //1.讀取上傳的文件 InputStream inputStream = file.getInputStream(); XSSFWorkbook wb = new XSSFWorkbook(inputStream); //2.獲取工作表對象 XSSFSheet sheet = wb.getSheetAt(0); //3.得到行的迭代器 Iterator<Row> iterator = sheet.iterator(); int rowNum = 0; while (iterator.hasNext()) { Row row = iterator.next(); //跳過標題行 if (rowNum == 0) { rowNum++; continue; } //4.遍歷,把每一行數據存到Object數組中 Object[] obj = new Object[6]; for (int i = 1; i < 6; i++) { obj[i] = getValue(row.getCell(i));//獲取到單元格內的數據,方法見下 } //5.創建用戶對象(用戶實體類的有參構造方法) User user = new User(obj, companyId); //6.將用戶對象保存到集合中 list.add(user); } //7.讀取完數據后,調用service層方法進行批量保存 UserService.saveAll(list); //8.重定向到企業列表 return "redirect:/dintalk/company/list.do"; } /** * 獲取單元格內的數據,並進行格式轉換 * @param cell * @return */ private Object getValue(Cell cell) { switch (cell.getCellType()) { case STRING: return cell.getStringCellValue(); case BOOLEAN: return cell.getBooleanCellValue(); case NUMERIC:// 數值和日期均是此類型,需進一步判斷 if (DateUtil.isCellDateFormatted(cell)) { //是日期類型 return cell.getDateCellValue(); } else { //是數值類型 return cell.getNumericCellValue(); } default: return null; } }
當我們碰到數據量比較大的時候(百萬級),我們該如何通過使用 POI 對百萬級數據報表進行導入和導出的操作呢?我們知道,Excel可以分為早期的 Excel2003版本(使用POI的HSSF對象操作)和 Excel2007版本(使用POI的 XSSF操作),兩者對百萬數據的支持如下:
-
HSSFWorkbook 最大行數和列數限制 最大支持65536行
-
XSSFWorkbook 最大支持1048576行
XSSFWorkbook 單個 sheet 表就支持近百萬條數據。但實際運行時還可能存在問題,原因是執行 POI 報表所產生的行對象,單元格對象,字體對象,他們都不會銷毀,這就有導致 OOM 的風險。我們可以使用JDK提供的性能工具 Jvisualvm 來監視程序的運行情況,包括 CUP,垃圾回收,內存的分配和使用情況(Jvisualvm位於JAVA_HOME/bin目錄下,雙擊打開即可)。
1.百萬數據報表導出
基於 XSSFWork 導出 Excel 報表,是通過將所有單元格對象保存到內存中,當所有的 Excel 單元格全部創建完成之后一次性寫入到 Excel 並導出。當百萬數據級別的Excel 導出時,隨着表格的不斷創建,內存中對象越來越多,直至內存溢出。Apache Poi 提供了 SXSSFWork 對象,專門用於處理大數據量 Excel 報表導出。 在實例化 SXSSFWork 這個對象時,可以指定在內存中所產生的 POI 導出相關對象的數量(默認 100),一旦內存中的對象的個數達到這個指定值時,就將內存中的這些對象的內容寫入到磁盤中(XML 的文件格式),就可以將這些對象從內存中銷毀,以后只要達到這個值,就會以類似的處理方式處理,直至 Excel 導出完成。SXSSFWorkbook它支持百萬級數據的POI,但是不支持模板打印也不支持太多的樣式。因此我們需要通過自定義的方式來進行導出。
/* * 下載用戶新增表 * @param inputDate 格式為:2019-01 */ @RequestMapping("/printExcel") public void printExcel(String inputDate)throws Exception{ //1.創建Excel對象 XSSFWorkbook wb = new XSSFWorkbook(); SXSSFWorkbook wb = new SXSSFWorkbook(1000);//默認值是100 //2.創建Sheet對象 Sheet sheet = wb.createSheet(); //3.定義一些可復用的對象 int rowIndex = 0;//行的索引 int cellIndex = 1;//單元格的索引 Row nRow = null; Cell nCell = null; //4.設置列的寬度(列索引,列寬*256 理解為固定寫法) sheet.setColumnWidth(1,26*256); sheet.setColumnWidth(2,12*256); sheet.setColumnWidth(3,29*256); sheet.setColumnWidth(4,12*256); sheet.setColumnWidth(5,15*256); //5.創建大標題行 大標題:2019年5月份新增用戶表 nRow = sheet.createRow(rowIndex++);//使用的是0,使用完了+1 //設置大標題行的高度 nRow.setHeightInPoints(36); //6.創建大標題的單元格 nCell = nRow.createCell(cellIndex); //7.合並單元格 sheet.addMergedRegion(new CellRangeAddress(0,0,1,5)); //8.設置大標題內容 String bigTitle = inputDate.replaceAll("-0","-").replaceAll("-","年") + "月份新增用戶表";//inputDate 2015-01 2015年1月份出貨表 nCell.setCellValue(bigTitle); //9.創建小標題內容 String[] titles = new String[]{"用戶名","性別","年齡","手機號","郵箱"}; //10.創建小標題行 nRow = sheet.createRow(rowIndex++);//使用的是1,使用完了再加1 //設置小標題行高 nRow.setHeightInPoints(26.25f); //12.創建小標題的單元格 for(String title : titles){ nCell = nRow.createCell(cellIndex++); //設置小標題內容 nCell.setCellValue(title); } //13.獲取要生成的數據(數據庫內的用戶數據) List<User> list = UserService.findUserByAddTime(inputDate); //14.遍歷數據 for(User user : list){ for(int i=0;i<5000;i++) { //15.創建數據行 nRow = sheet.createRow(rowIndex++); //16.設置數據行高 nRow.setHeightInPoints(24); //17.重置cellIndex cellIndex = 1; //18.創建數據單元格,設置單元格內容 //用戶名 nCell = nRow.createCell(cellIndex++); nCell.setCellValue(user.getUserName()); //性別 nCell = nRow.createCell(cellIndex++); nCell.setCellValue(user.getSex()); //年齡 nCell = nRow.createCell(cellIndex++); nCell.setCellValue(user.getAge()); //手機號 nCell = nRow.createCell(cellIndex++); nCell.setCellValue(user.getPhone()); //郵箱 nCell = nRow.createCell(cellIndex++); nCell.setCellValue(user.getEmail()); } } //最后,下載新增用戶表文件,字節數組的輸出流,它可存可取,帶緩沖區 ByteArrayOutputStream bos = new ByteArrayOutputStream(); wb.write(bos); new DownloadUtil().download(bos,response,bigTitle+".xlsx"); bos.close(); wb.close(); }
/** * By Mr.Song 2019-05-17 * @param byteArrayOutputStream 將文件內容寫入ByteArrayOutputStream * @param response HttpServletResponse 寫入response * @param returnName 返回的文件名 */ public void download(ByteArrayOutputStream byteArrayOutputStream, HttpServletResponse response, String returnName) throws IOException{ response.setContentType("application/octet-stream;charset=utf-8"); returnName = response.encodeURL(new String(returnName.getBytes(),"iso8859-1")); //保存的文件名,必須和頁面編碼一致,否則亂碼 response.addHeader("Content-Disposition", "attachment;filename=" + returnName); response.setContentLength(byteArrayOutputStream.size()); ServletOutputStream outputstream = response.getOutputStream(); //取得輸出流 byteArrayOutputStream.writeTo(outputstream); //寫到輸出流 byteArrayOutputStream.close(); //關閉 outputstream.flush(); //刷數據 }
導入,其實就是讀取,讀取excel的兩種思路:
第一種:全部讀取
-
優勢:對excel的增刪改查都方便
-
弊端:由於要加載完整合excel文件,如果文件過大時,對內存消耗嚴重
第二種:按事件觸發
-
觸發到什么事件,就讀什么內容。
-
事件分為:
-
讀到行的開始
-
讀到行的結束
-
讀到一行的內容
-
-
優勢:執行解析效率高,因為它是按照事件觸發的。一次只讀一行數據
-
弊端:不利於保存,更新和刪除。因為它沒有讀完整個excel,所以對整個excel的結構不清楚。
-
它適用於數據量級比較大的情況
第一步:導入POI坐標后創建處理器
/**這個類誰用誰寫(讀取excel內容要做的事,實現接口,重寫方法) * @author Mr.song * @date 2019/05/19 20:11 */ public class SheetHandler implements XSSFSheetXMLHandler.SheetContentsHandler { /** * 每一行創建一個user對象,當此行解析結束之后,打印user對象 */ private User user; //開始解析某一行,int :行號 @Override public void startRow(int i) { if (i >= 2) { //跳過標題行,開始創建user對象 user = new User(); } } //此行解析結束 @Override public void endRow(int i) { System.out.println(user); //這里簡單打印,可以存入列表。當列表內user對象大於1000時,存入數據庫 } /** * 獲取當前行的每一個單元格數據 * @param cellName : 當前單元格名稱 : B32 C23 DXX * @param cellValue : 當前單元格數據 * @param xssfComment : 單元格注釋 * 用戶名 性別 年齡 手機號 郵箱 */ @Override public void cell(String cellName, String cellValue, XSSFComment xssfComment) { String name = cellName.substring(0, 1);//B2--->B F2---->F if (user != null) { switch (name) { case "B": { user.setUserName(cellValue); break; } case "C": { user.setSex(cellValue); break; } case "D": { user.setAge(Integer.parseInt(cellValue)); break; } case "E": { user.setPhone(cellValue); break; } case "F": { user.setEmail(cellValue); break; } default: { break; } } } } }
/**解析器,寫法基本固定 * @author Mr.song * @date 2019/05/19 20:08 */ public class ExcelParse { public void parse (String path) throws Exception { //解析器 SheetHandler hl = new SheetHandler(); //1.根據 Excel 獲取 OPCPackage 對象 OPCPackage pkg = OPCPackage.open(path, PackageAccess.READ); try { //2.創建 XSSFReader 對象 XSSFReader reader = new XSSFReader(pkg); //3.獲取 SharedStringsTable 對象 SharedStringsTable sst = reader.getSharedStringsTable(); //4.獲取 StylesTable 對象 StylesTable styles = reader.getStylesTable(); XMLReader parser = XMLReaderFactory.createXMLReader(); // 處理公共屬性 parser.setContentHandler(new XSSFSheetXMLHandler(styles,sst, hl, false)); XSSFReader.SheetIterator sheets = (XSSFReader.SheetIterator) reader.getSheetsData(); //逐行讀取逐行解析 while (sheets.hasNext()) { InputStream sheetstream = sheets.next(); InputSource sheetSource = new InputSource(sheetstream); try { parser.parse(sheetSource); } finally { sheetstream.close(); } } } finally { pkg.close(); } } //測試解析桌面百萬級excel文件 public static void main(String[] args)throws Exception { new ExcelParse().parse("C:\\Users\\sh\\Desktop\\User.xlsx"); } }
測試時,可以通過 Jvisualvm 來監視程序的運行情況,包括 CUP,垃圾回收,內存的分配和使用情況(Jvisualvm位於JAVA_HOME/bin目錄下,雙擊打開即可)。
喜歡的朋友可以關注我的公眾號,需要廣告托管的朋友可以加QQ哦!