POI生成EXCEL文件
一、背景
根據指定格式的JSON
文件生成對應的excel
文件,需求如下
- 支持多sheet
- 支持單元格合併
- 支持插入圖片
- 支持單元格樣式可定製
- 需要 標題(title),表頭(head),數據(data) ,表尾(foot) 明確區分
二、效果預覽
三、數據格式
由於是生成Excel文件,這裏值考慮生成xlsx格式的Excel文件,數據多表頭默認考慮使用 | 表示,不在使用colspan rowspan作爲。如需要表示兩列兩行,第一列合併表頭格式爲: A|B,A|C生成的表格爲
A B C 前端通過post的方式將需要生成的數據構造成符合要求的JSON文件提交跟後臺。根據以上需求定義JSON格式如下
{ "saveName": "生成Excel的文件名.xlsx", "userStyles": [{ "id": "1", //不能出現重複,在需要設置單元樣式的地方,可以直接將style賦值爲此值 "style": { "font": { //設置字體基本格式 "blod": true,//是否加粗 "italic": true, //是否傾斜 "color": "#FF0000",//字體顏色 "name": "微軟雅黑", //字體名稱 "height": 20 //大小 }, "fmtStr": "", //單元格格式,#,##0.00_);#,##0.00;0 千分位 "align": "",//水平對齊方式 left right center "valign": "",//垂直對齊方式 top center bottom "borderColor": "", //設置邊框顏色 如 #FF0000 "bgColor": "" //設置單元格填充顏色 } }], "sheets": [{ "sheetName": "", //sheet名稱 "title": [], // 對應Sheet標題區域數據 "titleMerge": [], //對應Sheet標題區域合併信息 "head": [{}], //表頭信息 "data": [], //數據信息 "dataMerge": [], //數據合併信息 "foot": [], //表尾信息 "footMerge": [], //表尾合併信息 "img": [] //圖片信息,需要將圖片轉換base64 }] }
簡要說明
- head 數組中爲JSON對象格式爲
{ "name": "A|B", //表頭名稱,多表頭用|分割 "type": "str", //此列數據類型 str num ,在excel中日期也是數字類型,通過fmtStr,顯示爲日期格式 "field": "F_FIELD1", //備用字段,可不用 "style": { //此列數據爲列默認樣式,可以是Style對象,也可以是在userStyles中定義的id值 "align": "center" } }
- 在數組 title data foot 中,列表中的數據,可以是一個單獨的值如 1,”a”,也可以是一個對象,當爲對象時,格式爲
{ "value": "", //單元格具體的值 "type": "", //單元格類型,默認str "style": {} //單元格樣式 可以是Style對象,也可以是在userStyles中定義的id值,如果沒設置,默認取head總此列對應的style }
- titleMerge、dataMerge、footMerge數組值爲逗號分隔的字符串,其含義爲
"開始行,結束行,開始列,結束列"
,索引從0開始。如在title中有兩行三列數據,現在需要合併一行兩列數據對應的值爲"0,0,0,1"
- img數組中值爲對象,格式
{ "col": 1, //圖片開始列 "row": 0, //開始行 "colSpan": 1,//列跨度,最小值1 "rowSpan": 2, //行跨度,最小值1 "data": "" //base64圖片數據如: "data:image/png;base64,iVBO...ggg==" }
四、關鍵實現
07以後的Excle文件,其實是一個壓縮包,裏邊是一個個的xml文件,其中每一個sheet是一個xml文件,樣式是一個xml文件,圖片是對應的圖片文件,放在media文件夾中,所以,代碼思路依次爲
- 構建 XSSFWorkbook 對象
- 生成樣式
- 依次生成,title head data foot 行數據
- 依次處理合並信息 titlemerge datamerge footmerge
- 添加圖片信息
- 輸出文件流
功能入口如下
1 @Override 2 public void buildOutputStream() throws FileProducerException { 3 // 處理傳入的JSON數據 4 sheets = this.jsonData.getJSONArray(this.SHEETS); 5 Iterator<Object> sheetIter = sheets.iterator(); 6 if (sheets.isEmpty()) { 7 this.responseData.setErrcode(1001); 8 this.responseData.setSuccess(false); 9 this.responseData.setErrmsg("無數據可生成"); 10 throw new FileProducerException(); 11 } 12 wb = new XSSFWorkbook(); 13 // 建立全局格式 14 JSONArray userStyles = this.jsonData.getJSONArray(this.USERSTYLES); 15 this.initUserStyles(userStyles); 16 this.initDefaultHeadStyle(); 17 18 XSSFSheet ws; 19 JSONObject sheet; 20 JSONArray sheetData; 21 JSONArray sheetTitle; 22 JSONArray sheetHead; 23 JSONArray sheetFoot; 24 JSONArray sheetImgs; 25 26 String sheetName; 27 int sheetIndex = 0; 28 while (sheetIter.hasNext()) { 29 sheet = (JSONObject) sheetIter.next(); 30 // 獲取sheet名稱 31 sheetName = sheet.getString(this.SHEET_NAME); 32 ws = wb.createSheet(); 33 if (StringUtils.isNotBlank(sheetName)) { 34 wb.setSheetName(sheetIndex, sheetName); 35 } 36 int sheetRowIndex = 0; 37 sheetTitle = sheet.getJSONArray(this.SHEET_TITLE); 38 this.setMergeCells(ws, sheet.getJSONArray(this.SHEET_TITLE_MERGE), 39 sheetRowIndex); 40 sheetRowIndex = this.createRandom(ws, sheetTitle, sheetRowIndex); 41 42 sheetHead = sheet.getJSONArray(this.SHEET_HEAD); 43 sheetRowIndex = this.createHeadColumn(ws, sheetHead, sheetRowIndex); 44 45 this.setMergeCells(ws, sheet.getJSONArray(this.SHEET_DATA_MERGE), 46 sheetRowIndex); 47 sheetData = sheet.getJSONArray(this.SHEET_DATA); 48 sheetRowIndex = this.createData(ws, sheetData, sheetRowIndex); 49 50 sheetFoot = sheet.getJSONArray(this.SHEET_FOOT); 51 this.setMergeCells(ws, sheet.getJSONArray(this.SHEET_FOOT_MERGE), 52 sheetRowIndex); 53 sheetRowIndex = this.createRandom(ws, sheetFoot, sheetRowIndex); 54 55 sheetImgs = sheet.getJSONArray(this.SHEET_IMG); 56 57 this.setSheetImages(ws, sheetImgs); 58 } 59 60 // 返回輸出流 61 try { 62 ByteArrayOutputStream os = new ByteArrayOutputStream(); 63 wb.write(os); 64 this.outStreams.add(os); 65 } catch (IOException e) { 66 throw new FileProducerException(e.getMessage(), e.getCause()); 67 } 68 }
生成單元格樣式對象,包括字體
邊框
背景
對齊方式
private XSSFCellStyle createCellStyle(JSONObject style) { XSSFCellStyle cellStyle = wb.createCellStyle(); // 設置字體 JSONObject font = style.getJSONObject(this.STYLE_FONT); Font excelFont = this.createFont(font); if (excelFont != null) { cellStyle.setFont(excelFont); } // border統一黑色 cellStyle.setBorderBottom(BorderStyle.THIN); cellStyle.setBorderTop(BorderStyle.THIN); cellStyle.setBorderLeft(BorderStyle.THIN); cellStyle.setBorderRight(BorderStyle.THIN); String borderColor = style.getString(this.BORDER_COLOR); if (StringUtils.isNotBlank(borderColor)) { XSSFColor xfBorderColor = new XSSFColor(new Color(Integer.parseInt( borderColor.substring(1), 16))); cellStyle.setBorderColor(BorderSide.BOTTOM, xfBorderColor); cellStyle.setBorderColor(BorderSide.TOP, xfBorderColor); cellStyle.setBorderColor(BorderSide.LEFT, xfBorderColor); cellStyle.setBorderColor(BorderSide.RIGHT, xfBorderColor); } // 背景色 String bgColor = style.getString(this.BACKGROUND_COLOR); if (StringUtils.isNotBlank(bgColor)) { XSSFColor cellBgColor = new XSSFColor(new Color(Integer.parseInt( bgColor.substring(1), 16))); cellStyle.setFillForegroundColor(cellBgColor); cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); } // 對齊方式 String hAlignment = style.getString(this.HALIGNMENT); if (StringUtils.isNotBlank(hAlignment)) cellStyle.setAlignment(HorizontalAlignment.valueOf(hAlignment .toUpperCase())); String vAlignment = style.getString(this.VALIGNMENT); if (StringUtils.isNotBlank(vAlignment)) cellStyle.setVerticalAlignment(VerticalAlignment.valueOf(vAlignment .toUpperCase())); // 自動換行TRUE cellStyle.setWrapText(true); // 格式 String fmt = style.getString(this.FMTSTRING); if (StringUtils.isNotBlank(fmt)) cellStyle.setDataFormat(wb.createDataFormat().getFormat(fmt)); return cellStyle; }
創建字體樣式
1 private Font createFont(JSONObject fontCfg) { 2 if (fontCfg == null) 3 return null; 4 XSSFFont font = wb.createFont(); 5 font.setFontName(fontCfg.getString(this.FONT_NAME)); 6 Boolean fontBoole = fontCfg.getBoolean(FONT_BLOD); 7 if (fontBoole != null) 8 font.setBold(fontBoole.booleanValue()); 9 fontBoole = fontCfg.getBoolean(this.FONT_ITALIC); 10 if (fontBoole != null) 11 font.setItalic(fontBoole.booleanValue()); 12 fontBoole = fontCfg.getBoolean(this.FONT_UNDERLINE); 13 if (fontBoole != null && fontBoole.booleanValue() == true) 14 font.setUnderline(FontUnderline.SINGLE.getByteValue()); 15 Short fontHeight = fontCfg.getShort(this.FONT_HEIGHT); 16 if (fontHeight != null) 17 font.setFontHeightInPoints(fontHeight); 18 String colorStr = fontCfg.getString(this.FONT_COLOR); 19 if (colorStr != null) { 20 font.setColor(new XSSFColor(new Color(Integer.parseInt( 21 colorStr.substring(1), 16)))); 22 } 23 return font; 24 }
處理表頭,表過多表頭處理,採用 | 分割的方式,傳入head長度爲列數據,name中有幾個 | 就知道表頭有幾行。所以針對表頭處理有以下幾個步驟
- 生成默認列樣式
- 填充所有列數據,求出最大行數
- 橫向合併內容相同的單元
- 縱向合併空白的單元格
1 private int createHeadColumn(XSSFSheet ws, JSONArray sheetHead, 2 int sheetRowIndex) { 3 if (sheetHead == null) 4 return sheetRowIndex; 5 Iterator<Object> headIter = sheetHead.iterator(); 6 JSONObject curHead = null; 7 int colIndex = 0; 8 Object colStyle = null; 9 int colSize = sheetHead.size(); 10 headTypes = new String[colSize]; 11 headCellStyleKeys = new String[colSize]; 12 int[] headColLevel = new int[colSize]; 13 String colName = null; 14 String[] colNameAry = null; 15 int maxLevel = 0; 16 int colLevel = 0; 17 XSSFCell headCell = null; 18 ArrayList<ArrayList<String>> headValueList = new ArrayList<ArrayList<String>>(); 19 while (headIter.hasNext()) { 20 curHead = (JSONObject) headIter.next(); 21 // 處理默認樣式 22 if (curHead.containsKey(this.COLUMN_STYLE)) { 23 colStyle = curHead.get(this.COLUMN_STYLE); 24 if (colStyle instanceof JSONObject) { 25 headCellStyleKeys[colIndex] = this.COLUMNSTYLE_PREV 26 + colIndex; 27 this.userStyles.put(headCellStyleKeys[colIndex], 28 this.createCellStyle((JSONObject) colStyle)); 29 } else if (this.userStyles.containsKey(colStyle)) { 30 headCellStyleKeys[colIndex] = (String) colStyle; 31 } 32 } 33 // 處理默認列寬 34 if (curHead.containsKey(this.COLUMN_WIDTH)) { 35 ws.setDefaultColumnWidth(pixToExcelWdith(curHead 36 .getIntValue(this.COLUMN_WIDTH))); 37 } 38 // 保存列樣式 39 if (curHead.containsKey(this.COLUMN_TYPE)) { 40 headTypes[colIndex] = curHead.getString(this.COLUMN_TYPE); 41 } else { 42 headTypes[colIndex] = this.CELLTYPESTRING; 43 } 44 // 處理多表頭 45 colName = curHead.getString(this.COLUMN_NAME); 46 colNameAry = colName.split("\\|"); 47 colLevel = colNameAry.length; 48 headColLevel[colIndex] = colLevel; 49 if (colLevel > maxLevel) { 50 maxLevel = colLevel; 51 } 52 for (int i = 0; i < colLevel; i++) { 53 if (headValueList.size() <= i) { 54 headValueList.add(new ArrayList<String>()); 55 } 56 headValueList.get(i).add(colIndex, colNameAry[i]); 57 XSSFRow row = ws.getRow(sheetRowIndex + i); 58 if (row == null) { 59 row = ws.createRow(sheetRowIndex + i); 60 } 61 headCell = row.createCell(colIndex); 62 headCell.setCellValue(colNameAry[i]); 63 headCell.setCellStyle(this.userStyles.get(this.HEADSTYLE_KEY)); 64 } 65 colIndex++; 66 } 67 68 // 橫向合併 69 Iterator<ArrayList<String>> a = headValueList.iterator(); 70 JSONArray headMerge = new JSONArray(); 71 String prev = ""; 72 String curent = null; 73 int lRowIndex = 0; 74 int startCol = 0; 75 int mergeCol = 0; 76 ArrayList<String> columnInfo = null; 77 while (a.hasNext()) { 78 startCol = 0; 79 mergeCol = 0; 80 prev = ""; 81 columnInfo = a.next(); 82 // 第三列才能知道,第一列和第二列是否合併 83 columnInfo.add(""); 84 Iterator<String> b = columnInfo.iterator(); 85 XSSFCell lastRowCell = null; 86 while (b.hasNext()) { 87 curent = b.next(); 88 if (lRowIndex > 0) { 89 lastRowCell = ws.getRow(sheetRowIndex + lRowIndex - 1) 90 .getCell(startCol); 91 } 92 if (prev.equalsIgnoreCase(curent) && lRowIndex == 0) { 93 ws.getRow(sheetRowIndex + lRowIndex).getCell(startCol) 94 .setCellType(Cell.CELL_TYPE_BLANK); 95 mergeCol++; 96 } else if (prev.equalsIgnoreCase(curent) 97 && lRowIndex > 0 98 && StringUtils 99 .isBlank(lastRowCell.getStringCellValue())) { 100 ws.getRow(sheetRowIndex + lRowIndex).getCell(startCol) 101 .setCellType(Cell.CELL_TYPE_BLANK); 102 mergeCol++; 103 } else { 104 if (mergeCol > 0 && startCol > 0) { 105 headMerge.add(String.format("%d,%d,%d,%d", lRowIndex, 106 lRowIndex, startCol - mergeCol - 1, 107 startCol - 1)); 108 mergeCol = 0; 109 } 110 } 111 startCol++; 112 prev = curent; 113 } 114 lRowIndex++; 115 } 116 for (int i = 0; i < colSize; i++) { 117 if (headColLevel[i] < maxLevel) { // 存在列合併 118 headMerge.add(String.format("%d,%d,%d,%d", headColLevel[i] - 1, 119 maxLevel - 1, i, i)); 120 for (int r = headColLevel[i]; r < maxLevel; r++) { 121 ws.getRow(sheetRowIndex + r) 122 .createCell(i) 123 .setCellStyle( 124 this.userStyles.get(this.HEADSTYLE_KEY)); 125 } 126 } 127 } 128 129 this.setMergeCells(ws, headMerge, sheetRowIndex); 130 return sheetRowIndex + maxLevel; 131 }
添加圖片,默認採用單元格描點方式,將圖片固定指定的單元格區域內
1 private void addImg(XSSFSheet ws, JSONObject img, XSSFCreationHelper cHelper) { 2 String imgBase64 = img.getString(this.SHEET_IMG_DATA); 3 if (StringUtils.isBlank(imgBase64)) 4 return; 5 String[] imgary = imgBase64.split(","); 6 System.out.println(imgary[0]); 7 byte[] imgByte = Base64.decodeBase64(imgary[1]); 8 int imgIdx = wb.addPicture(imgByte, Workbook.PICTURE_TYPE_JPEG); 9 XSSFDrawing drawImg = ws.createDrawingPatriarch(); 10 XSSFClientAnchor anchor = cHelper.createClientAnchor(); 11 int col = img.getIntValue(this.SHEET_IMG_COL); 12 int row = img.getIntValue(this.SHEET_IMG_ROW); 13 anchor.setCol1(col); 14 anchor.setRow1(row); 15 XSSFPicture pict = drawImg.createPicture(anchor, imgIdx); 16 Integer colSpan = img.getInteger(this.SHEET_IMG_COLSPAN); 17 if (colSpan == null) 18 colSpan = 1; 19 Integer rowSpan = img.getInteger(this.SHEET_IMG_ROWSPAN); 20 if (rowSpan == null) 21 rowSpan = 1; 22 pict.resize(colSpan, rowSpan); 23 }
五、總結
這次通過傳入JSON對象生成樣式豐富的excel文件,對於POI操作office文檔又更加熟悉一些。相對於解析excel文檔,生成就不用考慮文件格式,如:兼容2003格式,考慮大文件sax方式解析。相對於js前端生成excel文件,增加了對生成後文件二次加工的可能性,所以在功能入口中,採用了生成二進制流的方式。文件生成好後,可以繼續發送郵件,上傳ftp等操作。
重點說明
- 對於各數據區域數據,保持區域數據獨立性(數據索引值)
- 對於圖片開始行和開始列,索引值是針對一個完整的sheet
- 對於表頭區域,多表頭採用 | 分割,減少部分傳輸數據
- excel中style爲所有sheet共享樣式。