代碼
- 首先自定義注解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導出
注解:
結果: