代码
- 首先自定义注解CellMerge,用于标记哪些属性需要合并,哪个是主键**(这里做了一个优化,可以标记多个主键)**
import org.dromara.common.excel.core.CellMergeStrategy;import java.lang.annotation.*;/*** excel 列单元格合并(合并列相同项)** 需搭配 {@link CellMergeStrategy} 策略使用** @author Lion Li*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface CellMerge {/*** col index*/int index() default -1;}
- 再创建自定义单元格合并策略类CellMergeStrategy,当Excel中两列主键相同时,合并被标记需要合并的列**(当前类增加多主键判断是否需要合并)**
import cn.hutool.core.collection.CollUtil;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.write.merge.AbstractMergeStrategy;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;
import org.dromara.common.core.utils.reflect.ReflectUtils;
import org.dromara.common.excel.annotation.CellMerge;import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;/*** 列值重复合并策略** @author Lion Li*/
@Slf4j
public class CellMergeStrategy extends AbstractMergeStrategy {private final List<CellRangeAddress> cellList;private final boolean hasTitle;private int rowIndex;public CellMergeStrategy(List<?> list, boolean hasTitle) {this.hasTitle = hasTitle;// 行合并开始下标this.rowIndex = hasTitle ? 1 : 0;this.cellList = handle(list, hasTitle);}@Overrideprotected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {// judge the list is not nullif (CollUtil.isNotEmpty(cellList)) {// the judge is necessaryif (cell.getRowIndex() == rowIndex && cell.getColumnIndex() == 0) {for (CellRangeAddress item : cellList) {sheet.addMergedRegion(item);}}}}@SneakyThrowsprivate List<CellRangeAddress> handle(List<?> list, boolean hasTitle) {List<CellRangeAddress> cellList = new ArrayList<>();if (CollUtil.isEmpty(list)) {return cellList;}Field[] fields = ReflectUtils.getFields(list.get(0).getClass(), field -> !"serialVersionUID".equals(field.getName()));// 有注解的字段List<Field> mergeFields = new ArrayList<>();List<Integer> mergeFieldsIndex = new ArrayList<>();for (int i = 0; i < fields.length; i++) {Field field = fields[i];if (field.isAnnotationPresent(CellMerge.class)) {CellMerge cm = field.getAnnotation(CellMerge.class);mergeFields.add(field);mergeFieldsIndex.add(cm.index() == -1 ? i : cm.index());if (hasTitle) {ExcelProperty property = field.getAnnotation(ExcelProperty.class);rowIndex = Math.max(rowIndex, property.value().length);}}}Map<Field, RepeatCell> map = new HashMap<>();// 生成两两合并单元格for (int i = 0; i < list.size(); i++) {for (int j = 0; j < mergeFields.size(); j++) {Field field = mergeFields.get(j);Object val = ReflectUtils.invokeGetter(list.get(i), field.getName());int colNum = mergeFieldsIndex.get(j);if (!map.containsKey(field)) {map.put(field, new RepeatCell(val, i));} else {RepeatCell repeatCell = map.get(field);Object cellValue = repeatCell.getValue();if (cellValue == null || "".equals(cellValue)) {// 空值跳过不合并continue;}if (!cellValue.equals(val)) {if (i - repeatCell.getCurrent() > 1) {cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum));}map.put(field, new RepeatCell(val, i));} else if (j == 0) {if (i == list.size() - 1) {if (i > repeatCell.getCurrent()) {cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex, colNum, colNum));}}} else {// 判断前面的是否合并了RepeatCell firstCell = map.get(mergeFields.get(0));if (repeatCell.getCurrent() != firstCell.getCurrent()) {if (i == list.size() - 1) {if (i > repeatCell.getCurrent()) {cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex, colNum, colNum));}} else if (repeatCell.getCurrent() < firstCell.getCurrent()) {if (i - repeatCell.getCurrent() > 1) {cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum));}map.put(field, new RepeatCell(val, i));}} else if (i == list.size() - 1) {if (i > repeatCell.getCurrent()) {cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex, colNum, colNum));}}}}}}return cellList;}@Data@AllArgsConstructorstatic class RepeatCell {private Object value;private int current;}
}
ExcelUtlis工具类
package org.dromara.common.excel.utils;import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.resource.ClassPathResource;
import cn.hutool.core.util.IdUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.builder.ExcelWriterSheetBuilder;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.fill.FillConfig;
import com.alibaba.excel.write.metadata.fill.FillWrapper;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.file.FileUtils;
import org.dromara.common.excel.convert.ExcelBigNumberConvert;
import org.dromara.common.excel.core.*;import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.Collection;
import java.util.List;
import java.util.Map;/*** Excel相关处理** @author Lion Li*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ExcelUtil {/*** 同步导入(适用于小数据量)** @param is 输入流* @return 转换后集合*/public static <T> List<T> importExcel(InputStream is, Class<T> clazz) {return EasyExcel.read(is).head(clazz).autoCloseStream(false).sheet().doReadSync();}/*** 使用校验监听器 异步导入 同步返回** @param is 输入流* @param clazz 对象类型* @param isValidate 是否 Validator 检验 默认为是* @return 转换后集合*/public static <T> ExcelResult<T> importExcel(InputStream is, Class<T> clazz, boolean isValidate) {DefaultExcelListener<T> listener = new DefaultExcelListener<>(isValidate);EasyExcel.read(is, clazz, listener).sheet().doRead();return listener.getExcelResult();}/*** 使用自定义监听器 异步导入 自定义返回** @param is 输入流* @param clazz 对象类型* @param listener 自定义监听器* @return 转换后集合*/public static <T> ExcelResult<T> importExcel(InputStream is, Class<T> clazz, ExcelListener<T> listener) {EasyExcel.read(is, clazz, listener).sheet().doRead();return listener.getExcelResult();}/*** 导出excel** @param list 导出数据集合* @param sheetName 工作表的名称* @param clazz 实体类* @param response 响应体*/public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, HttpServletResponse response) {try {resetResponse(sheetName, response);ServletOutputStream os = response.getOutputStream();exportExcel(list, sheetName, clazz, false, os, null);} catch (IOException e) {throw new RuntimeException("导出Excel异常");}}/*** 导出excel** @param list 导出数据集合* @param sheetName 工作表的名称* @param clazz 实体类* @param response 响应体* @param options 级联下拉选*/public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, HttpServletResponse response, List<DropDownOptions> options) {try {resetResponse(sheetName, response);ServletOutputStream os = response.getOutputStream();exportExcel(list, sheetName, clazz, false, os, options);} catch (IOException e) {throw new RuntimeException("导出Excel异常");}}/*** 导出excel** @param list 导出数据集合* @param sheetName 工作表的名称* @param clazz 实体类* @param merge 是否合并单元格* @param response 响应体*/public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, boolean merge, HttpServletResponse response) {try {resetResponse(sheetName, response);ServletOutputStream os = response.getOutputStream();exportExcel(list, sheetName, clazz, merge, os, null);} catch (IOException e) {throw new RuntimeException("导出Excel异常");}}/*** 导出excel** @param list 导出数据集合* @param sheetName 工作表的名称* @param clazz 实体类* @param merge 是否合并单元格* @param response 响应体* @param options 级联下拉选*/public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, boolean merge, HttpServletResponse response, List<DropDownOptions> options) {try {resetResponse(sheetName, response);ServletOutputStream os = response.getOutputStream();exportExcel(list, sheetName, clazz, merge, os, options);} catch (IOException e) {throw new RuntimeException("导出Excel异常");}}/*** 导出excel** @param list 导出数据集合* @param sheetName 工作表的名称* @param clazz 实体类* @param os 输出流*/public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, OutputStream os) {exportExcel(list, sheetName, clazz, false, os, null);}/*** 导出excel** @param list 导出数据集合* @param sheetName 工作表的名称* @param clazz 实体类* @param os 输出流* @param options 级联下拉选内容*/public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, OutputStream os, List<DropDownOptions> options) {exportExcel(list, sheetName, clazz, false, os, options);}/*** 导出excel** @param list 导出数据集合* @param sheetName 工作表的名称* @param clazz 实体类* @param merge 是否合并单元格* @param os 输出流*/public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, boolean merge,OutputStream os, List<DropDownOptions> options) {//配置字体,表头背景等HorizontalCellStyleStrategy horizontalCellStyleStrategy =setConfigure();ExcelWriterSheetBuilder builder = EasyExcel.write(os, clazz).autoCloseStream(false)// 自动适配.registerWriteHandler(new LongestMatchColumnWidthStyleStrategyPlus())//配置字体,表头背景等.registerWriteHandler(horizontalCellStyleStrategy)// 大数值自动转换 防止失真.registerConverter(new ExcelBigNumberConvert()).sheet(sheetName);if (merge) {// 合并处理器builder.registerWriteHandler(new CellMergeStrategy(list, true));}// 添加下拉框操作builder.registerWriteHandler(new ExcelDownHandler(options));//字符串去空格builder.registerWriteHandler(new StringTrimHandler());builder.doWrite(list);}/*** 单表多数据模板导出 模板格式为 {.属性}** @param filename 文件名* @param templatePath 模板路径 resource 目录下的路径包括模板文件名* 例如: excel/temp.xlsx* 重点: 模板文件必须放置到启动类对应的 resource 目录下* @param data 模板需要的数据* @param response 响应体*/public static void exportTemplate(List<Object> data, String filename, String templatePath, HttpServletResponse response) {try {resetResponse(filename, response);ServletOutputStream os = response.getOutputStream();exportTemplate(data, templatePath, os);} catch (IOException e) {throw new RuntimeException("导出Excel异常");}}/*** 单表多数据模板导出 模板格式为 {.属性}** @param templatePath 模板路径 resource 目录下的路径包括模板文件名* 例如: excel/temp.xlsx* 重点: 模板文件必须放置到启动类对应的 resource 目录下* @param data 模板需要的数据* @param os 输出流*/public static void exportTemplate(List<Object> data, String templatePath, OutputStream os) {ClassPathResource templateResource = new ClassPathResource(templatePath);ExcelWriter excelWriter = EasyExcel.write(os).withTemplate(templateResource.getStream()).autoCloseStream(false)// 大数值自动转换 防止失真.registerConverter(new ExcelBigNumberConvert()).build();WriteSheet writeSheet = EasyExcel.writerSheet().build();if (CollUtil.isEmpty(data)) {throw new IllegalArgumentException("数据为空");}// 单表多数据导出 模板格式为 {.属性}for (Object d : data) {excelWriter.fill(d, writeSheet);}excelWriter.finish();}/*** 多表多数据模板导出 模板格式为 {key.属性}** @param filename 文件名* @param templatePath 模板路径 resource 目录下的路径包括模板文件名* 例如: excel/temp.xlsx* 重点: 模板文件必须放置到启动类对应的 resource 目录下* @param data 模板需要的数据* @param response 响应体*/public static void exportTemplateMultiList(Map<String, Object> data, String filename, String templatePath, HttpServletResponse response) {try {resetResponse(filename, response);ServletOutputStream os = response.getOutputStream();exportTemplateMultiList(data, templatePath, os);} catch (IOException e) {throw new RuntimeException("导出Excel异常");}}/*** 多sheet模板导出 模板格式为 {key.属性}** @param filename 文件名* @param templatePath 模板路径 resource 目录下的路径包括模板文件名* 例如: excel/temp.xlsx* 重点: 模板文件必须放置到启动类对应的 resource 目录下* @param data 模板需要的数据* @param response 响应体*/public static void exportTemplateMultiSheet(List<Map<String, Object>> data, String filename, String templatePath, HttpServletResponse response) {try {resetResponse(filename, response);ServletOutputStream os = response.getOutputStream();exportTemplateMultiSheet(data, templatePath, os);} catch (IOException e) {throw new RuntimeException("导出Excel异常");}}/*** 多表多数据模板导出 模板格式为 {key.属性}** @param templatePath 模板路径 resource 目录下的路径包括模板文件名* 例如: excel/temp.xlsx* 重点: 模板文件必须放置到启动类对应的 resource 目录下* @param data 模板需要的数据* @param os 输出流*/public static void exportTemplateMultiList(Map<String, Object> data, String templatePath, OutputStream os) {ClassPathResource templateResource = new ClassPathResource(templatePath);ExcelWriter excelWriter = EasyExcel.write(os).withTemplate(templateResource.getStream()).autoCloseStream(false)// 大数值自动转换 防止失真.registerConverter(new ExcelBigNumberConvert()).build();WriteSheet writeSheet = EasyExcel.writerSheet().build();if (CollUtil.isEmpty(data)) {throw new IllegalArgumentException("数据为空");}for (Map.Entry<String, Object> map : data.entrySet()) {// 设置列表后续还有数据FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();if (map.getValue() instanceof Collection) {// 多表导出必须使用 FillWrapperexcelWriter.fill(new FillWrapper(map.getKey(), (Collection<?>) map.getValue()), fillConfig, writeSheet);} else {excelWriter.fill(map.getValue(), writeSheet);}}excelWriter.finish();}/*** 多sheet模板导出 模板格式为 {key.属性}** @param templatePath 模板路径 resource 目录下的路径包括模板文件名* 例如: excel/temp.xlsx* 重点: 模板文件必须放置到启动类对应的 resource 目录下* @param data 模板需要的数据* @param os 输出流*/public static void exportTemplateMultiSheet(List<Map<String, Object>> data, String templatePath, OutputStream os) {ClassPathResource templateResource = new ClassPathResource(templatePath);ExcelWriter excelWriter = EasyExcel.write(os).withTemplate(templateResource.getStream()).autoCloseStream(false)// 大数值自动转换 防止失真.registerConverter(new ExcelBigNumberConvert()).build();if (CollUtil.isEmpty(data)) {throw new IllegalArgumentException("数据为空");}for (int i = 0; i < data.size(); i++) {WriteSheet writeSheet = EasyExcel.writerSheet(i).build();for (Map.Entry<String, Object> map : data.get(i).entrySet()) {// 设置列表后续还有数据FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();if (map.getValue() instanceof Collection) {// 多表导出必须使用 FillWrapperexcelWriter.fill(new FillWrapper(map.getKey(), (Collection<?>) map.getValue()), fillConfig, writeSheet);} else {excelWriter.fill(map.getValue(), writeSheet);}}}excelWriter.finish();}/*** 重置响应体*/static void resetResponse(String sheetName, HttpServletResponse response) throws UnsupportedEncodingException {String filename = encodingFilename(sheetName);FileUtils.setAttachmentResponseHeader(response, filename);response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8");}/*** 解析导出值 0=男,1=女,2=未知** @param propertyValue 参数值* @param converterExp 翻译注解* @param separator 分隔符* @return 解析后值*/public static String convertByExp(String propertyValue, String converterExp, String separator) {StringBuilder propertyString = new StringBuilder();String[] convertSource = converterExp.split(StringUtils.SEPARATOR);for (String item : convertSource) {String[] itemArray = item.split("=");if (StringUtils.containsAny(propertyValue, separator)) {for (String value : propertyValue.split(separator)) {if (itemArray[0].equals(value)) {propertyString.append(itemArray[1] + separator);break;}}} else {if (itemArray[0].equals(propertyValue)) {return itemArray[1];}}}return StringUtils.stripEnd(propertyString.toString(), separator);}/*** 反向解析值 男=0,女=1,未知=2** @param propertyValue 参数值* @param converterExp 翻译注解* @param separator 分隔符* @return 解析后值*/public static String reverseByExp(String propertyValue, String converterExp, String separator) {StringBuilder propertyString = new StringBuilder();String[] convertSource = converterExp.split(StringUtils.SEPARATOR);for (String item : convertSource) {String[] itemArray = item.split("=");if (StringUtils.containsAny(propertyValue, separator)) {for (String value : propertyValue.split(separator)) {if (itemArray[1].equals(value)) {propertyString.append(itemArray[0] + separator);break;}}} else {if (itemArray[1].equals(propertyValue)) {return itemArray[0];}}}return StringUtils.stripEnd(propertyString.toString(), separator);}/*** 编码文件名*/public static String encodingFilename(String filename) {return IdUtil.fastSimpleUUID() + "_" + filename + ".xlsx";}//配置字体,表头背景等private static HorizontalCellStyleStrategy setConfigure() {// 头的策略WriteCellStyle headWriteCellStyle = new WriteCellStyle();
// // 背景色
// headWriteCellStyle.setFillForegroundColor(IndexedColors.WHITE.getIndex());
// WriteFont headWriteFont = new WriteFont();
// headWriteFont.setFontHeightInPoints((short) 10);
// headWriteCellStyle.setWriteFont(headWriteFont);// 内容的策略WriteCellStyle contentWriteCellStyle = new WriteCellStyle();contentWriteCellStyle.setVerticalAlignment(VerticalAlignment.CENTER);contentWriteCellStyle.setWrapped(true);contentWriteCellStyle.setBorderLeft(BorderStyle.THIN);contentWriteCellStyle.setBorderTop(BorderStyle.THIN);contentWriteCellStyle.setBorderRight(BorderStyle.THIN);contentWriteCellStyle.setBorderBottom(BorderStyle.THIN);//设置// 这个策略是 头是头的样式 内容是内容的样式 其他的策略可以自己实现return new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);}
}
选择工具类ExcelUtils导出
注解:
结果: