Java利用POI讀取、寫入Excel的方法指南
前言
Apache POI [1] 是用Java編寫的免費開源的跨平臺的 Java API,Apache POI提供API給Java程式對Microsoft Office格式檔案讀和寫的功能。POI為“Poor Obfuscation Implementation”的首字母縮寫,意為“簡潔版的模糊實現(xiàn)”。
做項目時經(jīng)常有通過程序讀取Excel數(shù)據(jù),或是創(chuàng)建新的Excel并寫入數(shù)據(jù)的需求;
網(wǎng)上很多經(jīng)驗教程里使用的POI版本都比較老了,一些API在新版里已經(jīng)廢棄,這里基于最新的Apache POI 4.0.1版本來總結(jié)一下整個讀取和寫入Excel的過程,希望能幫助到需要的人 ^_^
1. 準(zhǔn)備工作
1.1 在項目中引入Apache POI相關(guān)類庫
引入 Apache POI 和 Apache POI-OOXML 這兩個類庫,Maven坐標(biāo)如下:
<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>
2. 讀取或?qū)懭隕xcel數(shù)據(jù)
2.1 示例程序結(jié)構(gòu)說明
簡單說明一下示例程序的整體結(jié)構(gòu):
- ExcelReader.java是實現(xiàn)讀取Excel數(shù)據(jù)功能的類;
- ExcelWriter.java是創(chuàng)建新的Excel并向其中寫入數(shù)據(jù)的類;
- ExcelDataVO.java封裝了讀取或?qū)懭霑r每一“行”的數(shù)據(jù);
- MainTest.java是示例程序的入口類,其中演示了讀取和寫入Excel數(shù)據(jù)的整個過程;
2.2 讀取數(shù)據(jù)
示例程序需要從桌面讀取 readExample.xlsx 內(nèi)的數(shù)據(jù),readExample.xlsx 的內(nèi)容如下:
讀取Excel時主要調(diào)用ExcelReader.java類來讀取和解析Excel的具體內(nèi)容,這里以讀取系統(tǒng)文件的形式演示讀取過程:(兼容 xls 和 xlsx)
2.2.1 主程序入口類代碼:
/** * Author: Dreamer-1 * Date: 2019-03-01 * Time: 10:13 * Description: 示例程序入口類 */ public class MainTest { public static void main(String[] args) { // 設(shè)定Excel文件所在路徑 String excelFileName = "/Users/Dreamer-1/Desktop/myBlog/java解析Excel/readExample.xlsx"; // 讀取Excel文件內(nèi)容 List<ExcelDataVO> readResult = ExcelReader.readExcel(excelFileName); // todo 進行業(yè)務(wù)操作 } }
讀取和寫入時封裝每一“行”數(shù)據(jù)的ExcelDataVO.java代碼如下:
/** * Author: Dreamer-1 * Date: 2019-03-01 * Time: 11:33 * Description: 讀取Excel時,封裝讀取的每一行的數(shù)據(jù) */ public class ExcelDataVO { /** * 姓名 */ private String name; /** * 年齡 */ private Integer age; /** * 居住地 */ private String location; /** * 職業(yè) */ private String job; public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public String getLocation() { return location; } public void setLocation(String location) { this.location = location; } public String getJob() { return job; } public void setJob(String job) { this.job = job; } }
2.2.2 Excel解析類的代碼:
/** * Author: Dreamer-1 * Date: 2019-03-01 * Time: 10:21 * Description: 讀取Excel內(nèi)容 */ public class ExcelReader { private static Logger logger = Logger.getLogger(ExcelReader.class.getName()); // 日志打印類 private static final String XLS = "xls"; private static final String XLSX = "xlsx"; /** * 根據(jù)文件后綴名類型獲取對應(yīng)的工作簿對象 * @param inputStream 讀取文件的輸入流 * @param fileType 文件后綴名類型(xls或xlsx) * @return 包含文件數(shù)據(jù)的工作簿對象 * @throws IOException */ public static Workbook getWorkbook(InputStream inputStream, String fileType) throws IOException { Workbook workbook = null; if (fileType.equalsIgnoreCase(XLS)) { workbook = new HSSFWorkbook(inputStream); } else if (fileType.equalsIgnoreCase(XLSX)) { workbook = new XSSFWorkbook(inputStream); } return workbook; } /** * 讀取Excel文件內(nèi)容 * @param fileName 要讀取的Excel文件所在路徑 * @return 讀取結(jié)果列表,讀取失敗時返回null */ public static List<ExcelDataVO> readExcel(String fileName) { Workbook workbook = null; FileInputStream inputStream = null; try { // 獲取Excel后綴名 String fileType = fileName.substring(fileName.lastIndexOf(".") + 1, fileName.length()); // 獲取Excel文件 File excelFile = new File(fileName); if (!excelFile.exists()) { logger.warning("指定的Excel文件不存在!"); return null; } // 獲取Excel工作簿 inputStream = new FileInputStream(excelFile); workbook = getWorkbook(inputStream, fileType); // 讀取excel中的數(shù)據(jù) List<ExcelDataVO> resultDataList = parseExcel(workbook); return resultDataList; } catch (Exception e) { logger.warning("解析Excel失敗,文件名:" + fileName + " 錯誤信息:" + e.getMessage()); return null; } finally { try { if (null != workbook) { workbook.close(); } if (null != inputStream) { inputStream.close(); } } catch (Exception e) { logger.warning("關(guān)閉數(shù)據(jù)流出錯!錯誤信息:" + e.getMessage()); return null; } } } /** * 解析Excel數(shù)據(jù) * @param workbook Excel工作簿對象 * @return 解析結(jié)果 */ private static List<ExcelDataVO> parseExcel(Workbook workbook) { List<ExcelDataVO> resultDataList = new ArrayList<>(); // 解析sheet for (int sheetNum = 0; sheetNum < workbook.getNumberOfSheets(); sheetNum++) { Sheet sheet = workbook.getSheetAt(sheetNum); // 校驗sheet是否合法 if (sheet == null) { continue; } // 獲取第一行數(shù)據(jù) int firstRowNum = sheet.getFirstRowNum(); Row firstRow = sheet.getRow(firstRowNum); if (null == firstRow) { logger.warning("解析Excel失敗,在第一行沒有讀取到任何數(shù)據(jù)!"); } // 解析每一行的數(shù)據(jù),構(gòu)造數(shù)據(jù)對象 int rowStart = firstRowNum + 1; int rowEnd = sheet.getPhysicalNumberOfRows(); for (int rowNum = rowStart; rowNum < rowEnd; rowNum++) { Row row = sheet.getRow(rowNum); if (null == row) { continue; } ExcelDataVO resultData = convertRowToData(row); if (null == resultData) { logger.warning("第 " + row.getRowNum() + "行數(shù)據(jù)不合法,已忽略!"); continue; } resultDataList.add(resultData); } } return resultDataList; } /** * 將單元格內(nèi)容轉(zhuǎn)換為字符串 * @param cell * @return */ private static String convertCellValueToString(Cell cell) { if(cell==null){ return null; } String returnValue = null; switch (cell.getCellType()) { case NUMERIC: //數(shù)字 Double doubleValue = cell.getNumericCellValue(); // 格式化科學(xué)計數(shù)法,取一位整數(shù) DecimalFormat df = new DecimalFormat("0"); returnValue = df.format(doubleValue); break; case STRING: //字符串 returnValue = cell.getStringCellValue(); break; case BOOLEAN: //布爾 Boolean booleanValue = cell.getBooleanCellValue(); returnValue = booleanValue.toString(); break; case BLANK: // 空值 break; case FORMULA: // 公式 returnValue = cell.getCellFormula(); break; case ERROR: // 故障 break; default: break; } return returnValue; } /** * 提取每一行中需要的數(shù)據(jù),構(gòu)造成為一個結(jié)果數(shù)據(jù)對象 * * 當(dāng)該行中有單元格的數(shù)據(jù)為空或不合法時,忽略該行的數(shù)據(jù) * * @param row 行數(shù)據(jù) * @return 解析后的行數(shù)據(jù)對象,行數(shù)據(jù)錯誤時返回null */ private static ExcelDataVO convertRowToData(Row row) { ExcelDataVO resultData = new ExcelDataVO(); Cell cell; int cellNum = 0; // 獲取姓名 cell = row.getCell(cellNum++); String name = convertCellValueToString(cell); resultData.setName(name); // 獲取年齡 cell = row.getCell(cellNum++); String ageStr = convertCellValueToString(cell); if (null == ageStr || "".equals(ageStr)) { // 年齡為空 resultData.setAge(null); } else { resultData.setAge(Integer.parseInt(ageStr)); } // 獲取居住地 cell = row.getCell(cellNum++); String location = convertCellValueToString(cell); resultData.setLocation(location); // 獲取職業(yè) cell = row.getCell(cellNum++); String job = convertCellValueToString(cell); resultData.setJob(job); return resultData; } }
2.2.3 應(yīng)用場景補充
一般我們會有這樣的應(yīng)用場景,即:在前臺頁面的文件上傳入口上傳本地的Excel文件到后臺,后臺收到Excel文件后進行解析并做對應(yīng)的業(yè)務(wù)操作;
這里假設(shè)前臺已經(jīng)有了上傳文件的入口,再簡單貼一下后臺的解析代碼;
后臺接收前臺數(shù)據(jù)的Controller層代碼示例:
@PostMapping("/uploadExcel") public ResponseEntity<?> uploadImage(MultipartFile file) { // 檢查前臺數(shù)據(jù)合法性 if (null == file || file.isEmpty()) { logger.warning("上傳的Excel商品數(shù)據(jù)文件為空!上傳時間:" + new Date()); return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } try { // 解析Excel List<ExcelDataVO> parsedResult = ExcelReader.readExcel(file); // todo 進行業(yè)務(wù)操作 return new ResponseEntity<>(HttpStatus.OK); } catch (Exception e) { logger.warning("上傳的Excel商品數(shù)據(jù)文件為空!上傳時間:" + new Date()); return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } }
ExcelReader.java中的 readExcel() 方法需要做一定的修改,代碼如下:
/** * 讀取Excel文件內(nèi)容 * @param file 上傳的Excel文件 * @return 讀取結(jié)果列表,讀取失敗時返回null */ public static List<ExcelDataVO> readExcel(MultipartFile file) { Workbook workbook = null; try { // 獲取Excel后綴名 String fileName = file.getOriginalFilename(); if (fileName == null || fileName.isEmpty() || fileName.lastIndexOf(".") < 0) { logger.warning("解析Excel失敗,因為獲取到的Excel文件名非法!"); return null; } String fileType = fileName.substring(fileName.lastIndexOf(".") + 1, fileName.length()); // 獲取Excel工作簿 workbook = getWorkbook(file.getInputStream(), fileType); // 讀取excel中的數(shù)據(jù) List<ExcelDataVO> resultDataList = parseExcel(workbook); return resultDataList; } catch (Exception e) { logger.warning("解析Excel失敗,文件名:" + file.getOriginalFilename() + " 錯誤信息:" + e.getMessage()); return null; } finally { try { if (null != workbook) { workbook.close(); } } catch (Exception e) { logger.warning("關(guān)閉數(shù)據(jù)流出錯!錯誤信息:" + e.getMessage()); return null; } } }
2.3 寫入數(shù)據(jù)
寫入數(shù)據(jù)時主要調(diào)用ExcelWriter.java來創(chuàng)建Excel工作簿對象并寫入數(shù)據(jù),這里以寫入系統(tǒng)文件數(shù)據(jù)為例演示寫入的過程:
2.3.1 主程序入口類代碼
/** * Author: Dreamer-1 * Date: 2019-03-01 * Time: 10:13 * Description: 示例程序入口類 */ public class MainTest { private static Logger logger = Logger.getLogger(MainTest.class.getName()); public static void main(String[] args) { // 創(chuàng)建需要寫入的數(shù)據(jù)列表 List<ExcelDataVO> dataVOList = new ArrayList<>(2); ExcelDataVO dataVO = new ExcelDataVO(); dataVO.setName("小明"); dataVO.setAge(18); dataVO.setLocation("廣州"); dataVO.setJob("大學(xué)生"); ExcelDataVO dataVO2 = new ExcelDataVO(); dataVO2.setName("小花"); dataVO2.setAge(19); dataVO2.setLocation("深圳"); dataVO2.setJob("大學(xué)生"); dataVOList.add(dataVO); dataVOList.add(dataVO2); // 寫入數(shù)據(jù)到工作簿對象內(nèi) Workbook workbook = ExcelWriter.exportData(dataVOList); // 以文件的形式輸出工作簿對象 FileOutputStream fileOut = null; try { String exportFilePath = "/Users/Dreamer-1/Desktop/myBlog/java解析Excel/writeExample.xlsx"; File exportFile = new File(exportFilePath); if (!exportFile.exists()) { exportFile.createNewFile(); } fileOut = new FileOutputStream(exportFilePath); workbook.write(fileOut); fileOut.flush(); } catch (Exception e) { logger.warning("輸出Excel時發(fā)生錯誤,錯誤原因:" + e.getMessage()); } finally { try { if (null != fileOut) { fileOut.close(); } if (null != workbook) { workbook.close(); } } catch (IOException e) { logger.warning("關(guān)閉輸出流時發(fā)生錯誤,錯誤原因:" + e.getMessage()); } } } }
2.3.2 寫入Excel類的代碼
ExcelWriter.java類中,你可以根據(jù)實際需要替換 CELL_HEADS 列頭的信息,然后重寫 convertDataToRow 方法,轉(zhuǎn)換你自己的行數(shù)據(jù);
/** * Author: Dreamer-1 * Date: 2019-03-01 * Time: 11:09 * Description: 生成Excel并寫入數(shù)據(jù) */ public class ExcelWriter { private static List<String> CELL_HEADS; //列頭 static{ // 類裝載時就載入指定好的列頭信息,如有需要,可以考慮做成動態(tài)生成的列頭 CELL_HEADS = new ArrayList<>(); CELL_HEADS.add("姓名"); CELL_HEADS.add("年齡"); CELL_HEADS.add("居住城市"); CELL_HEADS.add("職業(yè)"); } /** * 生成Excel并寫入數(shù)據(jù)信息 * @param dataList 數(shù)據(jù)列表 * @return 寫入數(shù)據(jù)后的工作簿對象 */ public static Workbook exportData(List<ExcelDataVO> dataList){ // 生成xlsx的Excel Workbook workbook = new SXSSFWorkbook(); // 如需生成xls的Excel,請使用下面的工作簿對象,注意后續(xù)輸出時文件后綴名也需更改為xls //Workbook workbook = new HSSFWorkbook(); // 生成Sheet表,寫入第一行的列頭 Sheet sheet = buildDataSheet(workbook); //構(gòu)建每行的數(shù)據(jù)內(nèi)容 int rowNum = 1; for (Iterator<ExcelDataVO> it = dataList.iterator(); it.hasNext(); ) { ExcelDataVO data = it.next(); if (data == null) { continue; } //輸出行數(shù)據(jù) Row row = sheet.createRow(rowNum++); convertDataToRow(data, row); } return workbook; } /** * 生成sheet表,并寫入第一行數(shù)據(jù)(列頭) * @param workbook 工作簿對象 * @return 已經(jīng)寫入列頭的Sheet */ private static Sheet buildDataSheet(Workbook workbook) { Sheet sheet = workbook.createSheet(); // 設(shè)置列頭寬度 for (int i=0; i<CELL_HEADS.size(); i++) { sheet.setColumnWidth(i, 4000); } // 設(shè)置默認(rèn)行高 sheet.setDefaultRowHeight((short) 400); // 構(gòu)建頭單元格樣式 CellStyle cellStyle = buildHeadCellStyle(sheet.getWorkbook()); // 寫入第一行各列的數(shù)據(jù) Row head = sheet.createRow(0); for (int i = 0; i < CELL_HEADS.size(); i++) { Cell cell = head.createCell(i); cell.setCellValue(CELL_HEADS.get(i)); cell.setCellStyle(cellStyle); } return sheet; } /** * 設(shè)置第一行列頭的樣式 * @param workbook 工作簿對象 * @return 單元格樣式對象 */ private static CellStyle buildHeadCellStyle(Workbook workbook) { CellStyle style = workbook.createCellStyle(); //對齊方式設(shè)置 style.setAlignment(HorizontalAlignment.CENTER); //邊框顏色和寬度設(shè)置 style.setBorderBottom(BorderStyle.THIN); style.setBottomBorderColor(IndexedColors.BLACK.getIndex()); // 下邊框 style.setBorderLeft(BorderStyle.THIN); style.setLeftBorderColor(IndexedColors.BLACK.getIndex()); // 左邊框 style.setBorderRight(BorderStyle.THIN); style.setRightBorderColor(IndexedColors.BLACK.getIndex()); // 右邊框 style.setBorderTop(BorderStyle.THIN); style.setTopBorderColor(IndexedColors.BLACK.getIndex()); // 上邊框 //設(shè)置背景顏色 style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex()); style.setFillPattern(FillPatternType.SOLID_FOREGROUND); //粗體字設(shè)置 Font font = workbook.createFont(); font.setBold(true); style.setFont(font); return style; } /** * 將數(shù)據(jù)轉(zhuǎn)換成行 * @param data 源數(shù)據(jù) * @param row 行對象 * @return */ private static void convertDataToRow(ExcelDataVO data, Row row){ int cellNum = 0; Cell cell; // 姓名 cell = row.createCell(cellNum++); cell.setCellValue(null == data.getName() ? "" : data.getName()); // 年齡 cell = row.createCell(cellNum++); if (null != data.getAge()) { cell.setCellValue(data.getAge()); } else { cell.setCellValue(""); } // 所在城市 cell = row.createCell(cellNum++); cell.setCellValue(null == data.getLocation() ? "" : data.getLocation()); // 職業(yè) cell = row.createCell(cellNum++); cell.setCellValue(null == data.getJob() ? "" : data.getJob()); } }
示例程序運行后將會在指定的系統(tǒng)路徑下生成 writeExample.xlsx文件,其內(nèi)容如下:
2.3.3 應(yīng)用場景補充
一般寫入Excel時會有這樣的場景:前臺頁面上有一個導(dǎo)出按鈕,點擊后將后臺某張表里的數(shù)據(jù)以Excel的形式導(dǎo)出,導(dǎo)出的Excel文件通過瀏覽器下載到用戶系統(tǒng)中;
這里默認(rèn)前臺頁面已經(jīng)有相應(yīng)的按鈕功能,給出對應(yīng)的Controller層代碼供參考:
@GetMapping("/exportExcel") public void exportExcel(HttpServletRequest request, HttpServletResponse response) { Workbook workbook = null; OutputStream out = null; try { // todo 根據(jù)業(yè)務(wù)需求獲取需要寫入Excel的數(shù)據(jù)列表 dataList // 生成Excel工作簿對象并寫入數(shù)據(jù) workbook = ExcelWriter.exportData(dataList); // 寫入Excel文件到前端 if(null != workbook){ String excelName = "示例Excel導(dǎo)出"; String fileName = excelName + DateUtil.format(new Date(), DateUtil.SHORT_DATE) + ".xlsx"; fileName = new String(fileName.getBytes("UTF-8"),"iso8859-1"); response.setHeader("Content-Disposition", "attachment;filename=" + fileName); response.setContentType("application/x-download"); response.setCharacterEncoding("UTF-8"); response.addHeader("Pargam", "no-cache"); response.addHeader("Cache-Control", "no-cache"); response.flushBuffer(); out = response.getOutputStream(); workbook.write(out); out.flush(); } } catch (Exception e) { logger.WARNING("寫入Excel過程出錯!錯誤原因:" + e.getMessage()); } finally { try { if (null != workbook) { workbook.close(); } if (null != out) { out.close(); } } catch (IOException e) { logger.WARNING("關(guān)閉workbook或outputStream出錯!"); } } }
// 前臺頁面發(fā)送請求到后臺Controller時的JS代碼可參考: var url = "/exportExcel"; window.location=url;
3. 源碼下載
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
相關(guān)文章
解決Mybatis-plus和pagehelper依賴沖突的方法示例
這篇文章主要介紹了解決Mybatis-plus和pagehelper依賴沖突的方法示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04Java Scala偏函數(shù)與偏應(yīng)用函數(shù)超詳細(xì)講解
Scala是一種多范式的編程語言,支持面向?qū)ο蠛秃瘮?shù)式編程。Scala也支持異常處理,即在程序運行過程中發(fā)生意外或錯誤時,采取相應(yīng)的措施2023-04-04java利用遞歸調(diào)用實現(xiàn)樹形菜單的樣式
這篇文章主要給大家介紹了關(guān)于java利用遞歸調(diào)用實現(xiàn)樹形菜單樣式的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-09-09Spring Boot+Mybatis+Druid+PageHelper實現(xiàn)多數(shù)據(jù)源并分頁的方法
這篇文章主要給大家介紹了關(guān)于Spring Boot+Mybatis+Druid+PageHelper實現(xiàn)多數(shù)據(jù)源并分頁的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們來一起看看吧2018-05-05MAC?在類路徑或引導(dǎo)類路徑中找不到程序包?java.lang問題
這篇文章主要介紹了MAC?在類路徑或引導(dǎo)類路徑中找不到程序包?java.lang問題及解決,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-11-11