零、希望Springboot-java导出excel文件,包括动态表头与下边合并的列
使用 org.apache.poi 与自己封装工具类实现相关功能。代码如下
一、代码
1、依赖
implementation(group: 'org.apache.poi',name: 'poi-ooxml',version: '4.1.0')
implementation(group: 'org.apache.poi',name: 'poi',version: '4.1.0')
implementation(group: 'cn.hutool', name: 'hutool-all', version: '5.8.3')
2、工具类 ExcelMergeUtil.java
import cn.hutool.json.JSONUtil;
import com.longze.fengqx.HeaderNode;
import com.longze.fengqx.PoiModel;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.ss.util.RegionUtil;
import org.apache.poi.xssf.streaming.SXSSFCell;
import org.apache.poi.xssf.streaming.SXSSFSheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
/**
* @author Fengqx
* @version 1.0
* @description: excel文件合并
* @date 2023/8/20 13:13
*/
public class ExcelMergeUtil {
public static SXSSFSheet createExcelHead(SXSSFWorkbook book, String sheetName, String headJson){
List<HeaderNode> headerNodes = JSONUtil.toList(headJson, HeaderNode.class);
SXSSFSheet sxssfSheet = book.createSheet(sheetName);
CellStyle headStyle = book.createCellStyle();
defaultHeadStyle(headStyle);
//表头层级
int deep = headerNodes.stream().map(HeaderNode::getRow).reduce(Integer::max).orElse(1);
for (int i = 0; i < deep; i++) {
sxssfSheet.createRow(i);
}
//创建单元格
for (HeaderNode headerNode : headerNodes) {
int row = headerNode.getRow();
int col = headerNode.getColumn();
SXSSFCell sxssfCell = sxssfSheet.getRow(row).createCell(col);
sxssfSheet.setColumnWidth(col, headerNode.getWidth() * 256);
sxssfCell.setCellStyle(headStyle);
sxssfCell.setCellValue(headerNode.getHeaderName());
CellRangeAddress region;
//是否跨列
if (headerNode.isOverNode()) {
region = new CellRangeAddress(row, deep, col, col);
} else {
region = new CellRangeAddress(row, row, col, (col + headerNode.getOverNodeCount() - 1));
}
if (region.getNumberOfCells() > 1) {
sxssfSheet.addMergedRegionUnsafe(region);
//合并后设置下边框
RegionUtil.setBorderTop(BorderStyle.THIN, region, sxssfSheet);
RegionUtil.setBorderLeft(BorderStyle.THIN, region, sxssfSheet);
RegionUtil.setBorderBottom(BorderStyle.THIN, region, sxssfSheet);
RegionUtil.setBorderRight(BorderStyle.THIN, region, sxssfSheet);
}
}
return sxssfSheet;
}
public static void mergeCellFunc(Sheet sheet, String[] title, String[] field, List<Map<String, String>> list, Integer deep,List<Integer> mergeIndex){
Map<String, List<Map<String, String>>> map = Maps.newHashMap();
map.put("测试合并数据", list);
// 模拟大数据量情况下,任务中心可分页查询接口,分批返回数据
List<List<Map<String, String>>> listList =excelPageByNum(list, 5);
// 数据总数 + 表头
int size = listList.stream().mapToInt(List::size).sum() + deep;
List<PoiModel> poiModels = Lists.newArrayList();
for (List<Map<String, String>> listmid : listList) {
for (Map<String, String> mapMid : listmid) {
int index = sheet.getLastRowNum()+1;
Row row = sheet.createRow(index);
for (int i = 0; i < title.length; i++) {
String titleField = field[i];
String old = null;
if (index > deep+1) {
old = poiModels.get(i) == null ? null : poiModels.get(i).getContent();
}
for (int k : mergeIndex) {
if (index == deep+1) {
PoiModel poiModel =new PoiModel(mapMid.get(titleField),mapMid.get(titleField),null,deep+1,i);
poiModels.add(poiModel);
break;
}
PoiModel poiModel = poiModels.get(i);
String content = mapMid.get(titleField);
// 当前行的当前列与上一行的当前列的内容不一致时,则把当前行以上的合并
if (i > 0 && k == i) {
// 如果不需要考虑当前行与上一行内容相同,但是它们的前一列内容不一样则不合并的情况,把或条件删除
if (!StringUtils.equalsIgnoreCase(content, poiModel.getContent())
// || (StringUtils.equalsIgnoreCase(content, poiModel.getContent()) && !StringUtils.equalsIgnoreCase(poiModels.get(i - 1).getOldContent(),mapMid.get(field[i - 1])))
) {
get(poiModel, content, index, i, sheet);
}
}
// 处理第一列的情况
if (k == i && i == 0 && !StringUtils.equalsIgnoreCase(content,poiModel.getContent())) {
get(poiModel, content, index, i, sheet);
}
// 最后一行没有后续的行与之比较,所有当到最后一行时则直接合并对应列的相同内容
if (k == i && index == size && poiModels.get(i).getRowIndex() != index) {
CellRangeAddress cra = new CellRangeAddress(poiModels.get(i).getRowIndex(), index, poiModels.get(i).getCellIndex(), poiModels.get(i).getCellIndex());
sheet.addMergedRegion(cra);
}
}
Cell cell = row.createCell(i);
cell.setCellValue(mapMid.get(titleField));
poiModels.get(i).setOldContent(old);
}
}
}
}
/**
* 表头样式
*
* @param headStyle
*/
private static void defaultHeadStyle(CellStyle headStyle) {
headStyle.setBorderTop(BorderStyle.THIN);
headStyle.setBorderLeft(BorderStyle.THIN);
headStyle.setBorderBottom(BorderStyle.THIN);
headStyle.setBorderRight(BorderStyle.THIN);
headStyle.setAlignment(HorizontalAlignment.CENTER);
headStyle.setVerticalAlignment(VerticalAlignment.CENTER);
headStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
headStyle.setFillForegroundColor(IndexedColors.YELLOW.getIndex());
}
//合并单元格
private static void get(PoiModel poiModel, String content, int index, int i, Sheet sheet) {
if (poiModel.getRowIndex() != index - 1) {
CellRangeAddress cra = new CellRangeAddress(poiModel.getRowIndex(), index - 1, poiModel.getCellIndex(), poiModel.getCellIndex());
//在sheet里增加合并单元格
sheet.addMergedRegion(cra);
}
/*重新记录该列的内容为当前内容,行标记改为当前行标记,列标记则为当前列*/
poiModel.setContent(content);
poiModel.setRowIndex(index);
poiModel.setCellIndex(i);
}
public static <T> List<List<T>> excelPageByNum(List<T> list, int pageSize) {
return IntStream.range(0, list.size()).boxed().filter(t -> t % pageSize == 0).map(t -> list.stream().skip(t).limit(pageSize).collect(Collectors.toList())).collect(Collectors.toList());
}
}
3、实体对象
HeaderNode.java 和 PoiModel.java
public class PoiModel {
private String content;
private String oldContent;
private String primaryKey;
private int rowIndex;
private int cellIndex;
public PoiModel() {
}
public PoiModel(String content, String oldContent, String primaryKey, int rowIndex, int cellIndex) {
this.content = content;
this.oldContent = oldContent;
this.primaryKey = primaryKey;
this.rowIndex = rowIndex;
this.cellIndex = cellIndex;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getOldContent() {
return oldContent;
}
public void setOldContent(String oldContent) {
this.oldContent = oldContent;
}
public String getPrimaryKey() {
return primaryKey;
}
public void setPrimaryKey(String primaryKey) {
this.primaryKey = primaryKey;
}
public int getRowIndex() {
return rowIndex;
}
public void setRowIndex(int rowIndex) {
this.rowIndex = rowIndex;
}
public int getCellIndex() {
return cellIndex;
}
public void setCellIndex(int cellIndex) {
this.cellIndex = cellIndex;
}
}
public class HeaderNode {
/**
* 标题头
*/
private String headerName;
/**
* 层级
*/
private int row;
/**
* 非叶子节点列跨度
*/
private int overNodeCount;
/**
* 当前列没有子节点
*/
private boolean overNode = true;
/**
* 列
*/
private int column;
/**
* 宽度
*/
private int width = 13;
public String getHeaderName() {
return headerName;
}
public void setHeaderName(String headerName) {
this.headerName = headerName;
}
public int getRow() {
return row;
}
public void setRow(int row) {
this.row = row;
}
public int getOverNodeCount() {
return overNodeCount;
}
public void setOverNodeCount(int overNodeCount) {
this.overNodeCount = overNodeCount;
}
public boolean isOverNode() {
return overNode;
}
public void setOverNode(boolean overNode) {
this.overNode = overNode;
}
public int getColumn() {
return column;
}
public void setColumn(int column) {
this.column = column;
}
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
}
4、下载Controller
@GetMapping(value = "/downExcel")
@ResponseBody
public void downExcel(HttpServletResponse response,@RequestParam(required = true) String type) throws Exception {
try {
tengxunService.downExcel(response, type);
} catch (Exception ex) {
throw ex;
}
}
5、下载service
@Override
public void downloadExcel(HttpServletResponse response, String type)throws Exception {
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
// 这里URLEncoder.encode可以防止中文乱码
String fileName = URLEncoder.encode("腾讯充值文件", "UTF-8");
response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
OutputStream os = response.getOutputStream();
//加工数据
List<Map<String, String>> list = Lists.newArrayList();
for(int i=0;i<chongzhiList.size();i++){
Chongzhi dto=new Chongzhi ();
list.add(JSONObject.parseObject(JSON.toJSONString(dto),Map.class));
}
String weekStart ="08.01";
String weekEnd ="08.07";
String nextWeekStart = "08.08";
String nextWeekEnd ="08.15";
//合并单元格方法
try {
String customizeLabel = "[{\"headerName\":\"区域\",\"column\":0,\"row\":0}," +
"{\"headerName\":\"用户姓名\",\"column\":1,\"row\":0}," +
"{\"headerName\":\"本周("+weekStart+"-"+weekEnd+")充值记录\",\"column\":2,\"row\":0}," +
"{\"headerName\":\"本周("+weekStart+"-"+weekEnd+")充值详情\",\"column\":3,\"row\":0,\"overNodeCount\":4,\"overNode\":false},{\"headerName\":\"充值时间\",\"column\":3,\"row\":1}," +
"{\"headerName\":\"充值项目\",\"column\":4,\"row\":\"1\"}," +
"{\"headerName\":\"充值方式\",\"column\":5,\"row\":1}," +
"{\"headerName\":\"充值金额\",\"column\":6,\"row\":1}," +
"{\"headerName\":\"下周("+nextWeekStart+"-"+nextWeekEnd+")充值计划\",\"column\":7,\"row\":0}]";
//1、生book
SXSSFWorkbook book = new SXSSFWorkbook();
//2、生成动态标题
SXSSFSheet sxssfSheet = ExcelMerge.createExcelHead(book,"Sheet1", customizeLabel);
//3、取数据对应字段值 汇总
String[] title = {"区域", "用户姓名", "本周("+weekStart+"-"+weekEnd+")充值记录", "充值时间", "充值项目", "充值方式", "充值金额", "下周("+nextWeekStart+"-"+nextWeekEnd+")充值计划"};
//4、取数据对应属性的字段值 汇总
String[] field = {"areaName", "name", "chongzjilu", "chongzTime", "chongzProject", "chongzMethod", "chongzMoney", "nextChongz"};
//5、需要合并的列
List<Integer> mergeIndex = Arrays.asList(0,1,7);
//6、合并
ExcelMerge.mergeCellFunc(sxssfSheet,title,field, list, sxssfSheet.getLastRowNum(),mergeIndex);
//创建excel文件 下载
book.write(os);
} catch (IOException e){
logger.error("文件导出失败,错误信息{}",e);
// 重置response
response.reset();
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
Map<String, String> map = new HashMap<>();
map.put("statusCode", "500");
map.put("message", "下载文件失败" + e.getMessage());
response.getWriter().println(JSON.toJSONString(map));
}finally {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
三、下载
完事通过controller调用下载接口,直接可以下载出文件
可以任意改表头,与选择是否要合并的字段,当做参数传入,将需要合并的列顺序传入即可完成合并,一步到位,十分方便
//5、需要合并的列 List<Integer> mergeIndex = Arrays.asList(0,1,7);
ExcelMerge.mergeCellFunc(sxssfSheet,title,field, list, sxssfSheet.getLastRowNum(),mergeIndex);
截图如下
荆轲刺秦王!