Java使用poi导出excel针对不同数据配置设置不同单元格格式

  • 背景
  • 第一版
    • 实现方案
  • 第二版
    • 理想中的方案
    • 可实行的方案
  • 结束

背景

公司大部分业务都是查询相关的业务, 所以建了一个项目专门做数据查询, 数据中转等抽象通用的业务, 有一天给我安排了一个功能, 做excel导出, 配置好查询sql和表头字段映射后即可导出excel, 无需修改代码
后来因为导出数据要求保留几位小数或者转换成百分比等设置单元格格式需要支持配置化, 由于做数据中转有些系统需要的数据是不需要约束小数的, 所以数据层面不能做约束, 否则就不是真正意义上的通用了

第一版

项目中使用了hutool工具类库, 封装了很多的方法, 大大的降低了开发成本
第一版代码, 想着hutool工具里面有个ExcelUtil的类, 里面的方法对使用poi导出excel进行了封装简化于是就这么写了

实现方案

此处以商品订单为例, 不考虑一笔订单多个商品的情况, 不做单元格合并操作
这里使用的Hutool版本是5.8.15

<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.15</version></dependency>

BaseFileUtils.java

public abstract class BaseFileUtils {/** * 创建xlsx表格 * @param fileName 文件名 * @param headList 列头信息 * @param dataList 数据集 * @param response / */public static void writeXlsxByData(String fileName, List<Object> headList, List<List<Object>> dataList, HttpServletResponse response) {//创建xlsx格式ExcelWriter writer = ExcelUtil.getBigWriter();for (int i = 0; i < dataList.size(); i++) {writer.setRowHeight(i, 30);}for (int i = 0; i < headList.size(); i++) {writer.setColumnWidth(i, 30);}writer.writeHeadRow(headList);// 一次性写出内容,使用默认样式,强制输出标题writer.write(dataList, false);writeXlsx2Response(response, writer, fileName);}/** * 将excel写出 * @param response / * @param writer excel信息 * @param fileName 文件名 */private static void writeXlsx2Response(HttpServletResponse response, ExcelWriter writer, String fileName) {//out为OutputStream,需要写出到的目标流response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");response.setHeader("Content-Disposition","attachment;filename=" + fileName);try {writer.flush(response.getOutputStream(), true);} catch (IOException e) {e.printStackTrace();}// 关闭writer,释放内存writer.close();}}

FileService.java

@Servicepublic class FileServiceImpl implements IFileService {@Overridepublic void exportExcel(HttpServletResponse response) {//列头信息List<Object> headList = new ArrayList<>(Arrays.asList("订单号", "商品名称", "单价", "数量", "总价", "占收入百分比"));//数据集List<Object> data1 = new ArrayList<>();data1.add("3123ab3412c3");data1.add("鼠标");data1.add(new BigDecimal("89.9"));data1.add(1);data1.add(new BigDecimal("89.9"));data1.add(new BigDecimal("0.1"));List<Object> data2 = new ArrayList<>();data2.add("3123ab3412c4");data2.add("键盘");data2.add(new BigDecimal("298.9"));data2.add(2);data2.add(new BigDecimal("597.8"));data2.add(new BigDecimal("0.321300"));List<Object> data3 = new ArrayList<>();data3.add("3123ab3412c5");data3.add("显卡");data3.add(new BigDecimal("2048.6"));data3.add(1);data3.add(new BigDecimal("2048.6"));data3.add(new BigDecimal("0.5423"));List<Object> data4 = new ArrayList<>();data4.add("3123ab3412c6");data4.add("显示器");data4.add(new BigDecimal("1999.9"));data4.add(1);data4.add(new BigDecimal("1999.9"));data4.add(new BigDecimal("0.36740"));List<List<Object>> dataList = new ArrayList<>(Arrays.asList(data1, data2, data3, data4));BaseFileUtils.writeXlsxByData("订单信息", headList, dataList, response);}

FileController.java

@RestController@RequestMapping("/file")public class FileController {@Resourceprivate IFileService fileService;@GetMapping("/excel")public void exportExcel(HttpServletResponse response) {fileService.exportExcel(response);}}

导出结果
到此第一版的实现已经完成了

第二版

第二版需要支持单元格格式配置话, 这里的配置是在列头名称的配置那里一起配置写入
原本的列头就只是一个普通字符串"订单号", "商品名称"`, "单价", ...
现在需要额外配置格式的需要改成json字符串, 自定义一些规则, 例如:

{"text": "总价","excel_format": {number_format: "0.0"}}{"text": "商品名称","excel_format": {ground_color: "red"}}{"text": "占收入百分比","excel_format": {number_format: "0.00%"}}

理想中的方案

当时因为是用了Hutool的工具包做的, 于是就想着看看这个工具里面是不是有封装一些修改单元格格式的工具方法, 于是找到了以下方法
这个是ExcelWriter对象的方法, 该方法可以获取对应的一些样式集
通过一下四个方法
根据需要的格式调用上述四个方法
所以基于第一版进行修改如下:
修改writeXlsxByData方法

/** * 创建xlsx表格 * * @param fileName 文件名 * @param headList 列头信息 * @param dataList 数据集 * @param response / */public static void writeXlsxByData(String fileName, List<Object> headList, List<List<Object>> dataList, HttpServletResponse response) {//创建xlsx格式ExcelWriter writer = ExcelUtil.getBigWriter();for (int i = 0; i < dataList.size(); i++) {writer.setRowHeight(i, 30);}List<Object> newHeadList = new ArrayList<>();for (int i = 0; i < headList.size(); i++) {writer.setColumnWidth(i, 30);//由于不知道这个列头的信息是普通字符串的, 还是JSON字符串, 所以这里通过强转捕获的形式来区分, 当然也可以使用字符串判断, 具体可以根据列头规则定义try {JSONObject jsonObject = JSONObject.parseObject((String) headList.get(i));String text = jsonObject.getString("text");newHeadList.add(text);JSONObject excelFormat = jsonObject.getJSONObject("excel_format");if (excelFormat != null) {String numberFormat = excelFormat.getString("number_format");String ratioFormat = excelFormat.getString("ratio_format");StyleSet styleSet = writer.getStyleSet();CellStyle cellStyle = null;//数字格式化if (StringUtils.isNotBlank(numberFormat)) {cellStyle = styleSet.getCellStyleForNumber();cellStyle.setDataFormat(HSSFDataFormat.getBuiltinFormat(numberFormat));}//百分比格式化if (StringUtils.isNotBlank(ratioFormat)) {cellStyle = styleSet.getCellStyleForNumber();cellStyle.setDataFormat(HSSFDataFormat.getBuiltinFormat(ratioFormat));}writer.setColumnStyleIfHasData(i, 1, cellStyle);}} catch (JSONException ignored) {newHeadList.add(headList.get(i));}}writer.writeHeadRow(newHeadList);// 一次性写出内容,使用默认样式,强制输出标题writer.write(dataList, false);writeXlsx2Response(response, writer, fileName);}

将刚刚写在service上的测试数据也改成配置格式

public void exportExcel(HttpServletResponse response) {//列头信息List<Object> headList = new ArrayList<>(Arrays.asList("订单号","""{"text": "商品名称","excel_format": {ground_color: "red"}}""","单价","数量","""{"text": "总价","excel_format": {number_format: "0.0"}}""","""{"text": "占收入百分比","excel_format": {"ratio_format": "0.00%"}}"""));//数据集List<Object> data1 = new ArrayList<>();data1.add("3123ab3412c3");data1.add("鼠标");data1.add(new BigDecimal("89.9"));data1.add(1);data1.add(new BigDecimal("89.9"));data1.add(new BigDecimal("0.1"));List<Object> data2 = new ArrayList<>();data2.add("3123ab3412c4");data2.add("键盘");data2.add(new BigDecimal("298.9"));data2.add(2);data2.add(new BigDecimal("597.8"));data2.add(new BigDecimal("0.321300"));List<Object> data3 = new ArrayList<>();data3.add("3123ab3412c5");data3.add("显卡");data3.add(new BigDecimal("2048.6"));data3.add(1);data3.add(new BigDecimal("2048.6"));data3.add(new BigDecimal("0.5423"));List<Object> data4 = new ArrayList<>();data4.add("3123ab3412c6");data4.add("显示器");data4.add(new BigDecimal("1999.9"));data4.add(1);data4.add(new BigDecimal("1999.9"));data4.add(new BigDecimal("0.36740"));List<List<Object>> dataList = new ArrayList<>(Arrays.asList(data1, data2, data3, data4));BaseFileUtils.writeXlsxByData("订单信息", headList, dataList, response);}

最后导出结果如下
看到结果发现虽然格式设置是没问题了, 但是只有最后一个设置的格式生效, 并且所有设置单元格的列都生效了百分比的格式, 很明显这不是理想中的结果
后面试过了好多方案都以失败告终了, 可能是这个每一次设置的样式都是全局生效的, 所以此时决定放弃使用Hutool封装的方法了, 还是老老实实的用原生POI做了

可实行的方案

导入poi依赖

<dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>5.2.2</version></dependency>

在BaseFileUtils.java中新写方法

/** * 创建excel文件写入数据并导出 * @param fileName 文件名 * @param headList 列头 * @param dataList 数据集 * @param response / */public static void writeXlsxForPoi(String fileName, List<Object> headList, List<List<Object>> dataList, HttpServletResponse response) {Workbook wb = WorkbookUtil.createBook(true);Sheet sheet = wb.createSheet();//设置列宽默认为25sheet.setDefaultColumnWidth(25);//创建第一行表头Row headRow = sheet.createRow(0);//创建的单元格格式CellStyle headCellStyle = wb.createCellStyle();CellStyle dataCellStyle = wb.createCellStyle();//构建列头格式buildHeadCellStyle(headCellStyle);//构建数据单元格格式buildDataCellStyle(dataCellStyle);//设置第一行--列名setHeadLine(headList, headRow, headCellStyle);//填充数据setDataInfo(dataList, headList, sheet, dataCellStyle, wb);//写出excelbuildExcelDocument(fileName, wb, response);}/** * 设置数据行信息 * @param dataList 数据值 * @param sheet / * @param dataCellStyle 格式信息 */private static void setDataInfo(List<List<Object>> dataList, List<Object> headList, Sheet sheet, CellStyle dataCellStyle, Workbook wb) {Cell cell;for (int i = 0; i < dataList.size(); i++) {Row row = sheet.createRow(i + 1);List<Object> data = dataList.get(i);for (int j = 0; j < data.size(); j++) {cell = row.createCell(j);Object value = data.get(j);if (value != null) {if (value instanceof Date) {cell.setCellValue(value.toString());}else if (value instanceof BigDecimal) {cell.setCellValue(((BigDecimal) value).doubleValue());} else if (value instanceof Double) {cell.setCellValue(Double.parseDouble(value.toString()));}else {cell.setCellValue(value.toString());}}//新建一个cellStyle, 将需要的样式信息复制过来, 用同一个样式对象最后会只生效最后一个CellStyle cellStyle = wb.createCellStyle();cellStyle.cloneStyleFrom(dataCellStyle);try {JSONObject jsonObject = JSONObject.parseObject((String) headList.get(j));JSONObject excelFormat = jsonObject.getJSONObject("excel_format");if (excelFormat != null) {String numberFormat = excelFormat.getString("number_format");String ratioFormat = excelFormat.getString("ratio_format");String groundColor = excelFormat.getString("ground_color");if (StringUtils.isNotBlank(numberFormat)) {cellStyle.setDataFormat(HSSFDataFormat.getBuiltinFormat(numberFormat));}if (StringUtils.isNotBlank(ratioFormat)) {cellStyle.setDataFormat(HSSFDataFormat.getBuiltinFormat(ratioFormat));}if (StringUtils.isNotBlank(groundColor)) {//设置单元格颜色cellStyle.setFillForegroundColor(HSSFColor.HSSFColorPredefined.RED.getIndex());//设置填充样式(实心填充),不设置填充样式不会有颜色cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);}}} catch (JSONException ignored) {}cell.setCellStyle(cellStyle);}}}/** * 设置第一行数据--列头 * @param headList 列头数据 * @param row 行信息 * @param headCellStyle 格式信息 */private static void setHeadLine(List<Object> headList, Row row, CellStyle headCellStyle) {Cell cell;for (int i = 0; i < headList.size(); i++) {cell = row.createCell(i);Object obj = headList.get(i);try {JSONObject jsonObject = JSONObject.parseObject((String) obj);cell.setCellValue(jsonObject.getString("text"));cell.setCellStyle(headCellStyle);} catch (JSONException e) {cell.setCellValue(headList.get(i).toString());cell.setCellStyle(headCellStyle);}}}/** * 构建第一行--列头格式信息 * @param cellStyle 格式信息 */private static void buildHeadCellStyle(CellStyle cellStyle) {//设置单元格居中cellStyle.setAlignment(HorizontalAlignment.CENTER);//设置单元格颜色cellStyle.setFillForegroundColor(HSSFColor.HSSFColorPredefined.GREY_25_PERCENT.getIndex());//设置填充样式(实心填充),不设置填充样式不会有颜色cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);//设置边框cellStyle.setBorderBottom(BorderStyle.THIN);cellStyle.setBorderLeft(BorderStyle.THIN);cellStyle.setBorderRight(BorderStyle.THIN);cellStyle.setBorderTop(BorderStyle.THIN);}/** * 构建数据行格式信息 * @param dataCellStyle 格式信息 */private static void buildDataCellStyle(CellStyle dataCellStyle) {//设置单元格居中dataCellStyle.setAlignment(HorizontalAlignment.CENTER);//设置边框dataCellStyle.setBorderBottom(BorderStyle.THIN);dataCellStyle.setBorderLeft(BorderStyle.THIN);dataCellStyle.setBorderRight(BorderStyle.THIN);dataCellStyle.setBorderTop(BorderStyle.THIN);}/** * 响应写出excel * @param fileName 文件名 * @param wb 工作簿 * @param response HttpServletResponse */private static void buildExcelDocument(String fileName, Workbook wb, HttpServletResponse response){try {response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");response.setHeader("Content-Disposition", "attachment;filename="+ fileName);OutputStream outputStream = response.getOutputStream();wb.write(outputStream);outputStream.flush();outputStream.close();wb.close();} catch (IOException e) {logger.error(e.getMessage(),e);}}

最后将FileService.java中调用writeXlsxByData()方法改为调用writeXlsxForPoi()方法即可;
导出结果
正是理想中的结果

主要的关键点在于每一次设置样式都要创建一个新的样式对象, 否则仍会出现第一个方案中全局生效最后一个设置的样式一样的结果, 若是有需要默认的全局样式可以通过cellStyle.cloneStyleFrom()方法将原有的格式进行克隆保留原有样式, 使用第一种方案的时候或许也可以以此改良, 有兴趣的可以试试!

结束

本次记录到此结束, 总的来说也算是有一点点小小的进步吧, 可能有不对的地方或者是有其他更好的方案希望各位大佬多多指点!