easyExcel - 按模板导出

目录


前言

Java-easyExcel入门教程:https://blog.csdn.net/xhmico/article/details/134714025

之前有介绍过如何使用 easyExcel,以及写了两个入门的 demo ,这两个 demo 能应付在开发中大多数的导入和导出需求,不过有时候面对一些复杂的表格,就会有点不够用,该篇是如何使用模板实现导出功能


一、情景介绍

在实际的开发过程中可能会遇到需要导出一些带有复杂表头的表格,比如:工资表和考勤表

或者是类似于发票、报价单这种具有模板性质的表格

如果仅仅通过代码去实现是比较困难的,通常情况下会写一个模板 excel,模板中的需要变动的数据用占位符替代,导出的文件按照该模板填充数据


二、文档介绍

easyexcel 也提供了根据模板写入的功能,可以用较为简单的方式实现上述功能,其基本步骤为:

  • ① 读取模板文件
  • ② 填充模板中的占位符

2.1 读取模板

官方文档:根据模板写入

在官方文档中给出了如何使用 easyexcel 读取模板的代码示例:

官网代码示例:

java 复制代码
    /**
     * 根据模板写入
     * <p>1. 创建excel对应的实体对象 参照{@link IndexData}
     * <p>2. 使用{@link ExcelProperty}注解指定写入的列
     * <p>3. 使用withTemplate 写取模板
     * <p>4. 直接写即可
     */
    @Test
    public void templateWrite() {
        String templateFileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
        String fileName = TestFileUtil.getPath() + "templateWrite" + System.currentTimeMillis() + ".xlsx";
        // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
        // 这里要注意 withTemplate 的模板文件会全量存储在内存里面,所以尽量不要用于追加文件,如果文件模板文件过大会OOM
        // 如果要再文件中追加(无法在一个线程里面处理,可以在一个线程的建议参照多次写入的demo) 建议临时存储到数据库 或者 磁盘缓存(ehcache) 然后再一次性写入
        EasyExcel.write(fileName, DemoData.class).withTemplate(templateFileName).sheet().doWrite(data());
    }

从上述示例代码中可以看到读取模板文件的方法为:withTemplate()

java 复制代码
    public ExcelWriterBuilder withTemplate(InputStream templateInputStream) {
        this.writeWorkbook.setTemplateInputStream(templateInputStream);
        return this;
    }

    public ExcelWriterBuilder withTemplate(File templateFile) {
        this.writeWorkbook.setTemplateFile(templateFile);
        return this;
    }

    public ExcelWriterBuilder withTemplate(String pathName) {
        return this.withTemplate(new File(pathName));
    }

2.2 填充模板

官方文档:填充Excel

注意:这里建议先去看下官方文档中给的示例,会比较好理解

  • 在模板中占位符用 {变量} 表示,比如 {name}{age}
  • 如果占位符为列表,则使用 {.变量} 表示,比如:{.name}{.age}

进行填充的方法为:fill()

java 复制代码
    public ExcelWriter fill(Object data, WriteSheet writeSheet) {
        return this.fill((Object)data, (FillConfig)null, writeSheet);
    }

    public ExcelWriter fill(Object data, FillConfig fillConfig, WriteSheet writeSheet) {
        this.excelBuilder.fill(data, fillConfig, writeSheet);
        return this;
    }

    public ExcelWriter fill(Supplier<Object> supplier, WriteSheet writeSheet) {
        return this.fill((Object)supplier.get(), (FillConfig)null, writeSheet);
    }

    public ExcelWriter fill(Supplier<Object> supplier, FillConfig fillConfig, WriteSheet writeSheet) {
        this.excelBuilder.fill(supplier.get(), fillConfig, writeSheet);
        return this;
    }

EasyExcel 中,FillConfig 类用于配置单元格填充相关的设置。

以下是一些 FillConfig 类中常用的配置项:

  • forceNewRow:是否强制写入到新行。当设置为 true 时,每次写入都会创建新的行,而不是在现有行上进行覆盖
  • direction:填充方向,可以指定是水平填充还是垂直填充
    • 属性:
      • WriteDirectionEnum.VERTICAL:垂直填充
      • WriteDirectionEnum.HORIZONTAL:水平填充

如果有多列组合填充的情况,通过 {前缀.变量} 的形式区分不同的列表,例如

使用 FillWrapper 构建 Excel 写入时的填充模板

java 复制代码
			...
			
            excelWriter.fill(new FillWrapper("data1", data()), writeSheet);
            excelWriter.fill(new FillWrapper("data2", data()), writeSheet);
            excelWriter.fill(new FillWrapper("data3", data()), writeSheet);
            
            ...

三、代码示例

简单介绍了 easyexcel 如何使用模板进行导出,接下来就通过几个简单的案例进行演示

以下案例使用的模板文件均可在文章顶部下载


3.1 案例一:工资表

编写 excel 模板:

实体类:

StaffSalaryEntity.java

java 复制代码
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class StaffSalaryEntity {

    @ApiModelProperty(value = "姓名")
    private String name;

    @ApiModelProperty(value = "职称")
    private String post;

    @ApiModelProperty(value = "月薪")
    private BigDecimal mouthSalary;

    @ApiModelProperty(value = "时薪")
    private BigDecimal hourSalary;

    @ApiModelProperty(value = "应出勤天数")
    private Double shouldAttend;

    @ApiModelProperty(value = "出勤天数")
    private Double actualAttend;

    @ApiModelProperty(value = "平时加班时数")
    private Double overtime;

    @ApiModelProperty(value = "周末加班时数")
    private Double weekOvertime;

    @ApiModelProperty(value = "国假天数")
    private Double holiday;

    @ApiModelProperty(value = "正班薪资")
    private BigDecimal normalSalary;

    @ApiModelProperty(value = "平时加班薪资")
    private BigDecimal overtimeSalary;

    @ApiModelProperty(value = "周末加班薪资")
    private BigDecimal weekOvertimeSalary;

    @ApiModelProperty(value = "国假薪资")
    private BigDecimal holidaySalary;

    @ApiModelProperty(value = "岗位津贴")
    private BigDecimal postSubsidy;

    @ApiModelProperty(value = "全勤补贴")
    private BigDecimal fullAttendSubsidy;

    @ApiModelProperty(value = "全勤奖")
    private BigDecimal award;

    @ApiModelProperty(value = "事假缺勤扣款")
    private BigDecimal deduction;

    @ApiModelProperty(value = "社保费用")
    private BigDecimal social;

    @ApiModelProperty(value = "应得薪资")
    private BigDecimal shouldSalary;

    @ApiModelProperty(value = "个人扣税")
    private BigDecimal selfTax;

    @ApiModelProperty(value = "实发薪资")
    private BigDecimal actualSalary;

    @ApiModelProperty(value = "员工签名")
    private BigDecimal sign;
}

实体类中的字段要跟模板中占位符中的变量对应

代码示例:

java 复制代码
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.fill.FillConfig;
import com.mike.server.system.domain.excel.ProductOfferExcelEntity;
import com.mike.server.system.entity.StaffSalaryEntity;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

public class TestDemo {

    /**
     * 案例一:工资表
     */
    @Test
    public void salaryList() {
        // 模板文件路径
        String templateFilePath = "D:\\excel-files\\gzb-template.xlsx";
        // 输出文件路径
        String outFilePath = "D:\\excel-files\\gzb.xlsx";

        // 创建 ExcelWriter 实例
        ExcelWriter writer = EasyExcel
                // 写入到
                .write(outFilePath)
                // 指定模板
                .withTemplate(templateFilePath)
                .build();

        WriteSheet sheet = EasyExcel.writerSheet().build();

        // 获取员工工资数据
        List<StaffSalaryEntity> staffSalaryEntities = getStaffSalaryEntities();

        FillConfig fillConfig = FillConfig.builder()
                // 开启填充换行
                .forceNewRow(true)
                .build();

        // 执行填充操作
        writer.fill(staffSalaryEntities, fillConfig, sheet);

        // 结束
        writer.finish();
    }

    public List<StaffSalaryEntity> getStaffSalaryEntities() {
        List<StaffSalaryEntity> list = new ArrayList<>();

        list.add(StaffSalaryEntity.builder()
                .name("米大傻")
                .post("开发")
                .mouthSalary(new BigDecimal(1320))
                .hourSalary(new BigDecimal("7.59"))
                .shouldAttend(21.0)
                .actualAttend(21.0)
                .overtime(21.0)
                .weekOvertime(8.0)
                .holiday(0.0)
                .normalSalary(new BigDecimal(1320))
                .overtimeSalary(new BigDecimal("238.97"))
                .weekOvertimeSalary(new BigDecimal("242.76"))
                .holidaySalary(new BigDecimal(0))
                .postSubsidy(new BigDecimal(0))
                .award(new BigDecimal(20))
                .deduction(new BigDecimal(0))
                .social(new BigDecimal("113.6"))
                .shouldSalary(new BigDecimal("1688.12"))
                .selfTax(new BigDecimal(0))
                .actualSalary(new BigDecimal("1688.1"))
                .build());


        list.add(StaffSalaryEntity.builder()
                .name("曹大力")
                .post("店长")
                .mouthSalary(new BigDecimal(13200))
                .hourSalary(new BigDecimal("7.59"))
                .shouldAttend(21.0)
                .actualAttend(21.0)
                .overtime(21.0)
                .weekOvertime(8.0)
                .holiday(0.0)
                .normalSalary(new BigDecimal(1320))
                .overtimeSalary(new BigDecimal("238.97"))
                .weekOvertimeSalary(new BigDecimal("242.76"))
                .holidaySalary(new BigDecimal(0))
                .postSubsidy(new BigDecimal(0))
                .award(new BigDecimal(20))
                .deduction(new BigDecimal(0))
                .social(new BigDecimal("113.6"))
                .shouldSalary(new BigDecimal("13200.12"))
                .selfTax(new BigDecimal(0))
                .actualSalary(new BigDecimal("13200.1"))
                .build());

        list.add(StaffSalaryEntity.builder()
                .name("张大仙")
                .post("经理")
                .mouthSalary(new BigDecimal(13200))
                .hourSalary(new BigDecimal("7.59"))
                .shouldAttend(21.0)
                .actualAttend(21.0)
                .overtime(21.0)
                .weekOvertime(8.0)
                .holiday(0.0)
                .normalSalary(new BigDecimal(1320))
                .overtimeSalary(new BigDecimal("238.97"))
                .weekOvertimeSalary(new BigDecimal("242.76"))
                .holidaySalary(new BigDecimal(0))
                .postSubsidy(new BigDecimal(0))
                .deduction(new BigDecimal(0))
                .social(new BigDecimal("113.6"))
                .shouldSalary(new BigDecimal("13200.12"))
                .selfTax(new BigDecimal(0))
                .actualSalary(new BigDecimal("13200.1"))
                .build());

        return list;
    }
}

展示:


3.2 案例二:报价单

编写模板:

模板文件放在项目的 resources 目录下的 templates/excel 文件夹下

实体类:

OfferDetailEntity.java

java 复制代码
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OfferDetailEntity {

    @ApiModelProperty(value = "公司名称")
    private String companyName;

    @ApiModelProperty(value = "报价日期")
    private String offerDate;

    @ApiModelProperty(value = "报价有效期")
    private String offerValidDate;

    @ApiModelProperty(value = "客户名称")
    private String customerName;

    @ApiModelProperty(value = "客户地址")
    private String customerAddress;

    @ApiModelProperty(value = "联系方式")
    private String contact;

    @ApiModelProperty(value = "合计数量")
    private Integer totalQty;

    @ApiModelProperty(value = "合计金额")
    private BigDecimal totalAmount;
}

ProductOfferEntity.java

java 复制代码
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ProductOfferEntity {

    @ApiModelProperty(value = "产品名称")
    private String productName;

    @ApiModelProperty(value = "型号及规格")
    private String typeSpec;

    @ApiModelProperty(value = "数量")
    private Integer quantity;

    @ApiModelProperty(value = "单价")
    private BigDecimal price;

    @ApiModelProperty(value = "金额")
    private BigDecimal amount;

    @ApiModelProperty(value = "备注")
    private String remark;

}

代码示例:

TestController.java

java 复制代码
    @GetMapping("/export/by-template")
    @ApiOperation(value = "导出-按模板格式", produces = "application/octet-stream")
    public ResponseBean<String> exportByTemplate(HttpServletResponse response) {
        testService.exportByTemplate(response);
        return ResponseBean.success();
    }

TestService.java

java 复制代码
    /**
     * 导出-按模板格式
     */
    void exportByTemplate(HttpServletResponse response);

TestServiceImpl.java

java 复制代码
    @SneakyThrows
    @Override
    public void exportByTemplate(HttpServletResponse response) {

        ServletOutputStream out = response.getOutputStream();

        EasyExcelUtil.initResponse(response, "报价单");

        // 文件模板输入流,将 excel 模板放到 resources 目录下
        InputStream templateFile = new ClassPathResource("templates/excel/bjd.xlsx").getInputStream();

        ExcelWriter writer = EasyExcel
                .write(out)
                .withTemplate(templateFile)
                .build();

        WriteSheet sheet = EasyExcel.writerSheet().build();

        /*
        HashMap<String, Object> offerDetail = new HashMap<>();
        offerDetail.put("companyName", "多加辣科技");
        offerDetail.put("offerDate", "2024-04-07");
        offerDetail.put("offerValidDate", "2024-04-11");
        offerDetail.put("customerName", "米大傻");
        offerDetail.put("customerAddress", "广东省广州市");
        offerDetail.put("contact", "078-182****4568");
        offerDetail.put("totalQty", 5);
        offerDetail.put("totalAmount", new BigDecimal(10300));
        */

        OfferDetailEntity offerDetail = OfferDetailEntity.builder()
                .companyName("多加辣科技")
                .offerDate("2024-04-07")
                .offerValidDate("2024-04-11")
                .customerName("米大傻")
                .customerAddress("广东省广州市")
                .contact("078-182****4568")
                .totalQty(5)
                .totalAmount(new BigDecimal(10300))
                .build();

        // 填充普通占位符
        // 这里 data 使用对象或者 Map 都可以
        writer.fill(offerDetail, sheet);

        List<ProductOfferEntity> list = new ArrayList<>();
        list.add(ProductOfferEntity.builder()
                .productName("电脑")
                .typeSpec("联想")
                .price(new BigDecimal(5000))
                .quantity(2)
                .amount(new BigDecimal(10000))
                .build());
        list.add(ProductOfferEntity.builder()
                .productName("鼠标")
                .typeSpec("联想")
                .price(new BigDecimal(100))
                .quantity(3)
                .amount(new BigDecimal(300))
                .build());

        // 填充配置,开启组合填充换行
        FillConfig fillConfig = FillConfig.builder().forceNewRow(true).build();

        // 填充列表占位符
        writer.fill(list, fillConfig, sheet);

        //填充完成
        writer.finish();

    }

相关工具类:

EasyExcelUtil.java

java 复制代码
package com.mike.common.core.utils.excel;

import com.mike.common.core.constant.DateFormatConstant;
import com.mike.common.core.utils.StringUtils;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

public class EasyExcelUtil {

    /**
     * 初始化响应体
     * @param response 请求头
     * @param fileName 导出名称
     */
    public static void initResponse(HttpServletResponse response, String fileName) {
    	// 最终文件名:文件名_(截止yyyy-MM-dd)  --> 这块地方得根据你们自己项目做更改了
        String finalFileName = fileName + "_(截止"+ StringUtils.getNowTimeStr(DateFormatConstant.Y0M0D)+")";
        // 设置content---type 响应类型
        response.setContentType("application/vnd.ms-excel");
        response.setCharacterEncoding("utf-8");
        try {
            // 这里URLEncoder.encode可以防止中文乱码
            finalFileName = URLEncoder.encode(finalFileName, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        response.setHeader("Content-disposition", "attachment;filename=" + finalFileName + ".xlsx");
    }
}

测试:


四、我所遇到的问题

(1)驼峰命名导致填充数据失败

我在编写工资条案例时,最开始的模板如下:

导出效果:

数据都有,但是导出的表格中有些字段填充失败

这个我也不知道具体原因是为什么,不过测了几组数据之后,得出以下结论:

如果占位符中的变量采用的是驼峰命名的方式,且组成变量的 "单词" 存在过于简单的时就会出现该状况,比如说:

月薪的占位符 {.mSalary} 中的 mSalary 是由 msalary 组成,m 只有一个字母, {.mSalary} 填充不上,但是如果单独使用 {.m} 或者 {.salary} 却可以填充上去,或者把每个组成的单词写复杂一点,比如写成 {.mouthSalary} 也是可以填充上的

解决方案:要么就不要用驼峰命名,要么驼峰命名就写规范点,不要简写

(3)模板单元格样式消失

如果你使用模板导出发现有些单元格的样式消失了,比如:

这是因为在 write() 方法中添加了表格头的 class 对象

通过模板导出的方式不需要设置这个类对象,去掉即可

java 复制代码
        ExcelWriter writer = EasyExcel
                .write(out)
                .withTemplate(templateFile)
                .build();

参考文章:

easyexcel导出excel表格:https://blog.csdn.net/qq_57732418/article/details/136944211

SpringBoot集成阿里EasyExcel导出excel高级实战:https://blog.csdn.net/Blueeyedboy521/article/details/128257388

springboot 使用 EasyExcel 通过模板导出EXCEL 带多个动态列表:https://www.cnblogs.com/guanxiaohe/p/17719954.html

相关推荐
Themberfue几秒前
基础算法之双指针--Java实现(下)--LeetCode题解:有效三角形的个数-查找总价格为目标值的两个商品-三数之和-四数之和
java·开发语言·学习·算法·leetcode·双指针
深山夕照深秋雨mo9 分钟前
在Java中操作Redis
java·开发语言·redis
努力的布布15 分钟前
SpringMVC源码-AbstractHandlerMethodMapping处理器映射器将@Controller修饰类方法存储到处理器映射器
java·后端·spring
xujinwei_gingko15 分钟前
Spring MVC 常用注解
java·spring·mvc
PacosonSWJTU20 分钟前
spring揭秘25-springmvc03-其他组件(文件上传+拦截器+处理器适配器+异常统一处理)
java·后端·springmvc
PacosonSWJTU22 分钟前
spring揭秘26-springmvc06-springmvc注解驱动的web应用
java·spring·springmvc
原野心存1 小时前
java基础进阶——继承、多态、异常捕获(2)
java·java基础知识·java代码审计
进阶的架构师1 小时前
互联网Java工程师面试题及答案整理(2024年最新版)
java·开发语言
黄俊懿1 小时前
【深入理解SpringCloud微服务】手写实现各种限流算法——固定时间窗、滑动时间窗、令牌桶算法、漏桶算法
java·后端·算法·spring cloud·微服务·架构
木子02041 小时前
java高并发场景RabbitMQ的使用
java·开发语言