Java导出自定义Excel表格,一套组合拳解决

🔵 (一) 功能现状

   🍭目前大部分SpringBoot框架中自带了Excel导出功能,但其中并不支持自定义导出效果的可能性很大。比如很多框架中都能直接支持自动生成关于单表的增删改查操作的前后端代码,但是复杂的多表操作就无法做到,因为复杂的业务操作跟着需求走,自定义Excel表格导出也是如此。

🔵 (二) 核心问题

   🍯想要解决这个自定义问题的我们,大部分是卡在合并单元格的这个环节以及如何做到动态处理单元格这个核心问题的两个方面。那么定义需要做合并单元格处理的字段以及动态处理单元格策略成为了我们的解决这个核心问题的组合拳。

🔴 (三) 合并单元格字段

   🍬在我们实际操作Excel中会发现并不是所有的字段都需要做合并处理,所以Java层面需要指定哪些字段要做合并;而且某些字段内的数据量比较大或者字符数据较长,也需要在Java层面需要指明字段的列宽;同时,我们还需要指明合并字段的主键,以此来作为所导Excel行数以及序列计数的依据。

   1️⃣自定义注解CustomMerge,用于判断是否需要合并,以及合并后哪个字段作为主键。

/**
 * 自定义注解,用于判断是否需要合并以及合并的主键
 */
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface CustomMerge {

    /**
     * 是否需要合并单元格
     */
    boolean needMerge() default false;

    /**
     * 是否是主键,即该字段相同的行合并
     */
    boolean isPk() default false;
}

   2️⃣在实体类RoadSectionTrafficRep中写明@ExcelProperty (字段属性名称),@ColumnWidth (列宽),@CustomMerge (上面的自定义注解)

@Data
@EqualsAndHashCode(callSuper = false)
public class RoadSectionTrafficRep {

    @ExcelProperty(value = "***")
    @ColumnWidth(20)
    @CustomMerge(needMerge = true,isPk = true)
    @ApiModelProperty("***")
    private String NO;

    @ExcelProperty(value = "***")
    @ColumnWidth(20)
    @CustomMerge(needMerge = true)
    @ApiModelProperty("***")
    private String statisticalPeriod;

    @ExcelProperty(value = "***")
    @ColumnWidth(20)
    @CustomMerge(needMerge = true)
    @ApiModelProperty("***")
    private String roadSectionName;

	@ExcelProperty(value = "***")
    @ColumnWidth(20)
    @ApiModelProperty("***")
    private Integer smallVehicleTraffic;

    @ExcelProperty(value = "***")
    @ColumnWidth(20)
    @ApiModelProperty("***")
    private Integer middleVehicleTraffic;

    @ExcelProperty(value = "***")
    @ColumnWidth(20)
    @ApiModelProperty("***")
    private Integer bigVehicleTraffic;

    @ExcelProperty(value = "***")
    @ColumnWidth(20)
    @ApiModelProperty("***")
    private Integer vehicleCnt;

}

🔴 (四) 单元格合并策略

   🍪标记好需要合并的字段信息后,核心的合并策略参考以下代码,这里不做过多的阐述。

/**
 * 自定义单元格合并策略
 */
public class CustomMergeStrategy implements RowWriteHandler {
    /**
     * 主键下标
     */
    private Integer pkIndex;

    /**
     * 需要合并的列的下标集合
     */
    private List<Integer> needMergeColumnIndex = new ArrayList<>();

    /**
     * DTO数据类型
     */
    private Class<?> elementType;

    public CustomMergeStrategy(Class<?> elementType) {
        this.elementType = elementType;
    }

    @Override
    public void afterRowDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Integer relativeRowIndex, Boolean isHead) {
        // 如果是标题,则直接返回
        if (isHead) {
            return;
        }

        // 获取当前sheet
        Sheet sheet = writeSheetHolder.getSheet();

        // 获取标题行
        Row titleRow = sheet.getRow(0);

        if (null == pkIndex) {
            this.lazyInit(writeSheetHolder);
        }

        // 判断是否需要和上一行进行合并
        // 不能和标题合并,只能数据行之间合并
        if (row.getRowNum() <= 1) {
            return;
        }
        // 获取上一行数据
        Row lastRow = sheet.getRow(row.getRowNum() - 1);
        // 将本行和上一行是同一类型的数据(通过主键字段进行判断),则需要合并
        if (lastRow.getCell(pkIndex).getStringCellValue().equalsIgnoreCase(row.getCell(pkIndex).getStringCellValue())) {
            for (Integer needMerIndex : needMergeColumnIndex) {
                CellRangeAddress cellRangeAddress = new CellRangeAddress(row.getRowNum() - 1, row.getRowNum(),
                        needMerIndex, needMerIndex);
                sheet.addMergedRegionUnsafe(cellRangeAddress);
            }
        }
    }

    /**
     * 初始化主键下标和需要合并字段的下标
     */
    private void lazyInit(WriteSheetHolder writeSheetHolder) {

        // 获取当前sheet
        Sheet sheet = writeSheetHolder.getSheet();

        // 获取标题行
        Row titleRow = sheet.getRow(0);
        // 获取DTO的类型
        Class<?> eleType = this.elementType;

        // 获取DTO所有的属性
        Field[] fields = eleType.getDeclaredFields();

        // 遍历所有的字段,因为是基于DTO的字段来构建excel,所以字段数 >= excel的列数
        for (Field theField : fields) {
            // 获取@ExcelProperty注解,用于获取该字段对应在excel中的列的下标
            ExcelProperty easyExcelAnno = theField.getAnnotation(ExcelProperty.class);
            // 为空,则表示该字段不需要导入到excel,直接处理下一个字段
            if (null == easyExcelAnno) {
                continue;
            }
            // 获取自定义的注解,用于合并单元格
            CustomMerge customMerge = theField.getAnnotation(CustomMerge.class);

            // 没有@CustomMerge注解的默认不合并
            if (null == customMerge) {
                continue;
            }

            for (int index = 0; index < fields.length; index++) {
                Cell theCell = titleRow.getCell(index);
                // 当配置为不需要导出时,返回的为null,这里作一下判断,防止NPE
                if (null == theCell) {
                    continue;
                }
                // 将字段和excel的表头匹配上
                if (easyExcelAnno.value()[0].equalsIgnoreCase(theCell.getStringCellValue())) {
                    if (customMerge.isPk()) {
                        pkIndex = index;
                    }

                    if (customMerge.needMerge()) {
                        needMergeColumnIndex.add(index);
                    }
                }
            }
        }

        // 没有指定主键,则异常
        if (null == this.pkIndex) {
            throw new IllegalStateException("使用@CustomMerge注解必须指定主键");
        }

    }
}

🔴 (五) Controller层代码

   🍰导出说到底是对于查询到的数据再做一层处理的功能,所以将其代码放在Controller层即可。中间涉及到的包都是Alibaba中的依赖包,能自动导入,大家可以多点进去看看里面内部的实现机制。

   1️⃣导出表格处理,中间自定义的合并处理可以自行设置。

	@ApiOperation("导出*****表")
    @SneakyThrows
    @GetMapping("/exportRoadSectionTrafficReport")
    public void exportRoadSectionTrafficReport(HttpServletResponse response){
        List<RoadSectionTrafficRep> orderList = dataReportService.getRoadSectionTraffic();
        
        //中间自定义的合并处理,自由发挥咯... ...

        setExcelRespProp(response, "******表");
        EasyExcel.write(response.getOutputStream())
                .head(RoadSectionTrafficRep.class)
                .registerWriteHandler(new CustomMergeStrategy(RoadSectionTrafficRep.class))
                .excelType(ExcelTypeEnum.XLSX)
                .sheet("*****表")
                .doWrite(orderList);
    }

   2️⃣设置Excel下载响应头属性

	/**
     * 设置excel下载响应头属性
     */
    private void setExcelRespProp(HttpServletResponse response, String rawFileName) throws UnsupportedEncodingException {
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        String fileName = URLEncoder.encode(rawFileName, "UTF-8").replaceAll("\\+", "%20");
        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
    }

🍆🍆🍆路过的小伙伴,如果本篇博文对你的学习或者工作有所帮助,可以点赞+收藏+关注一波呀~👊👊👊小编后续每过一段时间会整理出相关项目实例的博文,感谢您的支持哦!!!✈️✈️✈️
在这里插入图片描述

  • 9
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
生成 Java 自定义 Excel 表格导出代码可以使用以下代码片段作为参考: ```java public class ExcelUtil { /** * 导出 Excel 表格 * * @param filename 文件名 * @param headers 表头 * @param data 表格数据 * @param out 输出流 * @throws IOException */ public static void exportExcel(String filename, String[] headers, List<Map<String, Object>> data, OutputStream out) throws IOException { XSSFWorkbook workbook = new XSSFWorkbook(); XSSFSheet sheet = workbook.createSheet(filename); // 设置表头样式 XSSFCellStyle headerStyle = workbook.createCellStyle(); headerStyle.setAlignment(HorizontalAlignment.CENTER); headerStyle.setFillForegroundColor(IndexedColors.LIGHT_TURQUOISE.getIndex()); headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); headerStyle.setBorderTop(BorderStyle.THIN); headerStyle.setBorderBottom(BorderStyle.THIN); headerStyle.setBorderLeft(BorderStyle.THIN); headerStyle.setBorderRight(BorderStyle.THIN); // 创建表头行 XSSFRow headerRow = sheet.createRow(0); for (int i = 0; i < headers.length; i++) { XSSFCell cell = headerRow.createCell(i); cell.setCellValue(headers[i]); cell.setCellStyle(headerStyle); } // 设置表格数据样式 XSSFCellStyle dataStyle = workbook.createCellStyle(); dataStyle.setBorderTop(BorderStyle.THIN); dataStyle.setBorderBottom(BorderStyle.THIN); dataStyle.setBorderLeft(BorderStyle.THIN); dataStyle.setBorderRight(BorderStyle.THIN); // 填充表格数据 for (int i = 0; i < data.size(); i++) { XSSFRow dataRow = sheet.createRow(i + 1); Map<String, Object> rowData = data.get(i); for (int j = 0; j < headers.length; j++) { XSSFCell cell = dataRow.createCell(j); Object value = rowData.get(headers[j]); if (value != null) { if (value instanceof String) { cell.setCellValue((String) value); } else if (value instanceof Integer) { cell.setCellValue((Integer) value); } else if (value instanceof Double) { cell.setCellValue((Double) value); } else if (value instanceof Date) { cell.setCellValue((Date) value); XSSFCellStyle dateStyle = workbook.createCellStyle(); dateStyle.setDataFormat(workbook.getCreationHelper().createDataFormat().getFormat("yyyy-MM-dd HH:mm:ss")); cell.setCellStyle(dateStyle); } } cell.setCellStyle(dataStyle); } } // 自适应列宽 for (int i = 0; i < headers.length; i++) { sheet.autoSizeColumn(i); } // 输出 Excel 文件 workbook.write(out); workbook.close(); } } ``` 在这里,我们使用了 Apache POI 库来生成 Excel 表格。在 `exportExcel` 方法中,我们首先创建了一个 `XSSFWorkbook` 对象来表示 Excel 文件。然后,我们创建了一个 `XSSFSheet` 对象来表示表格,并设置了表头样式和表头行。接着,我们填充了表格数据,并根据数据类型设置了单元格样式。最后,我们自适应列宽,并将 Excel 文件输出到指定的输出流中。 要使用该工具类导出 Excel 表格,只需要调用 `exportExcel` 方法即可,如下所示: ```java List<Map<String, Object>> data = new ArrayList<>(); Map<String, Object> row1 = new LinkedHashMap<>(); row1.put("id", 1); row1.put("name", "张三"); row1.put("age", 20); row1.put("create_time", new Date()); data.add(row1); Map<String, Object> row2 = new LinkedHashMap<>(); row2.put("id", 2); row2.put("name", "李四"); row2.put("age", 22); row2.put("create_time", new Date()); data.add(row2); String[] headers = {"id", "name", "age", "create_time"}; String filename = "test.xlsx"; OutputStream out = new FileOutputStream(filename); ExcelUtil.exportExcel(filename, headers, data, out); out.close(); ``` 在这个例子中,我们首先创建了一个包含两行数据的表格数据 `data`,然后创建了一个包含表头信息的字符串数组 `headers`,并指定了导出Excel 文件名 `filename` 和输出流 `out`。最后,我们调用 `exportExcel` 方法导出 Excel 表格,并关闭输出流。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Fish_Vast

您的打赏是对我最大的支持!!!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值