我是如何使用freemarker生成Word文件的?

推荐:亲身体验,数次踩坑,遂撰写此文,以备各位不时之需。

背景

一天,产品经理递给我了一份word报告,我定睛一看

我是如何使用freemarker生成Word文件的?
这个文档有大大小小的标题层级,还有排版好的段落、各种一目了然的饼图、走势图,当然还少不了颜色循环交替的报表。精致程度不亚于小明同学的学习报告。
我是如何使用freemarker生成Word文件的?

准备

鲁迅:身为一名Java程序员,任何时候都不要忘记站在巨人的肩膀上。

我是如何使用freemarker生成Word文件的?
通过某歌搜索关键词: java+word+导出,我立马得出了很多成熟的方案,通过横向、纵向比较,再结合本次报告样式比较多、用户可灵活选择不同模块导出的特点,最终,我决定使用 Freemarker 动态替换模版数据来导出word文档。至于导出文档的最终格式,有两种选择:
我是如何使用freemarker生成Word文件的?

那到底使用 doc还是 docx格式的文档?
每当人生当中每次面临选择我都很慎重。最终我选择使用 docx格式(原因文末会讲),但是为了让大家有更多的选择,满足更多的业务场景,借此机会,小明会给大家分别介绍使用 freemarker导出两种格式的word文档方式。

思路

FreeMarker是一个基于Java的模板引擎,最初专注于使用MVC软件架构生成动态网页。但是,它是一个通用的模板引擎,不依赖于servlets或HTTP或HTML,因此它通常还用于生成源代码,配置文件或电子邮件。

此时,我们用它动态生成xml文件,进而导出word文档。

我是如何使用freemarker生成Word文件的?
整体流程如下:
我是如何使用freemarker生成Word文件的?

准备

  • WPS

由金山软件股份有限公司发布,用于办公软件最常用的文字编辑、表格、演示稿等功能。

对,就是这个国产的办公软件。我也是第一次发现在导出文档这件事上,它如多年好友般友好。(word解析后的xml文件阅读性很强,一般人我不告诉他)

  • 开发工具(IDEA、Visual Studio Code等)
    你喜欢的,顺手的,就是最好的。

实现

集成Freemarker模版引擎

本次项目使用的框架依旧是Springboot,这个框架在集成各个组件表现都很便捷,不再赘述,这次集成Freemarker也不例外。

  • 首先我们在项目中增添依赖 spring-boot-starter-freemarker
    pom.xml文件如下所示:

    org.springframework.boot
    spring-boot-starter-freemarker

  • 按照默认约定,我们可以在resources下创建一个templates文件夹(查看FreeMarkerProperties源码可以发现默认目录就是这个),用于存放模版文档。
    我是如何使用freemarker生成Word文件的?
  • application.yml增加配置
spring:
  freemarker:
    template-loader-path: classpath:/templates
    cache: false # 开发环境缓存关闭
    suffix: xml
    charset: UTF-8

生成doc格式的文档

这里先拿使用freemarker导出doc格式的word文档举例。

  • 首先将docxTemplate.docx(调整好样式的模版文档)另存为WORD 2003 XML文档(*.xml)
    我是如何使用freemarker生成Word文件的?
    此处命名为docTemplete.xml,使用编辑工具首次打开时,会发现这个文档里面是压缩的xml,因此我们首先需要格式化一下。

注意:如果你使用的是 Visual Studio Code开发工具,一定要检查你所使用的xml格式化插件,是否会优化你的xml标签 。比如: <w:rpr></w:rpr>会变成 <rpr></rpr>。使用 Visual Studio Code的同学,oh my god ! 小明在这里推荐大家使用这个插件:XML Language Support by Red Hat

  • 现在,我们就使用freemarker语法编辑docTemplete.xml,比如使用占位符 ${}替换当前文档中的文本,以达到动态生成文本的目的,直接上代码。
public static Configuration getConfiguration(){
        //&#x521B;&#x5EFA;&#x914D;&#x7F6E;&#x5B9E;&#x4F8B;
        Configuration configuration = new Configuration(Configuration.VERSION_2_3_28);
        //&#x8BBE;&#x7F6E;&#x7F16;&#x7801;
        configuration.setDefaultEncoding("utf-8");
        configuration.setClassForTemplateLoading(WordUtil.class, "/templates");
        return configuration;
}

    /**
     * &#x751F;&#x6210;doc&#x6587;&#x4EF6;
     *
     * @param ftlFileName &#x6A21;&#x677F;ftl&#x6587;&#x4EF6;&#x7684;&#x540D;&#x79F0;
     * @param params      &#x52A8;&#x6001;&#x4F20;&#x5165;&#x7684;&#x6570;&#x636E;&#x53C2;&#x6570;
     * @param outFilePath &#x751F;&#x6210;&#x7684;&#x6700;&#x7EC8;doc&#x6587;&#x4EF6;&#x7684;&#x4FDD;&#x5B58;&#x5B8C;&#x6574;&#x8DEF;&#x5F84;
     */
    public void ftlToDoc(String ftlFileName, Map params, String outFilePath) {
        try {
            /** &#x52A0;&#x8F7D;&#x6A21;&#x677F;&#x6587;&#x4EF6; **/
            Template template = configuration.getTemplate(ftlFileName);
            /** &#x6307;&#x5B9A;&#x8F93;&#x51FA;word&#x6587;&#x4EF6;&#x7684;&#x8DEF;&#x5F84; **/
            File docFile = new File(outFilePath);
            FileOutputStream fos = new FileOutputStream(docFile);
            Writer bufferedWriter = new BufferedWriter(new OutputStreamWriter(fos, "utf-8"), 10240);
            template.process(params, bufferedWriter);
            if (bufferedWriter != null) {
                bufferedWriter.close();
            }
        } catch (TemplateException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

生成docx格式的文档

高能预警! 在成功使用Freemarker动态导出doc格式的文档之后,相信大家和我的心情一样非常激动。但以上操作只是一个小铺垫,接下来我们来看看如何实现docx格式的文档导出,小明相信一定会让各位看官大跌眼镜!不,大开眼界!
首先,告诉大家一个秘密: docx格式的文档其实是一个ZIP格式的压缩文件哦! 什么?你不信?验证如下:

  • windows的小伙伴
    将docx文档修改为ZIP格式(修改.docx后缀名为.zip),然后通过解压工具解压。
  • MacOS的小伙伴
    直接使用 unzip命令解压word文档,解压过后我们会发现该文档其实还有自己的 目录结构
    我是如何使用freemarker生成Word文件的?
    我是如何使用freemarker生成Word文件的?
    当然,这么多文件我们不必一一知悉,只需关注小明红线标注的文件和目录即可:
  • document.xml文件用于存放核心数据,文字,表格,图片引用等
  • media目录用于存放所有文档的图片
  • _rels目录下的document.xml.rels里存放的是配置信息,比如图片引用关系,即在document.xml中引用id对应media中的哪个图片。
  • 获取zip里的document.xml文档以及_rels文件夹下的document.xml.rels文档
  • 显而易见,如果我们要想根据数据动态导出不同的word文档,只需要:通过freemarker将本次数据填充到document.xml中,并将图片配置信息填充至document.xml.rels文档里,再用文件流把本次图片写入到media目录下替换已经存在的图片,最后把填充过内容的document.xml、document.xml.rels以及media用流的方式写入zip即可输出docx文档!上代码。

好吧,限于篇幅,代码见文末 Github地址

问题及解决方案

当然,大家在第一次尝试去干某一件事时,都不一定是一蹴而就的。就比如在导出word时,就可能会遇到以下问题。​

特殊字符

问题:有些文本数据中难免含有特殊字符,如: < > @ ! $ & 等等。

解决方案:这些特殊字符如果不进行转义,就会引起word打不开的现象,比如表格中的超链接的 &符号,就需要替换为 &,如果你的文档用office打开时提示文件损坏,九成是因为特殊符号引起的,我们可以打开documet.xml定位报错位置;当然还有终极方案,我们可以利用Freemarker的语法直接在模板中使用 <!--[CDATA[ ]]--> 处理。比如:

 <w:t><!--[CDATA[ ${article.title} ]]--></w:t>

图片变形

问题:因为echarts生成的图表是响应式的,不同的屏幕大小、分辨率,会造成每次前端传过来的图片宽高比例不一致,如果还直接将图片按照之前的比例放进文档,会造成生成后文档中的图片变形。

思路:首先将文档中的图片设置为原图,然后锁定宽高比,将图片调整到合适大小,解压文档从 document.xml,得到此时word中该图片宽高对应的值,如下所示:

我是如何使用freemarker生成Word文件的?

要想保证不同像素比例的宽高在文档中不变形,我们需要固定 cy的值,然后根据固定比例动态求得当前像素比例图片在word中代表的宽 cx的值。计算方法如下所示:
公式:

a/b = x/y

其中,a表示图片在word中宽的数值,b代表图片在word中高的数值,x表示前端传过来图片的宽(单位:像素),y表示前端传过来图片的高(单位:像素)。因此,已知b、x、y,根据公式,我们即可求出a;

我就是文末

当然,还有用一些其他注意事项:

  • 如果word中的模块比较多的话,使用Freemarker语法要仔细一点;
  • 为什么小明最终选择导出docx格式的文档呢?(还不是因为产品经理的需求嘛)因为doc格式的文档,小明尝试导出后,发现该文档并不是一个合法的doc文档,体现在:不能在手机上(微信、钉钉)正常预览,office提示以xml形式打开等。因此在导出doc文档时,通过Freemaker填充document.xml后得到的并不是一个合法的word文档,查了相关资料,还需要借助第三方工具进行签名,而签名还需要在windows系统下才能完成,但是我们平时用的生产环境都是Linux……因此,考虑再三,再三权衡,最终选择导出docx格式的文档。这种方式再适合不过,而且还能保证在当前主流APP上都能正常预览。
  • 敲黑板!导出docx文档最重要的一个思想是将本次数据写入覆盖模版文件(在商业中,相当于借壳上市),重新输出一个zip格式压缩的文件,这个文件就是我们最终想要的文档。

以上,就是小明word导出的前前后后,如果你也曾经遇到过或者现在正好遇到word文档导出开发的问题,欢迎一起讨论交流。

相关链接

我上传了工具类,包含doc、docx 的导出,以及导出word文档时特殊符号转义,还有图片Base64转换成文件输出的方法。
GitHub地址:https://github.com/WhenCoding/coder-xiaoming/blob/master/src/main/java/com/xm/coder/util/WordUtil.java

本文可转载,但需声明原文出处。 程序员小明,一个很少加班的程序员。欢迎关注微信公众号,获取更多优质文章。

Original: https://www.cnblogs.com/coderxx/p/13666246.html
Author: Coder小明
Title: 我是如何使用freemarker生成Word文件的?

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/610821/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • Oracle在操作系统层面kill所有session的语句

    原理,通过kill连接对应的进程来kill所有session。执行命令如下: ps -ef | grep LOCAL=NO | grep -v grep | awk ‘…

    Java 2023年5月30日
    057
  • 斐波那契数列(Fibonacci)递归和非递归实现

    序列前9项为:0, 1, 1, 2, 3, 5, 8, 13, 21 要注意非递归的话就是那一个变量帮助存储当前下一项的值,然后依次挪动两个指针往下即可 注意如果n太大 会溢出 O…

    Java 2023年5月30日
    090
  • Prometheus 安装

    官方文档 https://prometheus.io/docs/introduction/first_steps/ 中文文档 https://prometheus.fuckclou…

    Java 2023年6月9日
    091
  • Mybatis-Plus的使用

    1.什么是Mybatis-Plus 2.为什么要学习Mybatis-Plus 3.入门示例 3.1 说明 3.2 准备工作 3.3 配置步骤 4.常用配置 4.1 实体类全局配置 …

    Java 2023年6月7日
    070
  • SpringBoot 增删改查+文件上传

    注:本文所用到的版本 MySql 8.0.28 SpringBoot 2.7.2 准备工作 :建表 、pom.xml导入依赖 、application.yml 配置 建表 CREA…

    Java 2023年6月8日
    073
  • 记一次排查线上MySQL死锁过程,不能只会curd,还要知道加锁原理

    昨晚我正在床上睡得着着的,突然来了一条短信。 啥,线上MySQL死锁了,我赶紧登录线上系统,查看业务日志。 能清楚看到是这条insert语句发生了死锁。 MySQL如果检测到两个事…

    Java 2023年6月8日
    080
  • 如何使用Arthas定位问题

    在我们日常的工作中,经常会遇到一些线上才会遇到的问题。Arthas无疑是我们在工作中,定位线上问题的神奇。下面,我将介绍一下我们在工作中经常用到的一些功能。 dashboard 首…

    Java 2023年6月7日
    065
  • HashMap源码及原理详解

    HashMap概要 基本属性 Node节点源码 TreeNode节点源码 容量初始化 容量为2的幂次方的好处 哈希的计算 哈希冲突 put – 添加元素 get &#8…

    Java 2023年6月9日
    082
  • 浏览器地址栏从输入地址到页面展示都发生了什么

    1 URL输入 2 DNS解析 浏览器会首先从缓存中找是否存在域名,如果存在就直接取出对应的ip地址, 其中会请求 第二步,本地系统缓存 如果浏览器缓存没找到,就检查本地操作系统的…

    Java 2023年6月6日
    082
  • Java 面向对象概念

    面向对象概念 面向对象 OOP[Object-oriented programming] 是一种编程模式,面向对象是围绕数据或对象来组织软件设计的,而不是功能和逻辑。 面向对象是一…

    Java 2023年6月5日
    082
  • 《Java编程思想》读书笔记(二)

    三年之前就买了《Java编程思想》这本书,但是到现在为止都还没有好好看过这本书,这次希望能够坚持通读完整本书并整理好自己的读书笔记,上一篇文章是记录的第一章到第十章的内容,这一次记…

    Java 2023年6月5日
    0108
  • Skywalking-01:Skywalking介绍

    Skywalking介绍 Application performance monitor tool for distributed systems, especially desi…

    Java 2023年6月5日
    071
  • springboot 集成swagger2(转)

    原文:https://www.cnblogs.com/kingsonfu/p/11519728.html 使用Swagger 可以动态生成Api接口文档,在项目开发过程中可以帮助前…

    Java 2023年5月30日
    080
  • IO流–创建文件夹,复制移动文件

    创建多级文件夹 final String ROOTPATH = "/Users/mac/Downloads"; // 默认文件下载的位置 @Test //创建多…

    Java 2023年6月6日
    083
  • 求解马走棋问题C/C++

    在 m 行 n 列的棋盘上有一个中国象棋中的马,马走日字且只能向右走。 请找到可行路径的条数,使得马从棋盘的左下角 (1,1)(1 ,1 ) 走到右上角 (m,n)(m ,n )。…

    Java 2023年6月5日
    0119
  • SpringCloud之RocketMQ

    1.RocketMQ 与其他区别 2.名词之间关系、架构及图 2.1 架构图 2.2 消息模型(Message Model) (1)图 (2)组成 a.主要由 Producer、B…

    Java 2023年6月13日
    075
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球