使用Freemarker填充模板导出复杂Excel,其实很简单哒!

1. 需求分析

类似这样的一个表格

在这里插入图片描述

我们需要从数据库中查询对应的数据,将其汇总进该表格,并且可能还需要复制表格项,我这个案例中没有,只是一个动态列表,这时候我们可以分如下几个步骤。
在这里插入图片描述

我们以面向对象的思想来看, 编号一行 其实是一个列表,而剩余部分,则是表格的主题,这样构建对象的对象组成就是对象中包含一些属性,一个列表,其中一个列表再包含一些属性。

之前我对于这种固定模板,再套动态列表的导出,其实是有点懵的,但工作中碰到了,就要迎难而上,这篇文章就是来做一个梳理总结。

2. 对象生成

生成的对象如下所示

Invoice类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Invoice {
    // 客户名称
    private String customerName;

    // 客户编码
    private String customerCode;

    // 日期
    private Date date;

    // 总合计大写
    private String totalAmountInWords;

    // 合计
    private BigDecimal totalAmount;

    // 主管
    private String supervisor;

    // 财务
    private String finance;

    // 保管员
    private String custodian;

    // 经手人
    private String handler;

    List<InvoiceItem> invoiceItemList;
}

InvoiceItem类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class InvoiceItem {
    // 编号
    private String itemNumber;

    // 名称及规格
    private String itemNameAndSpecs;

    // 单位
    private String unit;

    // 数量
    private int quantity;

    // 单价
    private BigDecimal unitPrice;

    // 金额
    private BigDecimal amount;

    // 备注
    private String comments;

}

3. 列表插值

然后先不要将表格转化为xml格式,我们可以先写freemarker表达式,在空位插值,否则等xml已转化,就很不直观了。
插值如下。

在这里插入图片描述
可以看到我把中间的空行删了,因为我们会遍历列表,所以那些空行是没有用的。

重要的事!此刻先确定好表格的样式,否则等你转换为ftl,写好循环什么的,再改样式将会变得很困难,特别是更复杂的表格。

4. 另存xml+格式化

接着将对应的表格另存为xml。

用文本打开会发现是这样的,去对应网站进行格式化。
在这里插入图片描述

xml格式化

重点!
格式化后一定要再打开文件看一下样式!
格式化后一定要再打开文件看一下样式!
格式化后一定要再打开文件看一下样式!
我不知道是格式化网站的问题还是什么,格式化后的样式竟然会变,上面的这个就很靠谱。

5. ftl修改

我们可以将格式化后的代码复制到该文件 /templates/demo.ftl 你们随意命名即可。

我们找到worksheet这一行,下面的就是我们要更改的地方了。

在这里插入图片描述
因为我们只需要遍历列表即可,所以只需要在编号填充的上面引入列表即可,不要忘了在对应的地方结束循环。

在这里插入图片描述

6. 程序转化

service代码如下


@Service
@Slf4j
public class ExcelService {


    /**
     * 1.查询对象
     * 2.封装map
     * 3.指定模板与导出文件名称
     * 4.响应设置 不设置会导致乱码 文件格式出错等问题
     * 5.模板配置
     * 6.变量替换
     * 7.文件输出
     * @param id 商品id
     */
    public void export(Integer id, HttpServletResponse response) {
        //这里假装查询对应的对象 我处于方便直接让ai生成了
        Invoice invoice = getById(1);
        Map<String, Object> model = new HashMap<>();
        //要与ftl中的对象一致
        model.put("invoice", invoice);
        // 指定FreeMarker模板文件的位置
        String templateFilePath = "demo.ftl";
        String excelName = "商品出库单" + ".xlsx";


        FileInputStream inputStream = null;
        ServletOutputStream outputStream = null;
        File file = null;
        // 使用FreeMarker加载模板文件
        try {
            response.reset();
            response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(excelName, "UTF8"));
            response.setCharacterEncoding("utf-8");
            response.setContentType("application/vnd.ms-excel;charset=utf-8");

            Configuration cfg = new Configuration(Configuration.getVersion());
            // 指定FreeMarker模板文件的位置
            String path = "/templates/";
            cfg.setClassForTemplateLoading(getClass(), path);
            Template template = cfg.getTemplate(templateFilePath);


            file = new File(excelName);
            FileWriter out = new FileWriter(excelName);
            // 变量替换
            template.process(model, out);


            // 将文件输出到response,返回给客户端
            inputStream = new FileInputStream(file);
            byte[] buffer = new byte[inputStream.available()];
            inputStream.read(buffer);
            inputStream.close();

            outputStream = response.getOutputStream();
            outputStream.write(buffer);
            outputStream.flush();
            outputStream.close();

        } catch (Exception e) {
            log.error("出库单导出失败", e);
            throw new RuntimeException(e);
        }
    }




    public static Invoice getById(Integer id) {
        // 创建一个 Invoice 对象
        Invoice invoice = new Invoice();
        invoice.setCustomerName("客户名称示例");
        invoice.setCustomerCode("客户编码示例");
        invoice.setDate(new Date());
        invoice.setTotalAmountInWords("总合计大写示例");
        invoice.setTotalAmount(new BigDecimal("1000.00"));
        invoice.setSupervisor("主管示例");
        invoice.setFinance("财务示例");
        invoice.setCustodian("保管员示例");
        invoice.setHandler("经手人示例");

        // 创建并填充 5 个 InvoiceItem 对象,然后添加到 Invoice 的列表中
        List<InvoiceItem> invoiceItems = new ArrayList<>();
        for (int i = 1; i <= 5; i++) {
            InvoiceItem item = new InvoiceItem();
            item.setItemNumber("编 号 " + i);
            item.setItemNameAndSpecs("名称及规格示例 " + i);
            item.setUnit("单位示例");
            item.setQuantity(10 * i);
            item.setUnitPrice(new BigDecimal("10.00"));
            item.setAmount(new BigDecimal("100.00"));
            item.setComments("备注示例 " + i);
            invoiceItems.add(item);
        }
        invoice.setInvoiceItemList(invoiceItems);

        return invoice;
    }

}

controller 调用

@RestController
public class ExportController {

    @Autowired
    private ExcelService excelService;


    @GetMapping("/export")
    public void export(Integer id, HttpServletResponse response) {
        excelService.export(1, response);
    }
}

生成的表格如下所示,可以看到,效果还不错的。

在这里插入图片描述

7. 犯的错误

但是!!!!!

你以为这就完了吗? 我之前做的表格比这复杂很多,导致遇到了很多错误。

比如文件路径错误,模板错误excel格式不支持错误。样式不整齐,乱码,读出错误。

现在一一列举下我犯的错,所以这不单单是一个教学贴,也是对我错误的一个总结。

1.文件路径错误。

在写这段代码的时候,由于路径藏的很深,且我以为第二个参数不能直接这么用,要填写全路径,导致文件一致读取失败,现在明白了,只需要直接填充根路径往下的内容即可。

 // 指定FreeMarker模板文件的位置
            String path = "/templates/";
            cfg.setClassForTemplateLoading(getClass(), path);

2.对异常的不关注。

之前读取文件失败,还有个原因是ftl语法错误,但我打的异常并不明显,导致我一直没看到这个错误,计算把文件路径修复了,还是会报错,其实是找错了方向,提醒大家要注意异常,当时是日志太多了,确实忽略了这一点。

3.excel文件转化打不开。

其中一点,我们打开之前的xml文件,找到这两个字段
在这里插入图片描述
建议将这两个值改的大一点,否则文件格式会出问题,我这里是因为数据量少没出问题,但真实使用就不一定了,改个几百几千都可以。

还有一点,之前xml进行了格式化,但到了idea中,头文件内容错乱了,这点才导致文件无法打开,建议从之前的xml模板中复制头文件,在原基础上修改即可。

4.乱码

特别是中文命名的文件,一定要设置utf8格式,之前的response的设置可不是白加的。

5.freemarker空值问题
一旦某一个地方,比如日期,列表之类的为空,整个渲染就报错了,所以一定要提前判断某些地方会不会为null,提前设置默认值,一般来讲在表达式后面加!即可,当然只针对于字符串什么的。

6.遇到bug时的盲目

那时因为需求急,就算碰到了问题,也是一个劲的百度,cv大法,其实那时候静下心来,稍微放松一下,理清思路,也许很快就能解决问题,但知易行难,真正做起来才发现很有难度。

8. 总结

虽然我以前诟病freemarker很垃圾,只要一个语法出问题,就生成不了html页面了,但相比于用poi将复杂的excel导出,freemarker其实更好用,并且复杂的excel其实就是在原基础上做了更多的填充,字段复制罢了,原理不复杂,但对于没做过的人来说,可能会稍微有点恐惧,我的案例告诉你,只要咬牙迈过去就好了。

一个人最大的幸福莫过于在人生的中途、富有创造力的壮年,发现自己此生的使命。

在 Spring Boot 中集成 Freemarker 导出 Excel,可以通过以下步骤实现: 1. 首先,需要在 Spring Boot 中添加 Freemarker 和 Apache POI 依赖: ```xml <dependencies> <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>${freemarker.version}</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>${apache.poi.version}</version> </dependency> </dependencies> ``` 2. 创建 Excel 模板文件,可以使用 Freemarker模板语法来定义表头和数据内容,同时在模板中可以使用 `img` 标签来引用图片,例如: ```html <html> <head> <title>Excel Template</title> </head> <body> <table> <thead> <tr> <th>Name</th> <th>Age</th> <th>Photo</th> </tr> </thead> <tbody> <#list users as user> <tr> <td>${user.name}</td> <td>${user.age}</td> <td><img src="${user.photo}" /></td> </tr> </#list> </tbody> </table> </body> </html> ``` 其中,`users` 是一个包含数据的列表,每个元素是一个包含 `name`、`age` 和 `photo` 属性的对象,`photo` 属性是图片的 URL。 3. 在 Spring Boot 中定义一个控制器,用于处理导出 Excel 的请求: ```java @Controller public class ExcelController { @Autowired private Configuration freemarkerConfig; @GetMapping("/export") public void exportExcel(HttpServletResponse response) throws Exception { // 读取 Excel 模板文件 Template template = freemarkerConfig.getTemplate("excel-template.ftl"); // 准备数据 List<User> users = prepareData(); // 创建 Excel 工作簿 Workbook workbook = new XSSFWorkbook(); // 渲染模板生成 Excel 文件 Map<String, Object> model = new HashMap<>(); model.put("users", users); StringWriter out = new StringWriter(); template.process(model, out); InputStream is = new ByteArrayInputStream(out.toString().getBytes("UTF-8")); workbook = WorkbookFactory.create(is); // 设置响应头,告诉浏览器文件类型是 Excel response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-disposition", "attachment; filename=users.xlsx"); // 将 Excel 文件写入响应流中 OutputStream os = response.getOutputStream(); workbook.write(os); os.flush(); os.close(); } private List<User> prepareData() { // TODO: 从数据库或其他来源读取数据 return Arrays.asList( new User("Alice", 25, "https://example.com/alice.jpg"), new User("Bob", 30, "https://example.com/bob.jpg"), new User("Charlie", 20, "https://example.com/charlie.jpg") ); } private static class User { private String name; private int age; private String photo; public User(String name, int age, String photo) { this.name = name; this.age = age; this.photo = photo; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getPhoto() { return photo; } public void setPhoto(String photo) { this.photo = photo; } } } ``` 在该控制器中,我们注入了 `Configuration` 类,它是 Freemarker 的配置类,用于加载 Excel 模板文件。在 `exportExcel` 方法中,我们先准备数据,然后通过 `template.process` 方法渲染模板生成 Excel 文件。最后将 Excel 文件写入响应流中,浏览器会自动下载该文件。 4. 如果 Excel 模板中包含图片,那么需要在渲染模板之前,将图片下载到本地,然后将图片的本地路径传递给模板。例如: ```java private String downloadImage(String imageUrl) throws Exception { URL url = new URL(imageUrl); String fileName = url.getFile(); String filePath = "images/" + fileName.substring(fileName.lastIndexOf("/") + 1); FileUtils.copyURLToFile(url, new File(filePath)); return filePath; } @GetMapping("/export") public void exportExcel(HttpServletResponse response) throws Exception { // 读取 Excel 模板文件 Template template = freemarkerConfig.getTemplate("excel-template.ftl"); // 准备数据 List<User> users = prepareData(); // 下载图片并将本地路径传递给模板 for (User user : users) { String photoPath = downloadImage(user.getPhoto()); user.setPhoto(photoPath); } // 创建 Excel 工作簿 Workbook workbook = new XSSFWorkbook(); // 渲染模板生成 Excel 文件 Map<String, Object> model = new HashMap<>(); model.put("users", users); StringWriter out = new StringWriter(); template.process(model, out); InputStream is = new ByteArrayInputStream(out.toString().getBytes("UTF-8")); workbook = WorkbookFactory.create(is); // 设置响应头,告诉浏览器文件类型是 Excel response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-disposition", "attachment; filename=users.xlsx"); // 将 Excel 文件写入响应流中 OutputStream os = response.getOutputStream(); workbook.write(os); os.flush(); os.close(); } ``` 在该示例中,我们定义了一个 `downloadImage` 方法,用于下载图片,并将图片保存到 `images` 目录下。然后在 `exportExcel` 方法中,遍历用户列表,调用 `downloadImage` 方法下载每个用户的图片,并将本地路径传递给模板。注意,模板使用的图片路径应该和下载到本地的路径一致。
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值