Hutool操作excel相较于easyexcel更加的简单灵活,适合数据量小、格式不太复杂的excel操作

 此excel导出格式较为复杂,较多的合并单元格。首先定义具体类,采用list集合封装,调用合并方法进行合并单元格即可。

hutool读取 maven properties_字段

导入依赖

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

<dependency>
   <groupId>org.apache.poi</groupId>
   <artifactId>poi-ooxml</artifactId>
   <version>4.0.0</version>
</dependency>
//导出excel    
    // 通过工具类创建writer
    ExcelWriter writer = ExcelUtil.getWriter();
    int number = HutoolExcelUtil.setHeaderAlias(writer, DataTbbTableVO.class);

    // 默认的,未添加alias的属性也会写出,如果想只写出加了别名的字段,可以调用此方法排除之
    writer.setOnlyAlias(true);
    //标题字体样式
    Font font = HutoolExcelUtil.createFont(writer, true, false, "仿宋_GB2312", 22);
    //合并单元格后的标题行,使用默认标题样式
    CellStyle cellStyle = HutoolExcelUtil.createCellStyle(writer, font, true, VerticalAlignment.CENTER, HorizontalAlignment.CENTER);
    writer.merge(0, 0, 0, number - 1, dataTbbInfo.getTitle(), cellStyle);
    //跳过当前行  这里我也不知道为什么要调用,不调用内容表头不会显示
    writer.passCurrentRow();
    //写入标题
    writer.writeHeadRow(Arrays.asList(""));
    //全局字体默认样式
    Font font1 = HutoolExcelUtil.createFont(writer, false, false, "仿宋_GB2312", 12);
    //设置全局样式
    StyleSet styleSet = HutoolExcelUtil.setBaseGlobalStyle(writer, font1);
    //设置标题样式
    HutoolExcelUtil.createHeadCellStyle(styleSet, font1, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
    //手动设置列宽
    HutoolExcelUtil.setSizeColumn(writer, Arrays.asList(5, 12, 16, 15, 14, 15, 30, 15, 15, 12), number - 1);

    //合并单元格    
    Map<String, Object> merge = this.merge(dataTbbInfo.getDataTbbTableVOS(), writer);

    //设置筛选下拉框
    HutoolExcelUtil.setFilter(writer, "A2:D" + merge.get("index"));

    //一次性写出内容,使用默认样式,强制输出标题
    writer.write((List<List<Object>>) merge.get("result"), true);

    //下载文件
    HutoolExcelUtil.downloadExcel(response, dataTbbInfo.getTitle(), writer);
/**
 * @author z
 * @Description 导出excel工具类
 * @date 2022/11/21 18:21
 */
public class HutoolExcelUtil {

    /**
     * 方法描述: 全局基础样式设置
     * 默认 全局水平居中+垂直居中
     * 默认 自动换行
     * 默认单元格边框颜色为黑色,细线条
     * 默认背景颜色为白色
     *
     * @param writer writer
     * @param font   字体样式
     * @return cn.hutool.poi.excel.StyleSet
     */
    public static StyleSet setBaseGlobalStyle(ExcelWriter writer, Font font) {
        //全局样式设置
        StyleSet styleSet = writer.getStyleSet();
        //设置全局文本居中
        styleSet.setAlign(HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
        //设置全局字体样式
        styleSet.setFont(font, true);
        //设置背景颜色 第二个参数表示是否将样式应用到头部
        styleSet.setBackgroundColor(IndexedColors.WHITE, true);
        //设置自动换行 当文本长于单元格宽度是否换行
        styleSet.setWrapText();
        // 设置全局边框样式
        styleSet.setBorder(BorderStyle.THIN, IndexedColors.BLACK);
        return styleSet;
    }

    /**
     * 方法描述: 设置标题的基础样式
     *
     * @param styleSet            StyleSet
     * @param font                字体样式
     * @param horizontalAlignment 水平排列方式
     * @param verticalAlignment   垂直排列方式
     * @return org.apache.poi.ss.usermodel.CellStyle
     */
    public static CellStyle createHeadCellStyle(StyleSet styleSet, Font font,
                                                HorizontalAlignment horizontalAlignment,
                                                VerticalAlignment verticalAlignment) {
        CellStyle headCellStyle = styleSet.getHeadCellStyle();
        headCellStyle.setAlignment(horizontalAlignment);
        headCellStyle.setVerticalAlignment(verticalAlignment);
        headCellStyle.setFont(font);
        return headCellStyle;
    }

    /**
     * 方法描述: 设置基础字体样式字体 这里保留最基础的样式使用
     *
     * @param bold     是否粗体
     * @param fontName 字体名称
     * @param fontSize 字体大小
     * @return org.apache.poi.ss.usermodel.Font
     */
    public static Font createFont(ExcelWriter writer, boolean bold, boolean italic, String fontName, int fontSize) {
        Font font = writer.getWorkbook().createFont();
        //设置字体名称
        font.setFontName(fontName);
        //设置是否斜体
        font.setItalic(italic);
        //设置字体大小 以磅为单位
        font.setFontHeightInPoints((short) fontSize);
        //设置是否加粗
        font.setBold(bold);
        return font;
    }

    /**
     * 方法描述: 设置行或单元格基本样式
     *
     * @param writer              writer
     * @param font                字体样式
     * @param verticalAlignment   垂直居中
     * @param horizontalAlignment 水平居中
     * @return void
     */
    public static CellStyle createCellStyle(ExcelWriter writer, Font font, boolean wrapText,
                                            VerticalAlignment verticalAlignment,
                                            HorizontalAlignment horizontalAlignment) {
        CellStyle cellStyle = writer.getWorkbook().createCellStyle();
        cellStyle.setVerticalAlignment(verticalAlignment);
        cellStyle.setAlignment(horizontalAlignment);
        cellStyle.setWrapText(wrapText);
        cellStyle.setFont(font);
        return cellStyle;
    }

    /**
     * 方法描述: 设置边框样式
     *
     * @param cellStyle 样式对象
     * @param bottom    下边框
     * @param left      左边框
     * @param right     右边框
     * @param top       上边框
     * @return void
     */
    public static void setBorderStyle(CellStyle cellStyle, BorderStyle bottom, BorderStyle left, BorderStyle right,
                                      BorderStyle top) {
        cellStyle.setBorderBottom(bottom);
        cellStyle.setBorderLeft(left);
        cellStyle.setBorderRight(right);
        cellStyle.setBorderTop(top);
    }

    /**
     * 方法描述: 获取对象需要导出的列和别名 这里按字段顺序来(可以在自定义注解上添加属性标识字段顺序,重写方法)
     * 在需要导出的字段上贴上注解 这里是@ExcelProperty,也可以用自定义注解
     * 注解需要有value 标识别名 order 标识字段顺序
     *
     * @param clazz 对象类型
     * @return int 导出的字段个数
     */
        public static int setHeaderAlias(ExcelWriter writer, ExcelReader reader, Class<?> clazz) {
        //需要导出的字段数
        TreeMap<Integer, Map<String, String>> headerAliasMap = new TreeMap<>();
        //判断类中字段类型和是否有别名注释
        HutoolExcelUtil.getResult(headerAliasMap, clazz);
        LinkedHashMap<String, String> linkedMap = new LinkedHashMap<>();
        for (Map.Entry<Integer, Map<String, String>> entry : headerAliasMap.entrySet()) {
            linkedMap.putAll(entry.getValue());
        }
        Optional.ofNullable(writer).ifPresent(w -> w.setHeaderAlias(linkedMap));
        Optional.ofNullable(reader).ifPresent(r -> r.setHeaderAlias(linkedMap));
        return linkedMap.size();
    }

    /**
     * 判断类中字段类型和是否有别名注释
     *
     * @param headerAliasMap map集合
     * @param clazz          类
     */
    private static void getResult(TreeMap<Integer, Map<String, String>> headerAliasMap, Class<?> clazz) {
        Field[] fields = clazz.getDeclaredFields();
        Arrays.stream(fields).forEach(f -> {
            //获取字段类型
            Class<?> type = f.getType();
            //判断字段类型是否是list
            if (type.equals(List.class)) {
                //获取集合的泛型对象
                Type genericType = f.getGenericType();
                if (genericType instanceof ParameterizedType) {
                    ParameterizedType pt = (ParameterizedType) genericType;
                    Class<?> aClass = (Class<?>) pt.getActualTypeArguments()[0];
                    //递归调用
                    HutoolExcelUtil.getResult(headerAliasMap, aClass);
                }
            }
            //判断是否有注解
            if (f.isAnnotationPresent(ExcelProperty.class)) {
                ExcelProperty annotation = f.getAnnotation(ExcelProperty.class);
                //别名
                String[] value = annotation.value();
                int order = annotation.order();
                Map<String, String> alisMap = new HashMap<>();
                String fieldName = f.getName();
                //字段名
                String headerAlias = value[0];
                alisMap.put(fieldName, headerAlias);
                headerAliasMap.put(order, alisMap);
            }
        });
    }

    /**
     * 方法描述: 自适应宽度(中文支持)
     *
     * @param sheet 页
     * @param size  因为for循环从0开始,size值为 列数-1
     * @return void
     */
    public static void setSizeAutoColumn(Sheet sheet, int size) {
        for (int columnNum = 0; columnNum <= size; columnNum++) {
            int columnWidth = sheet.getColumnWidth(columnNum) / 256;
            for (int rowNum = 0; rowNum <= sheet.getLastRowNum(); rowNum++) {
                Row currentRow;
                //当前行未被使用过
                if (sheet.getRow(rowNum) == null) {
                    currentRow = sheet.createRow(rowNum);
                } else {
                    currentRow = sheet.getRow(rowNum);
                }

                if (currentRow.getCell(columnNum) != null) {
                    Cell currentCell = currentRow.getCell(columnNum);
                    if (currentCell.getCellType() == CellType.STRING) {
                        int length = currentCell.getStringCellValue().getBytes().length;
                        if (columnWidth < length) {
                            columnWidth = length;
                        }
                    }
                }
            }
            sheet.setColumnWidth(columnNum, columnWidth * 256);
        }
    }


    /**
     * 方法描述: 手动宽度(中文支持)
     */
    public static void setSizeColumn(ExcelWriter excelWriter, List<Integer> list, int size) {
        for (int columnNum = 0; columnNum <= size; columnNum++) {

            excelWriter.setColumnWidth(columnNum, list.get(columnNum));
        }
    }

    /**
     * 方法描述: excel 导出下载
     *
     * @param response 响应
     * @param fileName 文件名
     * @param writer   writer
     * @return void
     */
    public static void downloadExcel(HttpServletResponse response, String fileName, ExcelWriter writer) {
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
        // xxx.xlsx是弹出下载对话框的文件名
        ServletOutputStream out = null;
        try {
            // 设置请求头属性
            fileName = URLEncoder.encode(fileName, "UTF-8");
            response.setHeader("Content-Disposition",
                    "attachment;filename=" + new String((fileName + ".xlsx").getBytes(), StandardCharsets.ISO_8859_1));
            out = response.getOutputStream();
            // 写出到文件
            writer.flush(out, true);
            // 关闭writer,释放内存
            writer.close();
            // 此处记得关闭输出Servlet流
            IoUtil.close(out);
        } catch (IOException e) {
            throw new BusinessException("导出失败");
        }
    }

    /**
     * 方法描述: 最简单的导出,直接将数据放到excel
     * 导入的数据最好是List<map>格式数据,主要原因是List<Object> ,
     * 对象有字段是LocalDate或者是LocalDateTime类型的数据,
     * 时间格式化可能达不到想要的效果
     *
     * @param data       对象集合 两种
     * @param globalFont 全局字体样式
     * @param clazz      导出的对应class类型
     * @return void
     */
    public static ExcelWriter exportBaseExcel(ExcelWriter writer, List<?> data, Font globalFont, Class clazz) {
        Sheet sheet = writer.getSheet();
        writer.setStyleSet(setBaseGlobalStyle(writer, globalFont));
        int columnSize = setHeaderAlias(writer, clazz);
        writer.setOnlyAlias(true);
        writer.write(data);
        HutoolExcelUtil.setSizeAutoColumn(sheet, columnSize);
        return writer;
    }

    /**
     * 设置下拉列表框
     *
     * @param writer   ExcelWriter
     * @param str      内容
     * @param firstRow 第一行
     * @param lastRow  第二行
     * @param firstCol 第一列
     * @param lastCol  第二列
     */
    public static void setSelect(ExcelWriter writer, String[] str, int firstRow, int lastRow, int firstCol, int lastCol) {
        CellRangeAddressList addressList = new CellRangeAddressList(firstRow, lastRow, firstCol, lastCol);
        DataValidationHelper helper = writer.getSheet().getDataValidationHelper();
        DataValidationConstraint constraint = helper.createExplicitListConstraint(str);
        DataValidation dataValidation = helper.createValidation(constraint, addressList);
        writer.addValidationData(dataValidation);
    }

    /**
     * 设置筛选功能
     *
     * @param writer ExcelWriter
     * @param str    要筛选的坐标  例如:A2:D6
     */
    public static void setFilter(ExcelWriter writer, String str) {
        Sheet sheet = writer.getSheet();
        CellRangeAddress c = CellRangeAddress.valueOf(str);
        sheet.setAutoFilter(c);
    }


}



//注解类
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ExcelProperty {

    String[] value();

    int order();
}
/**
    合并单元格方法   
*/
 private Map<String, Object> merge(List<DataTbbTableVO> dataTbbTableVOS, ExcelWriter writer) {
        Map<String, Object> map = new HashMap<>(2);
        //有个字段要合并的就定义多少
        int index1 = 2;
        int index2 = 2;

        // 后面要用到的临时对象
        List<List<Object>> rows = new LinkedList<>();
        List<Object> rowA;

        //按照序号合并单元格
        Map<Integer, List<DataTbbTableVO>> reservoirNameMaps = dataTbbTableVOS.stream()
                .collect(Collectors.groupingBy(DataTbbTableVO::getNumber, LinkedHashMap::new, Collectors.toList()));

        for (Map.Entry<Integer, List<DataTbbTableVO>> reservoirNameEntry : reservoirNameMaps.entrySet()) {

            List<DataTbbTableVO> reservoirNameValue = reservoirNameEntry.getValue();
            //根据数据条数设置合并单元格信息
            if (reservoirNameValue.size() == 1) {//一条数据不合并
                index1 = index1 + reservoirNameValue.size();
            } else {
                //规则编写
                writer.merge(index1, index1 + reservoirNameValue.size() - 1, 0, 0, null, true);
                writer.merge(index1, index1 + reservoirNameValue.size() - 1, 1, 1, null, true);
                index1 = index1 + reservoirNameValue.size();

                //按照二级指标合并单元格
                Map<String, List<DataTbbTableVO>> reservoirScaleMaps = reservoirNameValue.stream()
                        .collect(Collectors.groupingBy(DataTbbTableVO::getSecondLevel, LinkedHashMap::new, Collectors.toList()));

                for (Map.Entry<String, List<DataTbbTableVO>> reservoirScaleEntry : reservoirScaleMaps.entrySet()) {
                    List<DataTbbTableVO> reservoirScaleValue = reservoirScaleEntry.getValue();
                    //根据数据条数设置合并单元格信息
                    if (reservoirScaleValue.size() == 1) {//一条数据不合并
                        index2 = index2 + reservoirScaleValue.size();
                    } else {

                        //规则编写
                        writer.merge(index2, index2 + reservoirScaleValue.size() - 1, 2, 2, null, true);
                        writer.merge(index2, index2 + reservoirScaleValue.size() - 1, 3, 3, null, true);
                        writer.merge(index2, index2 + reservoirScaleValue.size() - 1, 4, 4, null, true);
                        writer.merge(index2, index2 + reservoirScaleValue.size() - 1, 5, 5, null, true);
                        index2 = index2 + reservoirScaleValue.size();
                    }
                }
            }
            //保存数据
            for (DataTbbTableVO dataTbbTableVO : reservoirNameValue) {
                rowA = CollUtil.newArrayList(
                        dataTbbTableVO.getNumber(),
                        dataTbbTableVO.getFirstLevel(),
                        dataTbbTableVO.getSecondLevel(),
                        dataTbbTableVO.getSecondLevelData(),
                        dataTbbTableVO.getSecondLevelRank(),
                        dataTbbTableVO.getSecondBest(),
                        dataTbbTableVO.getThirdLevel(),
                        dataTbbTableVO.getThirdLevelData(),
                        dataTbbTableVO.getThirdLevelRank(),
                        dataTbbTableVO.getThirdBest()
                );
                rows.add(rowA);
            }
        }

        map.put("index", index2);
        map.put("result", rows);
        return map;
    }