java根据模板生成docx文件的方法(原创)

第一次在CSDN发博,编辑器使用的不熟练,如大家感觉排版上有影响视觉的问题,欢迎私信或留言指出。

使用freemarker可以很方便的生成xml格式的doc文件,操作很简单,在word中写好需生成的文件,其中需替换的变量使用${}包裹,然后保存为xml格式,最后在java中开发代码读取数据并使用freemarker就可以生成需要的doc文件。然而,这个doc文件是xml格式的,部分手机上无法打开正常阅读。本文所要说的是如何生成在手机上可以查看的docx文件以及在其中增加图片。

继续刚才的操作,在word中保存为xml格式的同时保存为docx格式备用。使用文本编辑器打开xml文件,再使用网上的“xml格式化”工具将xml文件格式化。格式化后,查找其中的$符,可以看到,大多数的$都没有和{相邻,需要手工进行修改。修改为类似于这样的(避免无法被freemarker识别):

<w:r>
  <w:rPr>
    <w:rFonts w:asciiTheme="minorHAnsi" w:eastAsiaTheme="minorHAnsi" w:hAnsiTheme="minorHAnsi" w:hint="eastAsia"/>
  </w:rPr>
  <w:t>${dataabc}</w:t>
</w:r>

此时,xml文件依然可以使用word打开,但如果进行了下面的修改操作,word将无法打开此类文件,所以,如果今后需求发生变化需要修改模板时,以上步骤需要从头再来一次。

如果文件中有需要循环显示的内容(比如一个表格的行需要动态增加),需要手工修改xml文件的内容,在需要循环的内容前加入

<#list datas as onedata>

在结束位置加入

</#list>

最终效果类似于(这个例子是需要循环显示数据的表格。为减少篇幅,删除了一些不重要的内容,如需完整版请参考下载的附件):

<#list datas as onedata>
      <w:tr w:rsidR="00B925F3" w:rsidRPr="003E2683" w14:paraId="3CF1874A" w14:textId="77777777" w:rsidTr="00B925F3">
        <w:tc>
          <w:p w14:paraId="136DB865" w14:textId="478A8BA5" w:rsidR="00B925F3" w:rsidRPr="003E2683" w:rsidRDefault="00B925F3" w:rsidP="00DA2894">
            <w:r w:rsidRPr="003E2683">
              <w:rPr>
                <w:rFonts w:asciiTheme="minorHAnsi" w:eastAsiaTheme="minorHAnsi" w:hAnsiTheme="minorHAnsi" w:hint="eastAsia"/>
                <w:sz w:val="18"/>
                <w:szCs w:val="18"/>
              </w:rPr>
              <w:t>${onedata.no}</w:t>  <!-- no是代表list中的序号 -->
            </w:r>
          </w:p>
        </w:tc>
        <w:tc>
          <w:p w14:paraId="7347AE3D" w14:textId="77B282D9" w:rsidR="00B925F3" w:rsidRPr="003E2683" w:rsidRDefault="00B925F3" w:rsidP="00DA2894">
            <w:r w:rsidRPr="003E2683">
              <w:rPr>
                <w:rFonts w:asciiTheme="minorHAnsi" w:eastAsiaTheme="minorHAnsi" w:hAnsiTheme="minorHAnsi" w:hint="eastAsia"/>
                <w:sz w:val="18"/>
                <w:szCs w:val="18"/>
              </w:rPr>
              <w:t>${onedata.cell1}</w:t>
            </w:r>
          </w:p>
        </w:tc>
<!-- 省略若干<w:tc> </w:tc> -->
      </w:tr>
</#list>
<!-- list结束标记 -->

如果文件中有需要根据某变量是否为空而进行显示的内容,使用if进行判断(if标签内的内容即是变量parameter1不为空的时候所需要显示的内容):

<#if (parameter1)??> 
</#if>

在我的项目里,这个判断是用于是否显示图片,如果传入的parameter1不为空需要显示图片,否则,无需显示。If判断在xml中的具体位置请参考附件中的xml文件。

准备好了xml文件之后,使用freemarker先测试一下,看是否可以正确生成你所需要的word文件。这个方式生成的word文件,用文本编辑器是可以打开的,打开后的内容就是xml格式的。同时此文档也是word可以打开的(这是必须的)。

java使用freemarker生成word文件的核心代码如下所示(完整测试类请参考附件):

public String printTest(String Id) {  
//实际的代码可能会传入一个数据id,方法内进行查询,或者直接传入一个对象,方法内使用对象的属性
    Map<String,Object> dataMap = new HashMap<String, Object>(16);
    Map<String,Object> forMap = new HashMap<String, Object>(16);   //循环所需要的值
    List<Map<String,Object>> lstforMap = new ArrayList<Map<String,Object>>();
    Writer outWriter = null;
    File dataFile = null;
    String now = String.valueOf(System.currentTimeMillis());
    try {
        dataMap.put("param1", "data0");
        dataMap.put("param2", "data1");
        dataMap.put("param3", "data2");
        dataMap.put("param4", "yyyy年MM月dd日");
        //生成二维码图片,以一些数据的组合作为字符串进行二维码图片生成
        String[] qrcodeArray = {"test", "this is a test qrcode", "qrcode no chinese", "20202020"};
        String strQrcode = StringUtils.join(qrcodeArray, ",");
        String strQrcodebase64 = getCodeImageBase64(strQrcode, 200, 200);
        dataMap.put("qrcodestr", strQrcodebase64);
        GenerateImage(strQrcodebase64, "/app/" + File.separator + "abcd-" + now + ".png");          
        String[] lstArray = {"循环数据1", "循环数据2", "循环数据3", "循环数据4", "循环数据5"};
        //这个循环可以自行扩展,list里是对象,循环内取出放到不同的key值中
        for (String one : lstArray) {
            forMap = new HashMap<String, Object>(16);
            forMap.put("leadername", one);
            lstforMap.add(forMap);
        }
        dataMap.put("leader", lstforMap);
        //Configuration 用于读取ftl文件
        Configuration configuration = new Configuration(new Version("2.3.0"));
        configuration.setDefaultEncoding("utf-8"); 
        //根据某个类的相对路径指定
        configuration.setClassForTemplateLoading(this.getClass(), "");
        //输出文档路径及名称
        dataFile = new File("/app/" + File.separator + "abcd-" + now + ".doc"); 
        //以utf-8的编码读取xml文件
        Template template = configuration.getTemplate("test.xml", "utf-8");
        outWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(dataFile), "utf-8"), 10240);
        template.process(dataMap, outWriter);            
    } catch (Exception e) {
        e.printStackTrace();
        return null;
    } finally {
        System.gc();    //回收资源
        if (outWriter != null) {
            try {
                outWriter.close();
            } catch (IOException e) {
                e.printStackTrace();
            }                
        }
    }
    return "/app/" + File.separator + "abcd-" + now + ".doc";
}

模板文件的内容详见附件中的范例,文件路径:src\com\test\docx\create\test.xml

生成的结果类似于下图
测试生成的word文档

然而,手机上打开此文档(在手机微信中打开),会显示成这个样子
糟糕的移动端体验

以上的测试是为了确认xml文件可以正确的生成doc文件。
接下来把第一步生成的docx文件使用winrar软件打开,右键,打开方式,选择其他应用程序,找到winrar即可。
打开之后,进入word目录,将其中的document.xml拖出来,用文本编辑器打开,使用xml格式化网站将此文件的内容格式化。

然后再用文本编辑器打开,重复刚才的手工修改xml文件内容的操作(包括循环、根据变量值显示内容等),将此文件内容修改为你所需要的模板格式。也可以直接进行这个替换步骤,毕竟最终的word文件什么样子与这一步是直接关联的。

此处注意的是不能使用刚才的xml文件替换此文件的内容。虽然都是xml文件格式,但内容存在很大差异。

修改好之后,将新的xml文件仍然放到和实现类文件同一目录,将docx文件放到resource目录,然后就是编写代码。

简单说一下原理(百度到的,但很抱歉没及时记录出处),重点是图片文件的替换(原创)。

docx文件其实是一个zip压缩文件,为了达到生成docx文件的目标,将此文件中的xml文件及图片文件使用zip文件的操作方法做替换

所以,将一个正常编写的docx文件中的xml文件取出,修改为freemarker所需要的xml模板文件,程序使用此模板文件生成doc文件(xml格式)后,再把此文件放到docx(zip文件)的相应目录,再改名为docx扩展名的文件。

对于图片文件,也是使用程序生成png文件,然后放到docx文件中的相应目录(word\media目录下)。

在上面的代码下增加如下的代码进行测试(filename变量即是上面的代码的返回值)

String filename = "/app/" + File.separator + "abcd-" + now + ".doc";        
String strReturn = "";
try {
    // abcd.doc.xml 其实是一个改了扩展名的docx文件,就是最初保存的docx文件
    InputStream inputStream = this.getClass().getResourceAsStream("/" + "abcd.doc.xml") ;
    // 用这种方式可以找到jar包中的模板文件
    ZipInputStream zipInputStream = ZipUtil.wrapZipInputStream(inputStream);
    // 打开已有的zip文件(其实是一个word文件,docx格式)
    ZipOutputStream zipOutputStream = ZipUtil.wrapZipOutputStream(new FileOutputStream(new File(filename + "tmp")));
    // 新生成的zip文件(扩展名是doctmp)
    String itemname = "word/document.xml";
    // 需要替换的文件在zip文件中的位置及文件名
    ZipUtil.replaceItem(zipInputStream, zipOutputStream, itemname, new FileInputStream(new File(filename)));
    // 把新生成的文件放回去   
    File pngfile = new File(filename.replaceAll("doc", "png"));
    if (pngfile.exists()) {
        // 如果需要放入的二维码图片文件存在
        zipInputStream = ZipUtil.wrapZipInputStream(new FileInputStream(new File(filename + "tmp")));
        // 打开上面生成的zip文件(扩展名是doctmp)
        zipOutputStream = ZipUtil.wrapZipOutputStream(new FileOutputStream(new File(filename + "x")));
        // 最终输出的文件,扩展名是docx
        itemname = "word/media/image1.png";
        // 需要替换的文件在zip文件中的位置及文件名
        ZipUtil.replaceItem(zipInputStream, zipOutputStream, itemname, new FileInputStream(new File(filename.replaceAll("doc", "png"))));
        // 把图片文件放到zip包 ,目前这个是同一文件中相同的图片文件
    } else {
        // 如果没有二维码图片
        File tmpfile = new File(filename + "tmp");
        File docxfile = new File(filename + "x");
        tmpfile.renameTo(docxfile);
        // 把上面的临时文件改名为docx扩展名的文件,此文件即是最终返回的文件名
    }
    File tmpFile = new File(filename + "tmp");
    if (tmpFile.exists()) {
        tmpFile.delete();
    }
    if (pngfile.exists()) {
        pngfile.delete();
    }
    File tmpFile2 = new File(filename);
    if (tmpFile2.exists()) {
        tmpFile2.delete();
    }
    //删除临时文件    
    strReturn = filename + "x";
} catch (Exception e) {
    strReturn = "Exception: " + e.toString();
}
return strReturn;

执行之后,生成的word文件和第一次的并没有外观上的直接差异,但在手机上可以直接打开了。

本文遗留问题:
一个word文档中如果有多个不同的图片,未研究需要如何处理,有此需求的朋友们可以自行研究解决方案。

参考资料
FreeMarker 中文官方参考手册 (http://freemarker.foofun.cn/toc.html)
FreeMarker中if标签内的判断条件 (https://www.iteye.com/blog/lj6684-1594769)
Xml文档在线格式化(https://tool.oschina.net/codeformat/xml

演示项目下载地址:
csdn下载

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值