Java使用 POI 操作Excel


  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>

2.使用API

  • 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();
}

2.批量導入(上傳)

在添加數據時,通過批量導入可大大減少人力。但是批量導入需要代碼解析固定格式的模板,因此我們最好給用戶提供模板下載功能。我們同樣以導入用戶表為例:

統一 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();
}

Tips: DownUtils的download方法:

/**
 * 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();                            //刷數據
}

2.百萬數據報表導入

導入,其實就是讀取,讀取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哦!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM