POI生成EXCEL檔案

_楊瀚博發表於2018-06-25

一、背景

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

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

二、效果預覽

POI生成EXCEL檔案
三、資料格式

由於是生成Excel檔案,這裡值考慮生成xlsx格式的Excel檔案,資料多表頭預設考慮使用 | 表示,不在使用colspan rowspan作為。如需要表示兩列兩行,第一列合併表頭格式為: A|B,A|C生成的表格為

A
BC

前端通過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
  • 新增圖片資訊
  • 輸出檔案流

功能入口如下

@Override
	public void buildOutputStream() throws FileProducerException {
		// 處理傳入的JSON資料
		sheets = this.jsonData.getJSONArray(this.SHEETS);
		Iterator<Object> sheetIter = sheets.iterator();
		if (sheets.isEmpty()) {
			this.responseData.setErrcode(1001);
			this.responseData.setSuccess(false);
			this.responseData.setErrmsg("無資料可生成");
			throw new FileProducerException();
		}
		wb = new XSSFWorkbook();
		// 建立全域性格式
		JSONArray userStyles = this.jsonData.getJSONArray(this.USERSTYLES);
		this.initUserStyles(userStyles);
		this.initDefaultHeadStyle();

		XSSFSheet ws;
		JSONObject sheet;
		JSONArray sheetData;
		JSONArray sheetTitle;
		JSONArray sheetHead;
		JSONArray sheetFoot;
		JSONArray sheetImgs;

		String sheetName;
		int sheetIndex = 0;
		while (sheetIter.hasNext()) {
			sheet = (JSONObject) sheetIter.next();
			// 獲取sheet名稱
			sheetName = sheet.getString(this.SHEET_NAME);
			ws = wb.createSheet();
			if (StringUtils.isNotBlank(sheetName)) {
				wb.setSheetName(sheetIndex, sheetName);
			}
			int sheetRowIndex = 0;
			sheetTitle = sheet.getJSONArray(this.SHEET_TITLE);
			this.setMergeCells(ws, sheet.getJSONArray(this.SHEET_TITLE_MERGE),
					sheetRowIndex);
			sheetRowIndex = this.createRandom(ws, sheetTitle, sheetRowIndex);

			sheetHead = sheet.getJSONArray(this.SHEET_HEAD);
			sheetRowIndex = this.createHeadColumn(ws, sheetHead, sheetRowIndex);

			this.setMergeCells(ws, sheet.getJSONArray(this.SHEET_DATA_MERGE),
					sheetRowIndex);
			sheetData = sheet.getJSONArray(this.SHEET_DATA);
			sheetRowIndex = this.createData(ws, sheetData, sheetRowIndex);

			sheetFoot = sheet.getJSONArray(this.SHEET_FOOT);
			this.setMergeCells(ws, sheet.getJSONArray(this.SHEET_FOOT_MERGE),
					sheetRowIndex);
			sheetRowIndex = this.createRandom(ws, sheetFoot, sheetRowIndex);

			sheetImgs = sheet.getJSONArray(this.SHEET_IMG);

			this.setSheetImages(ws, sheetImgs);
		}

		// 返回輸出流
		try {
			ByteArrayOutputStream os = new ByteArrayOutputStream();
			wb.write(os);
			this.outStreams.add(os);
		} catch (IOException e) {
			throw new FileProducerException(e.getMessage(), e.getCause());
		}
	}
複製程式碼

生成單元格樣式物件,包括字型 邊框 背景 對齊方式

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;
	}
複製程式碼

建立字型樣式

private Font createFont(JSONObject fontCfg) {
		if (fontCfg == null)
			return null;
		XSSFFont font = wb.createFont();
		font.setFontName(fontCfg.getString(this.FONT_NAME));
		Boolean fontBoole = fontCfg.getBoolean(FONT_BLOD);
		if (fontBoole != null)
			font.setBold(fontBoole.booleanValue());
		fontBoole = fontCfg.getBoolean(this.FONT_ITALIC);
		if (fontBoole != null)
			font.setItalic(fontBoole.booleanValue());
		fontBoole = fontCfg.getBoolean(this.FONT_UNDERLINE);
		if (fontBoole != null && fontBoole.booleanValue() == true)
			font.setUnderline(FontUnderline.SINGLE.getByteValue());
		Short fontHeight = fontCfg.getShort(this.FONT_HEIGHT);
		if (fontHeight != null)
			font.setFontHeightInPoints(fontHeight);
		String colorStr = fontCfg.getString(this.FONT_COLOR);
		if (colorStr != null) {
			font.setColor(new XSSFColor(new Color(Integer.parseInt(
					colorStr.substring(1), 16))));
		}
		return font;
	}
複製程式碼

處理表頭,表過多表頭處理,採用 | 分割的方式,傳入head長度為列資料,name中有幾個 | 就知道表頭有幾行。所以針對表頭處理有以下幾個步驟

  • 生成預設列樣式
  • 填充所有列資料,求出最大行數
  • 橫向合併內容相同的單元
  • 縱向合併空白的單元格
private int createHeadColumn(XSSFSheet ws, JSONArray sheetHead,
			int sheetRowIndex) {
		if (sheetHead == null)
			return sheetRowIndex;
		Iterator<Object> headIter = sheetHead.iterator();
		JSONObject curHead = null;
		int colIndex = 0;
		Object colStyle = null;
		int colSize = sheetHead.size();
		headTypes = new String[colSize];
		headCellStyleKeys = new String[colSize];
		int[] headColLevel = new int[colSize];
		String colName = null;
		String[] colNameAry = null;
		int maxLevel = 0;
		int colLevel = 0;
		XSSFCell headCell = null;
		ArrayList<ArrayList<String>> headValueList = new ArrayList<ArrayList<String>>();
		while (headIter.hasNext()) {
			curHead = (JSONObject) headIter.next();
			// 處理預設樣式
			if (curHead.containsKey(this.COLUMN_STYLE)) {
				colStyle = curHead.get(this.COLUMN_STYLE);
				if (colStyle instanceof JSONObject) {
					headCellStyleKeys[colIndex] = this.COLUMNSTYLE_PREV
							+ colIndex;
					this.userStyles.put(headCellStyleKeys[colIndex],
							this.createCellStyle((JSONObject) colStyle));
				} else if (this.userStyles.containsKey(colStyle)) {
					headCellStyleKeys[colIndex] = (String) colStyle;
				}
			}
			// 處理預設列寬
			if (curHead.containsKey(this.COLUMN_WIDTH)) {
				ws.setDefaultColumnWidth(pixToExcelWdith(curHead
						.getIntValue(this.COLUMN_WIDTH)));
			}
			// 儲存列樣式
			if (curHead.containsKey(this.COLUMN_TYPE)) {
				headTypes[colIndex] = curHead.getString(this.COLUMN_TYPE);
			} else {
				headTypes[colIndex] = this.CELLTYPESTRING;
			}
			// 處理多表頭
			colName = curHead.getString(this.COLUMN_NAME);
			colNameAry = colName.split("\\|");
			colLevel = colNameAry.length;
			headColLevel[colIndex] = colLevel;
			if (colLevel > maxLevel) {
				maxLevel = colLevel;
			}
			for (int i = 0; i < colLevel; i++) {
				if (headValueList.size() <= i) {
					headValueList.add(new ArrayList<String>());
				}
				headValueList.get(i).add(colIndex, colNameAry[i]);
				XSSFRow row = ws.getRow(sheetRowIndex + i);
				if (row == null) {
					row = ws.createRow(sheetRowIndex + i);
				}
				headCell = row.createCell(colIndex);
				headCell.setCellValue(colNameAry[i]);
				headCell.setCellStyle(this.userStyles.get(this.HEADSTYLE_KEY));
			}
			colIndex++;
		}

		// 橫向合併
		Iterator<ArrayList<String>> a = headValueList.iterator();
		JSONArray headMerge = new JSONArray();
		String prev = "";
		String curent = null;
		int lRowIndex = 0;
		int startCol = 0;
		int mergeCol = 0;
		ArrayList<String> columnInfo = null;
		while (a.hasNext()) {
			startCol = 0;
			mergeCol = 0;
			prev = "";
			columnInfo = a.next();
			// 第三列才能知道,第一列和第二列是否合併
			columnInfo.add("");
			Iterator<String> b = columnInfo.iterator();
			XSSFCell lastRowCell = null;
			while (b.hasNext()) {
				curent = b.next();
				if (lRowIndex > 0) {
					lastRowCell = ws.getRow(sheetRowIndex + lRowIndex - 1)
							.getCell(startCol);
				}
				if (prev.equalsIgnoreCase(curent) && lRowIndex == 0) {
					ws.getRow(sheetRowIndex + lRowIndex).getCell(startCol)
							.setCellType(Cell.CELL_TYPE_BLANK);
					mergeCol++;
				} else if (prev.equalsIgnoreCase(curent)
						&& lRowIndex > 0
						&& StringUtils
								.isBlank(lastRowCell.getStringCellValue())) {
					ws.getRow(sheetRowIndex + lRowIndex).getCell(startCol)
							.setCellType(Cell.CELL_TYPE_BLANK);
					mergeCol++;
				} else {
					if (mergeCol > 0 && startCol > 0) {
						headMerge.add(String.format("%d,%d,%d,%d", lRowIndex,
								lRowIndex, startCol - mergeCol - 1,
								startCol - 1));
						mergeCol = 0;
					}
				}
				startCol++;
				prev = curent;
			}
			lRowIndex++;
		}
		for (int i = 0; i < colSize; i++) {
			if (headColLevel[i] < maxLevel) { // 存在列合併
				headMerge.add(String.format("%d,%d,%d,%d", headColLevel[i] - 1,
						maxLevel - 1, i, i));
				for (int r = headColLevel[i]; r < maxLevel; r++) {
					ws.getRow(sheetRowIndex + r)
							.createCell(i)
							.setCellStyle(
									this.userStyles.get(this.HEADSTYLE_KEY));
				}
			}
		}

		this.setMergeCells(ws, headMerge, sheetRowIndex);
		return sheetRowIndex + maxLevel;
	}

複製程式碼

新增圖片,預設採用單元格描點方式,將圖片固定指定的單元格區域內

private void addImg(XSSFSheet ws, JSONObject img, XSSFCreationHelper cHelper) {
		String imgBase64 = img.getString(this.SHEET_IMG_DATA);
		if (StringUtils.isBlank(imgBase64))
			return;
		String[] imgary = imgBase64.split(",");
		System.out.println(imgary[0]);
		byte[] imgByte = Base64.decodeBase64(imgary[1]);
		int imgIdx = wb.addPicture(imgByte, Workbook.PICTURE_TYPE_JPEG);
		XSSFDrawing drawImg = ws.createDrawingPatriarch();
		XSSFClientAnchor anchor = cHelper.createClientAnchor();
		int col = img.getIntValue(this.SHEET_IMG_COL);
		int row = img.getIntValue(this.SHEET_IMG_ROW);
		anchor.setCol1(col);
		anchor.setRow1(row);
		XSSFPicture pict = drawImg.createPicture(anchor, imgIdx);
		Integer colSpan = img.getInteger(this.SHEET_IMG_COLSPAN);
		if (colSpan == null)
			colSpan = 1;
		Integer rowSpan = img.getInteger(this.SHEET_IMG_ROWSPAN);
		if (rowSpan == null)
			rowSpan = 1;
		pict.resize(colSpan, rowSpan);
	}
複製程式碼

五、總結

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

  • 對於各資料區域資料,保持區域資料獨立性(資料索引值)
  • 對於圖片開始行和開始列,索引值是針對一個完整的sheet
  • 對於表頭區域,多表頭採用 | 分割,減少部分傳輸資料
  • excel中style為所有sheet共享樣式。

相關文章