编码技巧——使用原生Apache poi导出/导入多sheet、设置单元格格式、合并单元格

1. 背景

基于springboot的工程项目,需求:

(1)查询到的多组查询结果导出为excel,将每个组的数据集放在一个sheet页;

(2)分组的数量无法预先确定,是根据实时的查询结果得到的;意味着sheet数量不确定

(3)每一组数据的数据结构不一样,意味着每个sheet的表头列不确定,也是根据实时的查询结果得到的;

2. 方案

之前介绍过easypoi对多sheet的导入导出,可参考我之前的文章编码技巧——使用Easypoi导出Excel、多sheet、模板导出;

但是这次的需求不同,sheet数量和每个sheet的表头都不确定,无法通过预先定义的导出类+注解的方式调用easypoi的API来便捷导入导出

解决方案

(1)使用原生Apache poi的API,根据当前实时查询到的分组数据,遍历每组数据来创建对应的sheet;再根据每组数据的属性,来手动的设置表头;

(2)需要对表头设置样式,如加粗字体、设置底色;

(3)需要根据表头的字符长度,自动的设置单元格宽度,方便导出时查阅(无需手动拉宽);

(4)因为导出时的每个sheet是手动创建的,在设置sheetName时,通过分组标识来匹配对应的sheetName,这么做可以方便导入解析,取出各个分组的数据;

(5)因为每个sheet的表头是手动创建的,并且要求表头顺序、数量在导入时不被篡改,需要提供每个sheet的表头防篡改校验;

3. 代码

下面提供每个关键功能模块对应的代码(不提供完整的项目代码);

注:文中默认的excel格式为xlsx;

(1)Excel工具类

包含读取从起始坐标到终点坐标的矩形范围数据到二维数组的方法、excel表格cell数据读取等;

import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.xssf.usermodel.XSSFCell;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.web.multipart.MultipartFile;import java.io.InputStream;
import java.util.List;/*** @description 基于原生poi的excel工具类 解决easypoi/easyExcel解决不了的问题*/
@Slf4j
public class ExcelReaderUtils {/*** excel文件类型后缀*/public static final String OFFICE_EXCEL_XLSX = ".xlsx";/*** 读取excel行数的最大值,防止OOM*/public static final int MAX_READABLE_LINE_COUNT = 10000;/*** 解析XSSFSheet,从起始行start到结束行end,取出【指定列】column的item放入list返回** @param xssfSheet* @param column    列数,从0计数* @param rowStart  起始行(闭区间),从0计数* @param rowEnd    结束行(闭区间),从0计数* @return*/public static List<String> getListFrXSSFSheetColumn(XSSFSheet xssfSheet, int column, int rowStart, int rowEnd) {List<String> result = Lists.newArrayList();try {XSSFRow xssfRow;XSSFCell xssfCell;for (int row = rowStart; row <= rowEnd; row++) {xssfRow = xssfSheet.getRow(row);xssfCell = xssfRow.getCell(column);// 都转成String,避免小数精度xssfCell.setCellType(CellType.STRING);result.add(getCellValueByCell(xssfCell));}} catch (Exception e) {log.error("failed to read data from excel file!");throw new RuntimeException();}return result;}/*** 解析XSSFSheet,从起始列start到结束列end,取出【指定行】row的item放入list返回** @param xssfSheet* @param rowNum      行数,从0计数* @param columnStart 起始行(闭区间),从0计数* @param columnEnd   结束行(闭区间),从0计数* @return*/public static List<String> getListFrXSSFSheetRow(XSSFSheet xssfSheet, int rowNum, int columnStart, int columnEnd) {List<String> result = Lists.newArrayList();try {XSSFRow xssfRow = xssfSheet.getRow(rowNum);XSSFCell xssfCell;for (int column = columnStart; column <= columnEnd; column++) {xssfCell = xssfRow.getCell(column);// 都转成String,避免小数精度xssfCell.setCellType(CellType.STRING);result.add(getCellValueByCell(xssfCell));}} catch (Exception e) {log.error("failed to read data from excel file!");throw new RuntimeException();}return result;}/*** 解析XSSFSheet,从起始行start到结束行end,取出从列columnStart到columnEnd的item放入list返回** @param xssfSheet* @param columnStart 起始列数(闭区间),从0计数* @param columnEnd   截至列数(闭区间),从0计数* @param rowStart    起始行(闭区间),从0计数* @param rowEnd      结束行(闭区间),从0计数* @return*/public static List<List<String>> getListFrXSSFSheet(XSSFSheet xssfSheet, int columnStart, int columnEnd, int rowStart, int rowEnd) {List<List<String>> result = Lists.newArrayList();try {XSSFRow xssfRow;for (int row = rowStart; row <= rowEnd; row++) {List<String> dataInRow = Lists.newArrayList();xssfRow = xssfSheet.getRow(row);XSSFCell xssfCell;for (int col = columnStart; col <= columnEnd; col++) {xssfCell = xssfRow.getCell(col);// 都转成String,避免小数精度xssfCell.setCellType(CellType.STRING);dataInRow.add(getCellValueByCell(xssfCell));}result.add(dataInRow);}} catch (Exception e) {log.error("failed to read data from excel file!");throw new RuntimeException();}return result;}/*** 获取单元格各类型值,返回字符串类型** @param cell* @return*/private static String getCellValueByCell(XSSFCell cell) {//判断是否为null或空串if (cell == null || ("").equals(cell.toString().trim())) {return "";}String cellValue = "";CellType cellType = cell.getCellTypeEnum();// 以下是判断数据的类型switch (cellType) {// 数字case NUMERIC:cellValue = String.valueOf(cell.getNumericCellValue());break;// 字符串case STRING:cellValue = cell.getStringCellValue();break;// Booleancase BOOLEAN:cellValue = String.valueOf(cell.getBooleanCellValue());break;// 公式case FORMULA:cellValue = String.valueOf(cell.getCellFormula());break;// 空值case BLANK:cellValue = "";break;// 故障case ERROR:cellValue = "非法字符";break;default:cellValue = "未知类型";break;}return cellValue;}/*** 解析.xlsx文件,转成XSSFWorkbook** @param excelFile* @return*/public static XSSFWorkbook getXSSFWorkbookFrExcel(MultipartFile excelFile) {log.warn("receive excel file, file size:[{} MB]", excelFile.getSize() / (1000 * 1024));XSSFWorkbook wb;try {InputStream is = excelFile.getInputStream();wb = new XSSFWorkbook(is);is.close();} catch (Exception e) {log.error("get XSSFWorkbook from excelFile error! e:{} message:{}", e, e.getMessage());return null;}return wb;}/*** 获取XSSFSheet的行数** @param xssfSheet* @return*/public static int getRowCount(XSSFSheet xssfSheet) {return xssfSheet.getPhysicalNumberOfRows();}/*** 获取xssfSheet的列数(默认第一行)** @param xssfSheet* @return*/public static int getColumnCount(XSSFSheet xssfSheet) {return xssfSheet.getRow(0).getPhysicalNumberOfCells();}
}

(2)遍历分组创建sheet

        XSSFWorkbook workbook = new XSSFWorkbook();// 遍历分组数据 groupIdDataMap为groupby之后的Mapif (MapUtils.isNotEmpty(groupIdDataMap)) {groupIdDataMap.forEach((groupId, dataInGroup) -> {// 将分组ID冗余到sheetName中方便导入时的解析XSSFSheet sheet = workbook.createSheet(String.join("-", SHEET_NAME_PREFIX, String.valueOf(groupId)));// 创建表头 第1行(初始下标为0)List<String> headerNames = generateHeaderNames(dataInGroup);buildTitle(workbook, sheet, headers, 1);				// 生成excel数据二维数组List<List<String>> rows = initRows(dataInGroup);// 按行填充数据(第1行放说明、第2行放表头)writeRows(sheet, 2, rows, 0, headers.size() - 1);})}return workbook;

(3)设置表头格式/自动宽度/边框/合并单元格/背景色

    /*** 设置sheet的表头 设置表头在第(startRow+1)行 设置表头宽度*/private static void buildTitle(XSSFWorkbook workbook, XSSFSheet sheet, List<String> headers, int startRow) {// 第0行内容:固定提示文案 合并单元格(起始坐标和终点坐标)CellRangeAddress rangeAddress = new CellRangeAddress(0, 0, 0, headers.size() - 1);sheet.addMergedRegion(rangeAddress);XSSFRow row0 = sheet.createRow(0);XSSFCell cell0 = row0.createCell(0);cell0.setCellValue("导入须知:不能在该Excel模板中对表头信息进行增加、删除、修改或位置调整!");XSSFCellStyle headerCellStyle0 = workbook.createCellStyle();headerCellStyle0.setFillForegroundColor(IndexedColors.YELLOW.getIndex());headerCellStyle0.setFillPattern(FillPatternType.SOLID_FOREGROUND);cell0.setCellStyle(headerCellStyle0);// 第1行内容:表头列从第1行开始XSSFCellStyle headerCellStyle = workbook.createCellStyle();// 字体格式XSSFFont font = workbook.createFont();font.setBold(true);headerCellStyle.setFont(font);int columns = headers.size();XSSFRow row = sheet.createRow(startRow);// 背景色headerCellStyle.setFillForegroundColor(IndexedColors.RED.getIndex());headerCellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);// 边框线条宽度headerCellStyle.setBorderBottom(BorderStyle.MEDIUM);headerCellStyle.setBorderTop(BorderStyle.MEDIUM);headerCellStyle.setBorderLeft(BorderStyle.MEDIUM);headerCellStyle.setBorderRight(BorderStyle.MEDIUM);// 水平/垂直居中headerCellStyle.setAlignment(HorizontalAlignment.CENTER);headerCellStyle.setVerticalAlignment(VerticalAlignment.CENTER);for (int column = 0; column < columns; column++) {String headerName = headers.get(column);XSSFCell cell = row.createCell(column);XSSFRichTextString text = new XSSFRichTextString(headerName);cell.setCellValue(text);cell.setCellStyle(headerCellStyle);// 设置自动表头列宽度int colLength = cell.getStringCellValue().getBytes().length * 192;sheet.setColumnWidth(column, colLength);}}

(4)二维数据集写入excel表格

    /*** 填充数据行,数据可以是不对齐的** @apiNote 列序号从0开始 要求 0 <= startColumnIndex <= endColumnIndex < 表头长度-1*/void writeRows(XSSFSheet sheet, Integer startRowIndex, List<List<String>> rows, Integer startColumnIndex, Integer endColumnIndex) {if (CollectionUtils.isNotEmpty(rows)) {// 写数据从第startRowIndex行开始for (int rownum = startRowIndex; rownum < rows.size(); rownum++) {XSSFRow row = sheet.createRow(rownum);for (int col = startColumnIndex; col <= endColumnIndex; col++) {row.createCell(col).setCellValue(rows.get(rownum).get(col - startColumnIndex));}}}}

(5)导入后读出多sheet

负责将HTTP请求的MultipartFile类以数据流的方式转换成Excel对应的Java对象;

将excel的多个sheet读出;

    /*** 获取excel文件流,解析并校验上传文件,返回XSSFSheet*/private List<XSSFSheet> checkExcelFile(MultipartFile excelFile) {XSSFWorkbook xssfWorkbook = Optional.ofNullable(ExcelReaderUtils.getXSSFWorkbookFrExcel(excelFile)).orElseThrow(() -> new BizException(ResultCodeEnum.EXCEL_ANALYSIS_ERROR.getCode(), ResultCodeEnum.EXCEL_ANALYSIS_ERROR.getDesc()));Iterator<Sheet> sheetIterator = xssfWorkbook.sheetIterator();List<XSSFSheet> sheets = Lists.newArrayList();while (sheetIterator.hasNext()) {XSSFSheet sheet = (XSSFSheet) sheetIterator.next();sheets.add(sheet);}List<String> sheetNames = sheets.stream().map(XSSFSheet::getSheetName).collect(Collectors.toList());log.warn("解析上传excel成功 [sheetNum={} sheetNames={}]", sheets.size(), JSON.toJSONString(sheetNames));// 数量检测sheets.forEach(sheet -> {int recordNum = ExcelReaderUtils.getRowCount(sheet);log.warn("analyseExcel success. [records count={}]", recordNum);if (recordNum > ExcelReaderUtils.MAX_READABLE_LINE_COUNT) {throw new BizException(ResultCodeEnum.EXCHCODE_UPLOAD_SIZE_OVERFLOW.getCode(), ResultCodeEnum.EXCHCODE_UPLOAD_SIZE_OVERFLOW.getDesc());}});return sheets;}

(6)校验表头

复用表头的生成规则,取当前sheet的表头,做一下字符串匹配校验;

    private void checkPointImportExcelHeader(Long modelIdInType, List<String> sortedHeaders, List<String> listFrXSSFSheetRow) {// 预期的表头 保证表头排序规则固定String standard = String.join("-", sortedHeaders);// 当前输入读出来的表头String input = String.join("-", listFrXSSFSheetRow);if (!StringUtils.equals(standard, input)) {log.error("导入的excel表头被篡改 [standard={} input={}]", standard, input);throw new BizException("导入的excel表头被篡改 请重新下载模板后再上传!");}}

(7)读取数据

        for (XSSFSheet sheet : xssfSheets) {// 根据sheetName解析当前sheet页对应哪一个分组数据String sheetName = sheet.getSheetName();int rowCount = ExcelReaderUtils.getRowCount(sheet);String[] split = sheetName.split("-");if (split.length < N) {log.error("表格sheet名称解析失败! [sheetName={}]", sheetName);throw new BizException(ResultCodeEnum.EXCEL_ANALYSIS_ERROR.getCode(), ResultCodeEnum.EXCEL_ANALYSIS_ERROR.getDesc());}// 省略细节...log.warn("开始处理sheet【{}】 rowCount={} ]", sheetName, rowCount);List<EvaluateModelDataDO> evaUpdateDomainList = Lists.newArrayList();List<PointModelDataDO> pointUpdateDomainList = Lists.newArrayList();if (EVALUATE_PREFIX.equals(modelType)) {// 取出当前输入sheet的表头List<String> listFrXSSFSheetRow = ExcelReaderUtils.getListFrXSSFSheetRow(sheet, 2, 1, 0, lastColumnIndex);// 根据当前sheetName解析出的分组信息 生成表头 省略List<String> sortedHeaders = generateStandardHeaders(sheetName);// 校验表头是否篡改checkEvaImportExcelHeader(modelIdInType, sortedHeaders, listFrXSSFSheetRow);// 读出数据 矩阵转秩 / 注意:解析规则固定,因此不能删除列,不能修改列顺序;可以修改行顺序和删除列List<List<String>> data = ExcelReaderUtils.getListFrXSSFSheet(sheet, 0, lastColumnIndex, 2, sheet.getLastRowNum());data.forEach(rowData -> {// 考虑:可能误操作在excel新增了不存在的行 跳过...// 遍历数据处理...});}}

以上;本文只提供思路和注意的点,不提供完整代码;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/790690.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

讲讲你对数据结构-线性表了解多少?

线性表 - 数组和矩阵 当谈到线性表时&#xff0c;数组和矩阵是两种常见的数据结构。 数组&#xff08;Array&#xff09;&#xff1a; 数组是有序的元素集合&#xff0c;可以通过索引来访问和操作其中的元素。它是最简单、最基本的数据结构之一。数组的特点包括&#xff1a; …

C++ | Leetcode C++题解之第7题整数反转

题目&#xff1a; 题解&#xff1a; class Solution { public:int reverse(int x) {int rev 0;while (x ! 0) {if (rev < INT_MIN / 10 || rev > INT_MAX / 10) {return 0;}int digit x % 10;x / 10;rev rev * 10 digit;}return rev;} };

wordpress课程项目主题电脑版+手机版自适应

这款主题适合做资源、课程、素材等&#xff0c;演示站&#xff1a;点击查看

八数码问题——A*算法的应用(A-Star)

文章目录 1 问题描述2 启发式搜索3 A*算法3.1 参考网址3.2 是什么3.3 为什么A*算法适用于八数码问题3.4 A* 算法的基本框架 4 A* 算法如何解决八数码问题4.1 八数码状态的存储4.2 启发式函数4.3 构造目标状态元素位置的字典4.4 在二维列表中查找目标元素4.5 A* 算法主体4.6 路径…

RUST语言函数的定义与调用

1.定义函数 定义一个RUST函数使用fn关键字 函数定义语法: fn 函数名(参数名:参数类型,参数名:参数类型) -> 返回类型 { //函数体 } 定义一个没有参数,没有返回类型的参数 fn add() {println!("调用了add函数!"); } 定义有一个参数的函数 fn add(a:u32)…

vue-codeirror编辑器vue3中的使用

vue-codeirror编辑器vue3中的使用 <script lang"ts" setup> import { ref,reactive } from vue; import { Codemirror } from "vue-codemirror"; import { oneDark } from "codemirror/theme-one-dark"; import { json } from codemirror/…

C++ //练习 11.12 编写程序,读入string和int的序列,将每个string和int存入一个pair中,pair保存在一个vector中。

C Primer&#xff08;第5版&#xff09; 练习 11.12 练习 11.12 编写程序&#xff0c;读入string和int的序列&#xff0c;将每个string和int存入一个pair中&#xff0c;pair保存在一个vector中。 环境&#xff1a;Linux Ubuntu&#xff08;云服务器&#xff09; 工具&#x…

前端(动态雪景背景+动态蝴蝶)

1.CSS样式 <style>html, body, a, div, span, table, tr, td, strong, ul, ol, li, h1, h2, h3, p, input {font-weight: inherit;font-size: inherit;list-style: none;border-spacing: 0;border: 0;border-collapse: collapse;text-decoration: none;padding: 0;margi…

网站如何运用百度文心一言API进行AI内容创作?

网站如何运用百度文心一言API进行AI内容创作&#xff1f; 当我们做好一个网站的时候会因为创作内容而发愁&#xff0c;随着chatgpt的出现&#xff0c;内容创作已经不再是什么困难的事情&#xff0c;但是由于gpt是国外的&#xff0c;在国内使用有诸多不便&#xff0c;因此我们今…

06-编辑器

gedit编辑器 gedit是Ubuntu系统自带的编辑器&#xff0c;可以用来轻度编辑和记录一些内容。 在终端中我们通过以下命令打开&#xff1a; gedit 要打开或者新建的文件名虽然Ubuntu的图形界面也能通过gedit打开文件&#xff0c;但是用终端打开gedit可以动用更高的权限&#xff…

Apache Pulsar源码解析之Lookup机制

引言 在学习Pulsar一段时间后&#xff0c;相信大家也或多或少听说Lookup这个词&#xff0c;今天就一起来深入剖析下Pulsar是怎么设计的它吧 Lookup是什么 在客户端跟服务端建立TCP连接前有些信息需要提前获取&#xff0c;这个获取方式就是Lookup机制。所获取的信息有以下几种…

针孔相机、鱼眼相机、全景相机

先进性简述&#xff0c;后续慢慢会补充1. 针孔相机&#xff1a; 针孔相机是一种基于针孔成像原理的传统相机&#xff0c;它使用一个非常小的孔径&#xff08;即“针孔”&#xff09;来限制光线进入相机的方式。 这种相机通常具有简单的结构&#xff0c;由一个孔径较小的光学元…

acwing算法提高之图论--最小生成树的扩展应用

目录 1 介绍2 训练 1 介绍 本专题用来记录使用最小生成树算法&#xff08;prim或kruskal&#xff09;解决的扩展题目。 2 训练 题目1&#xff1a;1146新的开始 C代码如下&#xff0c; #include <iostream> #include <cstring> #include <algorithm>usin…

Flutter中setState函数的使用注意事项

文章目录 Flutter中setState函数的使用注意事项只能在具有State对象的类中使用不要在build方法中使用将状态更新逻辑放在setState方法内部避免频繁调用使用回调函数更新状态 Flutter中setState函数的使用注意事项 setState()函数是Flutter中非常重要的一个函数&#xff0c;它用…

C#使用Selenium驱动Chrome浏览器

1.Selenium库依赖安装 Selenium WebDriver是Selenium项目的一部分&#xff0c;用于模拟用户在Web应用程序中的交互操作。它支持多种浏览器&#xff0c;如Chrome、Firefox、IE等&#xff0c;且与各种编程语言&#xff08;如Java、Python、C#等&#xff09;兼容&#xff0c;具有…

运用抽象工厂模式实现多个产品族的独立创建与组合管理

抽象工厂模式&#xff08;Abstract Factory Pattern&#xff09; 是一种软件设计模式&#xff0c;属于创建型设计模式类别&#xff0c;主要用于处理对象的创建过程&#xff0c;特别是在应对具有多个相关或相互依赖对象的特定上下文时。该模式的主要目的是将一组相关或者相互依赖…

SpringMvc处理器方法的返回值

1、返回ModelAndView ModelAndView中的视图会经过你视图解析器处理得到真实的jsp文件页面并且跳转。 RequestMapping("test01")public ModelAndView test01() {ModelAndView mv new ModelAndView();// 携带数据mv.addObject("teamName", "牛逼&quo…

AI绘图:Controlnet在ComfyUI中的使用教程:提升Stable Diffusion可控性

前言 Controlnet是Stable Diffusion中一个至关重要的功能&#xff0c;它极大地增强了图像生成的可控性。通过引入额外的条件&#xff0c;Controlnet使我们能够更精确地控制生成的图像内容。本教程将详细介绍如何在ComfyUI中使用Controlnet&#xff0c;从而让你的创作过程更加灵…

2012年认证杯SPSSPRO杯数学建模B题(第二阶段)节能减排全过程文档及程序

2012年认证杯SPSSPRO杯数学建模 节能减排、抑制全球气候变暖 B题 白屋顶计划 原题再现&#xff1a; 第二阶段问题   虽然环境学家对地球环境温度的改变有许多种不同观点&#xff0c;但大多数科学家可以达成一个基本的共识&#xff1a;近年来人类的活动&#xff0c;尤指二氧…

云存储中常用的相同子策略的高效、安全的基于属性的访问控制的论文阅读

参考文献为2022年发表的Efficient and Secure Attribute-Based Access Control With Identical Sub-Policies Frequently Used in Cloud Storage 动机 ABE是实现在云存储中一种很好的访问控制手段&#xff0c;但是其本身的计算开销导致在实际场景中应用收到限制。本论文研究了…