POI生成EXCEL文件

POI生成EXCEL文件

一、背景

根據指定格式的JSON文件生成對應的excel文件,需求如下

  • 支持多sheet
  • 支持單元格合併
  • 支持插入圖片
  • 支持單元格樣式可定製
  • 需要 標題(title),表頭(head),數據(data) ,表尾(foot) 明確區分

二、效果預覽

Alt text

三、數據格式

由於是生成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 }
View Code

 生成單元格樣式對象,包括字體 邊框 背景 對齊方式

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;
}
View Code

創建字體樣式

 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 }
View Code

 處理表頭,表過多表頭處理,採用 | 分割的方式,傳入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 }
View Code

 添加圖片,默認採用單元格描點方式,將圖片固定指定的單元格區域內

 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 }
View Code

 

五、總結

這次通過傳入JSON對象生成樣式豐富的excel文件,對於POI操作office文檔又更加熟悉一些。相對於解析excel文檔,生成就不用考慮文件格式,如:兼容2003格式,考慮大文件sax方式解析。相對於js前端生成excel文件,增加了對生成後文件二次加工的可能性,所以在功能入口中,採用了生成二進制流的方式。文件生成好後,可以繼續發送郵件,上傳ftp等操作。
重點說明

  • 對於各數據區域數據,保持區域數據獨立性(數據索引值)
  • 對於圖片開始行和開始列,索引值是針對一個完整的sheet
  • 對於表頭區域,多表頭採用 | 分割,減少部分傳輸數據
  • excel中style爲所有sheet共享樣式。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章