天天看點

Java 如何優雅的導出 Excel

雲栖号資訊:【 點選檢視更多行業資訊

在這裡您可以找到不同行業的第一手的上雲資訊,還在等什麼,快來!

前言

公司項目最近有一個需要:報表導出。整個系統下來,起碼超過一百張報表需要導出。這個時候如何優雅的實作報表導出,釋放生産力就顯得很重要了。下面主要給大家分享一下該工具類的使用方法與實作思路。

實作的功能點

對于每個報表都相同的操作,我們很自然的會抽離出來,這個很簡單。而最重要的是:如何把那些每個報表不相同的操作進行良好的封裝,盡可能的提高複用性;針對以上的原則,主要實作了一下關鍵功能點:

  • 導出任意類型的資料
  • 自由設定表頭
  • 自由設定字段的導出格式

使用執行個體

上面說到了本工具類實作了三個功能點,自然在使用的時候設定好這三個要點即可:

  • 設定資料清單
  • 設定表頭
  • 設定字段格式

下面的export函數可以直接向用戶端傳回一個excel資料,其中productInfoPos為待導出的資料清單,ExcelHeaderInfo用來儲存表頭資訊,包括表頭名稱,表頭的首列,尾列,首行,尾行。因為預設導出的資料格式都是字元串型,是以還需要一個Map參數用來指定某個字段的格式化類型(例如數字類型,小數類型、日期類型)。這裡大家知道個大概怎麼使用就好了,下面會對這些參數進行詳細解釋。

@Override
    public void export(HttpServletResponse response, String fileName) {
        // 待導出資料
        List<TtlProductInfoPo> productInfoPos = this.multiThreadListProduct();
        ExcelUtils excelUtils = new ExcelUtils(productInfoPos, getHeaderInfo(), getFormatInfo());
        excelUtils.sendHttpResponse(response, fileName, excelUtils.getWorkbook());
    }

    // 擷取表頭資訊
    private List<ExcelHeaderInfo> getHeaderInfo() {
        return Arrays.asList(
                new ExcelHeaderInfo(1, 1, 0, 0, "id"),
                new ExcelHeaderInfo(1, 1, 1, 1, "商品名稱"),

                new ExcelHeaderInfo(0, 0, 2, 3, "分類"),
                new ExcelHeaderInfo(1, 1, 2, 2, "類型ID"),
                new ExcelHeaderInfo(1, 1, 3, 3, "分類名稱"),

                new ExcelHeaderInfo(0, 0, 4, 5, "品牌"),
                new ExcelHeaderInfo(1, 1, 4, 4, "品牌ID"),
                new ExcelHeaderInfo(1, 1, 5, 5, "品牌名稱"),

                new ExcelHeaderInfo(0, 0, 6, 7, "商店"),
                new ExcelHeaderInfo(1, 1, 6, 6, "商店ID"),
                new ExcelHeaderInfo(1, 1, 7, 7, "商店名稱"),

                new ExcelHeaderInfo(1, 1, 8, 8, "價格"),
                new ExcelHeaderInfo(1, 1, 9, 9, "庫存"),
                new ExcelHeaderInfo(1, 1, 10, 10, "銷量"),
                new ExcelHeaderInfo(1, 1, 11, 11, "插入時間"),
                new ExcelHeaderInfo(1, 1, 12, 12, "更新時間"),
                new ExcelHeaderInfo(1, 1, 13, 13, "記錄是否已經删除")
        );
    }

    // 擷取格式化資訊
    private Map<String, ExcelFormat> getFormatInfo() {
        Map<String, ExcelFormat> format = new HashMap<>();
        format.put("id", ExcelFormat.FORMAT_INTEGER);
        format.put("categoryId", ExcelFormat.FORMAT_INTEGER);
        format.put("branchId", ExcelFormat.FORMAT_INTEGER);
        format.put("shopId", ExcelFormat.FORMAT_INTEGER);
        format.put("price", ExcelFormat.FORMAT_DOUBLE);
        format.put("stock", ExcelFormat.FORMAT_INTEGER);
        format.put("salesNum", ExcelFormat.FORMAT_INTEGER);
        format.put("isDel", ExcelFormat.FORMAT_INTEGER);
        return format;
    }           

實作效果

Java 如何優雅的導出 Excel

源碼分析

哈哈,自己分析自己的代碼,有點意思。由于不友善貼出太多的代碼,大家可以先到github上clone源碼,再回來閱讀文章。✨源碼位址✨LZ使用的poi 4.0.1版本的這個工具,想要實用海量資料的導出自然得使用SXSSFWorkbook這個元件。關于poi的具體用法在這裡我就不多說了,這裡主要是給大家講解如何對poi進行封裝使用。

成員變量

我們重點看ExcelUtils這個類,這個類是實作導出的核心,先來看一下三個成員變量。

Java 如何優雅的導出 Excel

list

該成員變量用來儲存待導出的資料。

ExcelHeaderInfo

該成員變量主要用來儲存表頭資訊,因為我們需要定義多個表頭資訊,是以需要使用一個清單來儲存,ExcelHeaderInfo構造函數如下ExcelHeaderInfo(int firstRow, int lastRow, int firstCol, int lastCol, String title)

  • firstRow:該表頭所占位置的首行
  • lastRow:該表頭所占位置的尾行
  • firstCol:該表頭所占位置的首列
  • lastCol:該表頭所占位置的尾行
  • title:該表頭的名稱

ExcelFormat

該參數主要用來格式化字段,我們需要預先約定好轉換成那種格式,不能随使用者自己定。是以我們定義了一個枚舉類型的變量,該枚舉類隻有一個字元串類型成員變量,用來儲存想要轉換的格式,例如FORMAT_INTEGER就是轉換成整型。因為我們需要接受多個字段的轉換格式,是以定義了一個Map類型來接收,該參數可以省略(預設格式為字元串)。

public enum ExcelFormat {

    FORMAT_INTEGER("INTEGER"),
    FORMAT_DOUBLE("DOUBLE"),
    FORMAT_PERCENT("PERCENT"),
    FORMAT_DATE("DATE");

    private String value;

    ExcelFormat(String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }
}           

核心方法

  1. 建立表頭
Java 如何優雅的導出 Excel
// 建立表頭
    private void createHeader(Sheet sheet, CellStyle style) {
        for (ExcelHeaderInfo excelHeaderInfo : excelHeaderInfos) {
            Integer lastRow = excelHeaderInfo.getLastRow();
            Integer firstRow = excelHeaderInfo.getFirstRow();
            Integer lastCol = excelHeaderInfo.getLastCol();
            Integer firstCol = excelHeaderInfo.getFirstCol();

            // 行距或者列距大于0才進行單元格融合
            if ((lastRow - firstRow) != 0 || (lastCol - firstCol) != 0) {
                sheet.addMergedRegion(new CellRangeAddress(firstRow, lastRow, firstCol, lastCol));
            }
            // 擷取目前表頭的首行位置
            Row row = sheet.getRow(firstRow);
            // 在表頭的首行與首列位置建立一個新的單元格
            Cell cell = row.createCell(firstCol);
            // 指派單元格
            cell.setCellValue(excelHeaderInfo.getTitle());
            cell.setCellStyle(style);
            sheet.setColumnWidth(firstCol, sheet.getColumnWidth(firstCol) * 17 / 12);
        }
    }           
  1. 轉換資料
Java 如何優雅的導出 Excel
// 将原始資料轉成二維數組
    private String[][] transformData() {
        int dataSize = this.list.size();
        String[][] datas = new String[dataSize][];
        // 擷取報表的列數
        Field[] fields = list.get(0).getClass().getDeclaredFields();
        // 擷取實體類的字段名稱數組
        List<String> columnNames = this.getBeanProperty(fields);
        for (int i = 0; i < dataSize; i++) {
            datas[i] = new String[fields.length];
            for (int j = 0; j < fields.length; j++) {
                try {
                    // 指派
                    datas[i][j] = BeanUtils.getProperty(list.get(i), columnNames.get(j));
                } catch (Exception e) {
                    LOGGER.error("擷取對象屬性值失敗");
                    e.printStackTrace();
                }
            }
        }
        return datas;
    }           

這個方法中我們通過使用反射技術,很巧妙的實作了任意類型的資料導出(這裡的任意類型指的是任意的報表類型,不同的報表,導出的資料肯定是不一樣的,那麼在Java實作中的實體類肯定也是不一樣的)。要想将一個List轉換成相應的二維數組,我們得知道如下的資訊:

  • 二維數組的列數
  • 二維數組的行數
  • 二維數組每個元素的值

如果擷取以上三個資訊呢?

  • 通過反射中的Field[] getDeclaredFields()這個方法擷取實體類的所有字段,進而間接知道一共有多少列
  • List的大小不就是二維數組的行數了嘛
  • 雖然每個實體類的字段名不一樣,那麼我們就真的無法擷取到實體類某個字段的值了嗎?不是的,你要知道,你擁有了反射,你就相當于擁有了全世界,那還有什麼做不到的呢。這裡我們沒有直接使用反射,而是使用了一個叫做BeanUtils的工具,該工具可以很友善的幫助我們對一個實體類進行字段的指派與字段值的擷取。很簡單,通過BeanUtils.getProperty(list.get(i), columnNames.get(j))這一行代碼,我們就擷取了實體list.get(i)中名稱為columnNames.get(j)這個字段的值。list.get(i)當然是我們周遊原始資料的實體類,而columnNames清單則是一個實體類所有字段名的數組,也是通過反射的方法擷取到的,具體實作可以參考LZ的源代碼。
  1. 指派正文
Java 如何優雅的導出 Excel
// 建立正文
    private void createContent(Row row, CellStyle style, String[][] content, int i, Field[] fields) {
        List<String> columnNames = getBeanProperty(fields);
        for (int j = 0; j < columnNames.size(); j++) {
            if (formatInfo == null) {
                row.createCell(j).setCellValue(content[i][j]);
                continue;
            }
            if (formatInfo.containsKey(columnNames.get(j))) {
                switch (formatInfo.get(columnNames.get(j)).getValue()) {
                    case "DOUBLE":
                        row.createCell(j).setCellValue(Double.parseDouble(content[i][j]));
                        break;
                    case "INTEGER":
                        row.createCell(j).setCellValue(Integer.parseInt(content[i][j]));
                        break;
                    case "PERCENT":
                        style.setDataFormat(HSSFDataFormat.getBuiltinFormat("0.00%"));
                        Cell cell = row.createCell(j);
                        cell.setCellStyle(style);
                        cell.setCellValue(Double.parseDouble(content[i][j]));
                        break;
                    case "DATE":
                        row.createCell(j).setCellValue(this.parseDate(content[i][j]));
                }
            } else {
                row.createCell(j).setCellValue(content[i][j]);
            }
        }
    }           

導出工具類的核心方法就差不多說完了,下面說一下關于多線程查詢的問題。

多扯兩點

  1. 多線程查詢資料
Java 如何優雅的導出 Excel

下面先說說具體思路:因為多個線程之間是同時執行的,你不能夠保證哪個線程先執行完畢,但是我們卻得保證資料順序的一緻性。在這裡我們使用了Callable接口,通過實作Callable接口的線程可以擁有傳回值,我們擷取到所有子線程的查詢結果,然後合并到一個結果集中即可。那麼如何保證合并的順序呢?我們先建立了一個FutureTask類型的List,該FutureTask的類型就是傳回的結果集。

Java 如何優雅的導出 Excel

接下來,就是順序塞值了,我們按順序從tasks清單中取出FutureTask,然後執行FutureTask的get()方法,該方法會阻塞調用它的線程,知道拿到傳回結果。這樣一套循環下來,就完成了所有資料的按順序存儲。

for (FutureTask<List<TtlProductInfoPo>> task : tasks) {
            try {
                productInfoPos.addAll(task.get());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }           
  1. 如何解決接口逾時

如果需要導出海量資料,可能會存在一個問題:接口逾時,主要原因就是整個導出過程的時間太長了。其實也很好解決,接口的響應時間太長,我們縮短響應時間不就可以了嘛。我們使用異步程式設計解決方案,異步程式設計的實作方式有很多,這裡我們使用最簡單的spring中的Async注解,加上了這個注解的方法可以立馬傳回響應結果。關于注解的使用方式,大家可以自己查閱一下,下面講一下關鍵的實作步驟:

  • 編寫異步接口,該接口負責接收用戶端的導出請求,然後開始執行導出(注意:這裡的導出不是直接向用戶端傳回,而是下載下傳到伺服器本地),隻要下達了導出指令,就可以馬上給用戶端傳回一個該excel檔案的唯一标志(用于以後查找該檔案),接口結束。
  • 編寫excel狀态接口,用戶端拿到excel檔案的唯一标志之後,開始每秒輪詢調用該接口檢查excel檔案的導出狀态
  • 編寫從伺服器本地傳回excel檔案接口,如果用戶端檢查到excel已經成功下載下傳到到伺服器本地,這個時候就可以請求該接口直接下載下傳檔案了。

這樣就可以解決接口逾時的問題了。

源碼位址

https://github.com/dearKundy/excel-utils

源碼服用姿勢

1.建表(資料自己插入哦)

CREATE TABLE `ttl_product_info` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '記錄唯一辨別',
  `product_name` varchar(50) NOT NULL COMMENT '商品名稱',
  `category_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '類型ID',
  `category_name` varchar(50) NOT NULL COMMENT '備援分類名稱-避免跨表join',
  `branch_id` bigint(20) NOT NULL COMMENT '品牌ID',
  `branch_name` varchar(50) NOT NULL COMMENT '備援品牌名稱-避免跨表join',
  `shop_id` bigint(20) NOT NULL COMMENT '商品ID',
  `shop_name` varchar(50) NOT NULL COMMENT '備援商店名稱-避免跨表join',
  `price` decimal(10,2) NOT NULL COMMENT '商品目前價格-屬于熱點資料,而且價格變化需要記錄,需要價格詳情表',
  `stock` int(11) NOT NULL COMMENT '庫存-熱點資料',
  `sales_num` int(11) NOT NULL COMMENT '銷量',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '插入時間',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
  `is_del` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '記錄是否已經删除',
  PRIMARY KEY (`id`),
  KEY `idx_shop_category_salesnum` (`shop_id`,`category_id`,`sales_num`),
  KEY `idx_category_branch_price` (`category_id`,`branch_id`,`price`),
  KEY `idx_productname` (`product_name`)
) ENGINE=InnoDB AUTO_INCREMENT=15000001 DEFAULT CHARSET=utf8 COMMENT='商品資訊表';           

運作程式

在浏覽器的位址欄輸入:

http://localhost:8080/api/excelUtils/export

即可完成下載下傳

【雲栖号線上課堂】每天都有産品技術專家分享!

課程位址:

https://yqh.aliyun.com/zhibo

立即加入社群,與專家面對面,及時了解課程最新動态!

【雲栖号線上課堂 社群】

https://c.tb.cn/F3.Z8gvnK

原文釋出時間:2020-05-09

本文作者: 你在我家門口

本文來自:“

網際網路架構師 微信公衆号

”,了解相關資訊可以關注“

網際網路架構師