Apache POI是一套基於 OOXML 標準(Office Open XML)和 OLE2 標準來讀寫各類格式文件的 Java API,也就是說只要是遵循以上標準的文件,POI 都可以進行讀寫,而不只僅只能操做咱們熟知的辦公程序文件。本文只會涉及到 excel 相關內容,其餘文件的操做能夠參考poi官方網站。html
這裏先總結下 POI 的使用體驗。POI 面向接口的設計很是巧妙,使用 ss.usermodel
包讀寫 xls 和 xlsx 時,可使用同一套代碼,即便這兩種文件格式採用的是徹底不一樣的標準。POI 提供了SXSSFWorkbook
用於解決 xlsx 寫大文件時容易出現的 OOM 問題。可是,仍是存在如下不足(都只針對讀場景):java
針對以上問題,阿里的 easyexcel 對 POI 進行高級封裝,提供了一套很是簡便的 API,其中,讀部分只封裝了 SAX 部分 API,事實上,使用 easyexcel 讀 excel 只會採用 SAX 方式,另外,easyexcel 重寫了 POI 對 xlsx 的解析,可以本來一個3M的 excel 用 POI SAX 依然須要100M左右內存下降到幾M,easyexcel 的內容本文也會涉及到。mysql
OLE2 和 OOXML 本質上都是一種文件格式規範或標準,平時看到的 excel 中,有字體、公式、顏色、圖片等等,看起來很是複雜,可是在文件結構上都遵循着固定的格式。git
OLE2 文件通常包括 xls、doc、ppt 等,是二進制格式的文件。 相關內容能夠參考:複合文檔Ole對象二進制儲存格式。github
OOXML文件通常包括 xlsx、docx、pptx 等。該類文件以指定格式的 xml 爲基礎並以 zip 格式壓縮,這裏我利用解壓工具解壓本地的一個 xml 文件,能夠看到如下文件結構,在本文例子中,咱們會重點關注 sharedStrings.xml 和 sheet1.xml 的內容,由於使用 SAX API 時必須用到:sql
針對不一樣應用的文件,使用時須要引入對應的 maven 依賴,這裏給出官方給出的指引。若是咱們不使用 SAX API 方式讀寫 excel,通常只會用到這個 org.apache.poi.ss 中的 API,具體的實現類放在 org.apache.poi.hssf 或 org.apache.poi.xssf 。數據庫
組件 | 做用 | Maven依賴 |
---|---|---|
POIFS | OLE2 Filesystem | poi |
HPSF | OLE2 Property Sets | poi |
HSSF | Excel XLS | poi |
HSLF | PowerPoint PPT | poi-scratchpad |
HWPF | Word DOC | poi-scratchpad |
HDGF | Visio VSD | poi-scratchpad |
HPBF | Publisher PUB | poi-scratchpad |
HSMF | Outlook MSG | poi-scratchpad |
DDF | Escher common drawings | poi |
HWMF | WMF drawings | poi-scratchpad |
OpenXML4J | OOXML | poi-ooxml plus either poi-ooxml-schemas or ooxml-schemas and ooxml-security |
XSSF | Excel XLSX | poi-ooxml |
XSLF | PowerPoint PPTX | poi-ooxml |
XWPF | Word DOCX | poi-ooxml |
XDGF | Visio VSDX | poi-ooxml |
Common SL | PowerPoint PPT 和 PPTX 共用組件 | poi-scratchpad and poi-ooxml |
Common SS | Excel XLS 和 XLSX 共用組件 | poi-ooxml |
JDK:1.8.0_201apache
maven:3.6.1xss
IDE:Spring Tool Suite 4.3.2.RELEASEmaven
POI:4.1.2
easyexcel:2.1.6
mysql:5.7.28
項目類型Maven Project,打包方式 jar。
<!-- junit --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <!-- poi-ooxml --> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>4.1.2</version> </dependency> <!-- easyexcel --> <dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>2.1.6</version> </dependency> <!-- hikari --> <dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>2.6.1</version> </dependency> <!-- mysql驅動 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.15</version> </dependency>
讀取指定 excel 第一個單元格的內容。該指定文件第一個單元格內容爲「測試」。
建議採用WorkbookFactory
來獲取Workbook
實例,而不是根據文件類型寫死具體的實現類。另外,獲取單元格對象時建議採用 SheetUtil
獲取,裏面會對行對象進行判空操做。
@Test public void test01() throws IOException { // 處理XSSF String path = "extend\\file\\poi_test_01.xlsx"; // 處理HSSF //String path = "extend\\file\\poi_test_01.xls"; // 建立工做簿,會根據excel命名選擇不一樣的Workbook實現類 Workbook wb = WorkbookFactory.create(new File(path)); // 獲取工做表 Sheet sheet = wb.getSheetAt(0); // 獲取行 Row row = sheet.getRow(0); // 獲取單元格 Cell cell = row.getCell(0); // 也能夠採用如下方式獲取單元格 // Cell cell = SheetUtil.getCell(sheet, 0, 0); // 獲取單元格內容 String value = cell.getStringCellValue(); System.err.println("第一個單元格字符:" + value); // 釋放資源 wb.close(); }
運行以上方法,控制檯打印出第一個單元格的內容:
生成一個 excel 文件,並給第一個單元格賦值爲"測試",並設置列寬 26,行高 20.25,內容居中,下框線,單元格橙色填充。
CellUtil
是 POI 自帶的工具類,這裏簡化了三句代碼(建立單元格,設置樣式,賦值)。注意,當寫入 xlsx 的大文件時,能夠考慮使用SXSSFWorkbook
來避免 OOM。
@Test public void test01() throws FileNotFoundException, IOException { // 處理XSSF String path = "extend\\file\\poi_test_01.xlsx"; // 處理HSSF // String path = "extend\\file\\poi_test_01.xls"; // 建立工做簿 boolean flag = path.endsWith(".xlsx"); Workbook wb = WorkbookFactory.create(flag ? true : false); // Workbook wb = new SXSSFWorkbook(100);//內存僅保留100行數據,可避免OOM // 建立工做表 Sheet sheet = wb.createSheet(WorkbookUtil.createSafeSheetName("MySheet001")); // 設置列寬 sheet.setColumnWidth(0, 26 * 256); // 建立行(索引從0開始) Row row = sheet.createRow(0); // 設置行高 row.setHeightInPoints(20.25f); // 建立單元格樣式對象 CellStyle style = wb.createCellStyle(); // 設置樣式 style.setAlignment(HorizontalAlignment.CENTER); // 橫向居中 style.setVerticalAlignment(VerticalAlignment.CENTER);// 縱向居中 style.setBorderBottom(BorderStyle.THIN); style.setFillForegroundColor(IndexedColors.ORANGE.getIndex()); style.setFillPattern(FillPatternType.SOLID_FOREGROUND); // 建立單元格、設置樣式和內容 CellUtil.createCell(row, 0, "測試", style); // 保存到本地目錄 OutputStream out = new FileOutputStream(new File(path)); wb.write(out); // 釋放資源 out.close(); wb.close(); }
運行以上方法,指定路徑下生成了 excel 文件,並填充了第一個單元格:
將 excel 中的用戶數據導入到數據庫(sql 已提供,在當前項目的 extend/sql 下),數據格式以下:
該文件總計1000條數據,xls 大小 128 KB,xlsx 大小 40 KB,兩種類型文件內容一致。
通常 excel 的內容格式是提早約定好的,咱們知道用戶數據哪一列是用戶名,哪一列是電話號碼,因此,在獲取單元格數據後能夠準確地轉換,但這種方式須要針對不一樣的對象分別定義一個轉換方法。
@Test public void test02() throws SQLException, IOException { // 處理XSSF String path = "extend\\file\\user_data.xlsx"; // 處理HSSF //String path = "extend\\file\\user_data.xls"; // 定義集合,用於存放excel中的用戶數據 List<UserDTO> list = new ArrayList<>(); InputStream in = new FileInputStream(path); // 建立工做簿 Workbook wb = WorkbookFactory.create(in); // 獲取工做表 Sheet sheet = wb.getSheetAt(0); // 獲取全部行 Iterator<Row> iterator = sheet.iterator(); int rowNum = 0; // 遍歷行 while(iterator.hasNext()) { Row row = iterator.next(); // 跳過標題行 if(rowNum == 0 || rowNum == 1) { rowNum++; continue; } // 將用戶對象保存到集合中 list.add(constructUserByRow(row)); } // 批量保存 new UserService().save(list); // 釋放資源 in.close(); wb.close(); } /** * <p>經過行數據構造用戶對象</p> */ private UserDTO constructUserByRow(Row row) { UserDTO userDTO = new UserDTO(); Cell cell = null; // 用戶名 cell = row.getCell(1); userDTO.setName(cell.getStringCellValue()); // 性別 cell = row.getCell(2); userDTO.setGenderStr(cell.getStringCellValue()); // 年齡 cell = row.getCell(3); userDTO.setAge(((Double)cell.getNumericCellValue()).intValue()); // 電話 cell = row.getCell(4); userDTO.setPhone(cell.getStringCellValue()); return userDTO; }
運行以上方法,能夠在數據庫看到導入的數據:
將數據庫的用戶數據導出到excel中。這個例子使用模板進行導出,模板以下(若是是 xlsx 的大文件,爲了可以使用SXSSFWorkbook
最好不要用模板)。
寫入的時候使用樣式仍是比較繁瑣,實際開發能不使用盡可能不要用,或者也能夠單獨封裝成一個方法。注意,構造Workbook
時不要使用WorkbookFactory.create(file)
方式,不然,模板也會被修改。
@Test public void test02() throws SQLException, IOException { // 處理XSSF String templatePath = "extend\\file\\user_data_template.xlsx"; String outpath = "extend\\file\\user_data.xlsx"; // 處理HSSF // String templatePath = "extend\\file\\user_data_template.xls"; // String path = "extend\\file\\user_data.xls"; InputStream in = new FileInputStream(templatePath); // 建立工做簿,注意,這裏若是傳入File對象,模板也會被改寫 Workbook wb = WorkbookFactory.create(in); // 讀取工做表 Sheet sheet = wb.getSheetAt(0); // 定義複用變量 int rowIndex = 0; // 行的索引 int cellIndex = 1; // 單元格的索引 Row nRow = null; Cell nCell = null; // 讀取大標題行 nRow = sheet.getRow(rowIndex++); // 使用後 +1 // 讀取大標題的單元格 nCell = nRow.getCell(cellIndex); // 設置大標題的內容 nCell.setCellValue("2020年2月用戶表"); // 跳過第二行(模板的小標題) rowIndex++; // 讀取第三行,獲取它的樣式 nRow = sheet.getRow(rowIndex); // 讀取行高 float lineHeight = nRow.getHeightInPoints(); // 獲取第三行的4個單元格中的樣式 CellStyle cs1 = nRow.getCell(cellIndex++).getCellStyle(); CellStyle cs2 = nRow.getCell(cellIndex++).getCellStyle(); CellStyle cs3 = nRow.getCell(cellIndex++).getCellStyle(); CellStyle cs4 = nRow.getCell(cellIndex++).getCellStyle(); // 查詢用戶列表 List<UserDTO> userList = new UserService().findAll().stream().map((x) -> new UserDTO(x)).collect(Collectors.toList()); // 遍歷數據 for(UserDTO user : userList) { // 建立數據行 nRow = sheet.createRow(rowIndex++); // 設置數據行高 nRow.setHeightInPoints(lineHeight); // 重置cellIndex,從第一列開始寫數據 cellIndex = 1; // 建立數據單元格,設置單元格內容和樣式 // 用戶名 nCell = nRow.createCell(cellIndex++); nCell.setCellStyle(cs1); nCell.setCellValue(user.getName()); // 性別 nCell = nRow.createCell(cellIndex++); nCell.setCellStyle(cs2); nCell.setCellValue(user.getGenderStr()); // 年齡 nCell = nRow.createCell(cellIndex++); nCell.setCellStyle(cs3); nCell.setCellValue(user.getAge()); // 手機號 nCell = nRow.createCell(cellIndex++); nCell.setCellStyle(cs4); nCell.setCellValue(user.getPhone()); } // 保存到本地目錄 OutputStream out = new FileOutputStream(new File(outpath)); wb.write(out); // 釋放資源 out.close(); wb.close(); }
運行以上方法,在指定文件夾能夠看到生成的文件:
使用 SAX 的方式將 xls 中的用戶數據導入到數據庫,數據與以上例子同樣。
相比前面的例子,使用 SAX 方式內存佔用小,效率高,可是 POI 提供的這套 API 用起來很是繁瑣,使用時不得沒必要須去了解 xls 文件的結構。我這裏只是簡單展現,監聽器部分的代碼不太嚴謹,實際項目仍是用 easyexcel 來操做吧。
@Test public void test02() throws Exception { // 建立POIFSFileSystem String filename = "extend\\file\\user_data.xls"; POIFSFileSystem poifs = new POIFSFileSystem(new File(filename)); // 建立HSSFRequest,並添加自定義監聽器 HSSFRequest req = new HSSFRequest(); EventExample listener = new EventExample(); req.addListenerForAllRecords(listener); // 解析和觸發事件 HSSFEventFactory factory = new HSSFEventFactory(); factory.processWorkbookEvents(req, poifs); // 保存用戶到數據庫 new UserService().save(listener.getList()); poifs.close(); } private static class EventExample implements HSSFListener { private SSTRecord sstrec; private int lastCellRow = -1; private int lastCellColumn = -1; private List<UserDTO> list = new ArrayList<UserDTO>(); private UserDTO user; @Override public void processRecord(Record record) { switch(record.getSid()) { // 進入新的sheet case BoundSheetRecord.sid: lastCellRow = -1; lastCellColumn = -1; break; // excel中的數值類型和字符存放在不一樣的位置 case NumberRecord.sid: NumberRecord numrec = (NumberRecord)record; // 用戶年齡 user.setAge(Double.valueOf(numrec.getValue()).intValue()); lastCellRow = numrec.getRow(); lastCellColumn = numrec.getColumn(); break; // SSTRecords中存儲着excel中使用的字符,重複的會合併爲一個 case SSTRecord.sid: sstrec = (SSTRecord)record; break; // 讀取到單元格的字符 case LabelSSTRecord.sid: LabelSSTRecord lrec = (LabelSSTRecord)record; int thisRow = lrec.getRow(); // 用戶數據從第三行開始 if(thisRow >= 2) { // 進入新行時,原對象放入集合,並建立新對象 if(thisRow != lastCellRow) { if(user != null) { list.add(user); } user = new UserDTO(); } // 根據列數爲用戶對象設置屬性 switch(lrec.getColumn()) { case 1: // 用戶名 user.setName(sstrec.getString(lrec.getSSTIndex()).getString()); break; case 2: // 用戶性別 user.setGenderStr(sstrec.getString(lrec.getSSTIndex()).getString()); break; case 4: // 用戶電話 user.setPhone(sstrec.getString(lrec.getSSTIndex()).getString()); break; default: break; } lastCellRow = thisRow; lastCellColumn = lrec.getColumn(); } break; case EOFRecord.sid: // 最後一行讀取完後直接放入集合 if(lastCellRow != -1 && user != null && lastCellColumn == 4) { list.add(user); } break; default: break; } } public List<UserDTO> getList() { return list; } }
運行以上方法,能夠在數據庫看到導入的數據:
使用 SAX 的方式將 xlsx 中的用戶數據導入到數據庫,數據與以上例子同樣。
POI 針對 xlsx 的 SAX API 也是很是繁瑣,屬於很是低級的封裝,這裏居然須要使用 JDK 原生的 SAX 解析來處理事件,定義事件處理器時,我必須去了解 xml 的節點結構。和上面例子同樣,這裏也只是簡單地演示這套 API 的使用,具體代碼不太嚴謹,固然,實際開發咱們不會採用這種方式,建議仍是使用 easyexcel 吧。
@Test public void test01() throws Exception { String filename = "extend\\file\\user_data.xlsx"; OPCPackage pkg = OPCPackage.open(filename); XSSFReader r = new XSSFReader(pkg); // 獲取sharedStrings.xml的內容,這裏存放着excel中的字符 SharedStringsTable sst = r.getSharedStringsTable(); // 接下來就是採用SAX方式解析xml的過程 // 構造解析器,這裏會設置自定義的處理器 XMLReader parser = XMLHelper.newXMLReader(); SheetHandler handler = new SheetHandler(sst); parser.setContentHandler(handler); // 解析指定的sheet InputStream sheet2 = r.getSheet("rId1"); parser.parse(new InputSource(sheet2)); // 保存用戶到數據庫 new UserService().save(handler.getList()); // handler.getList().forEach(System.err::println); sheet2.close(); } private static class SheetHandler extends DefaultHandler { private SharedStringsTable sst; private String cellContents; private boolean cellContentsIsString; private int cellColumn = -1; private int cellRow = -1; List<UserDTO> list = new ArrayList<>(); UserDTO user; private SheetHandler(SharedStringsTable sst) { this.sst = sst; } @Override public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException { // 讀取到行 if("row".equals(name)) { cellRow++; if(cellRow >= 2) { // 換行時從新建立用戶實例 user = new UserDTO(); } } // 讀取到列 c => cell if("c".equals(name) && cellRow >= 2) { // 設置當前讀取到哪一列 char columnChar = attributes.getValue("r").charAt(0); switch(columnChar) { case 'B': cellColumn = 1; break; case 'C': cellColumn = 2; break; case 'D': cellColumn = 3; break; case 'E': cellColumn = 4; break; default: cellColumn = -1; break; } // 當前單元格中的值是否爲字符,是的話對應的值被放在SharedStringsTable中 if("s".equals(attributes.getValue("t"))) { cellContentsIsString = true; } } // Clear contents cache cellContents = ""; } @Override public void endElement(String uri, String localName, String name) throws SAXException { // 跳過標題 if(cellRow < 2) { return; } // v節點是c的子節點,表示單元格的值 if(name.equals("v")) { int idx; if(cellContentsIsString) { idx = Integer.parseInt(cellContents); } else { idx = Double.valueOf(cellContents).intValue(); } switch(cellColumn) { case 1: user.setName(sst.getItemAt(idx).getString()); break; case 2: user.setGenderStr(sst.getItemAt(idx).getString()); break; case 3: // 年齡的值是數值類型,不在SharedStringsTable中 user.setAge(idx); break; case 4: user.setPhone(sst.getItemAt(idx).getString()); break; default: break; } } // 讀取完一行,將用戶對象放入集合中 if("row".equals(name) && user != null) { list.add(user); } // 重置參數 if("c".equals(name)) { cellColumn = -1; cellContentsIsString = false; } } @Override public void characters(char[] ch, int start, int length) { cellContents += new String(ch, start, length); } public List<UserDTO> getList() { return list; } }
運行以上方法,能夠在數據庫看到導入的數據:
經過以上例子,咱們會發現,POI SAX 方式的 API 確實很是繁瑣,使用時我必須熟悉地掌握 OLE2 或 OOXML 的規範,纔可以使用。這是比較低層級的封裝。相比之下,ss.usermodel 的 API 要好用不少,可是這套 API 底層解析 方式有點相似 DOM,效率較低,且內存佔用較大。
前面已經講過,easyexcel 對 POI 進行了高級封裝,極大地方便了咱們讀寫 excel,並且只會採用 SAX 這種更快的方式來讀取,下面補充下如何使用 easyexcel 讀寫 excel。
使用 easyexcel 讀寫 excel 時,咱們不須要本身寫 row => entity 或者 entity => row 的方法,只要按照如下註解好就行。被@ExcelProperty
註解的屬性對應 row 中的具體內容,而被@ExcelIgnore
註解表示不須要與 row 進行轉換。
@ContentRowHeight(16) public class UserDTO implements Serializable { private static final long serialVersionUID = 1L; @ExcelIgnore private String id; /** * <p>用戶名</p> */ @ExcelProperty(value = { "用戶名" }, index = 1) private String name; /** * <p>性別</p> */ @ExcelProperty(value = { "性別" }, index = 2) private String genderStr; /** * <p>年齡</p> */ @ExcelProperty(value = { "年齡" }, index = 3) private Integer age; /** * <p>電話號碼</p> */ @ExcelProperty(value = { "手機號" }, index = 4) @ColumnWidth(14) private String phone; @ExcelIgnore private Integer gender = 0; // 如下省略setter/getter方法 }
easyexcel 封裝或重寫了 POI SAX 部分的 API,因此也是須要設置回調的監聽器,如下方式會採用默認的監聽器,並返回封裝好的對象。
@Test public void test02() throws SQLException, IOException { // XSSF String path = "D:\\growUp\\git_repository\\09-poi-demo\\extend\\file\\user_data.xlsx"; // HSSF // String path = "D:\\growUp\\git_repository\\09-poi-demo\\extend\\file\\user_data.xls"; // 讀取excel List<UserDTO> list = EasyExcel.read(path).head(UserDTO.class).sheet(0).headRowNumber(2).doReadSync(); // 保存 new UserService().save(list); }
固然,咱們也能夠採用自定義的監聽器,以下:
@Test public void test01() throws SQLException, IOException { // XSSF String path = "D:\\growUp\\git_repository\\09-poi-demo\\extend\\file\\user_data.xlsx"; // HSSF // String path = "D:\\growUp\\git_repository\\09-poi-demo\\extend\\file\\user_data.xls"; List<UserDTO> list = new ArrayList<UserDTO>(); // 定義回調監聽器 ReadListener<UserDTO> syncReadListener = new AnalysisEventListener<UserDTO>() { @Override public void invoke(UserDTO data, AnalysisContext context) { list.add(data); } @Override public void doAfterAllAnalysed(AnalysisContext context) { // TODO Auto-generated method stub } }; // 讀取excel EasyExcel.read(path, UserDTO.class, syncReadListener).sheet(0).headRowNumber(2).doRead(); // 保存 new UserService().save(list); }
和讀同樣,這裏也只用了一行代碼就完成了對 excel 的操做。
@Test public void test01() throws SQLException, IOException { // XSSF String path = "D:\\growUp\\git_repository\\09-poi-demo\\extend\\file\\user_data.xlsx"; // HSSF // String path = "D:\\growUp\\git_repository\\09-poi-demo\\extend\\file\\user_data.xls"; // 獲取用戶數據 List<UserDTO> list = new UserService().findAll().stream().map((x) -> new UserDTO(x)).collect(Collectors.toList()); // 寫入excel EasyExcel.write(path, UserDTO.class).sheet(0).relativeHeadRowIndex(1).doWrite(list); }
Apache POI - the Java API for Microsoft Documents
本文爲原創文章,轉載請附上原文出處連接: http://www.javashuo.com/article/p-flxvizqo-v.html