一、学习背景 Freemarker
是一款强大的模板引擎,可以用来生成网页、邮件、文档等。对于简单的 Word
文档导出,只需要手动编写 ftl
文件即可。但如果要导出复杂的文档,比如带有复杂样式、页眉页脚、内嵌图片、批注等,手动编写模板就行不通了。现在提出一个从目标文档出发的解决方案:先将目标 Word
模板文档转换为 xml
文档,然后将 xml
文档转换为 ftl
文档,手动替换模板中的变量之后即可导出复杂 Word
。
二、根据目标文档获取 ftl
文档 我们以导出房屋租赁合同文档为例,模板中有房东、租客信息、房屋信息等。
1. 将目标模板转换为 xml
文档 操作 Word
文档,点击【文件】,另存为 xml
文档。
用 NotePad++
或 Sublime
打开 xml
文档,内容缺乏层次感,这里需要格式化一下。
2. 将 xml
文档转换为 ftl
文档 格式化之后的 xml
文档,选择【文件】,另存为 ftl
文档。接下来需要手动替换模板参数。
文本参数: 根据模板中的默认值,找到其所在位置,直接替换。
图片参数: 图片参数是对图片进行 Base64
加密之后的值,加密操作可以由 Java
来完成。
三、使用 Java
根据 ftl
模板导出 Word
文档 在 Resource
目录下新建文件夹 freemarker_template
,将 ftl
文档粘贴进去。
图片 Base64
位编码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import com.company.exception.ServiceException;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Service;import sun.misc.BASE64Encoder;import java.io.File;import java.io.FileInputStream;import java.io.IOException;import java.io.InputStream;@Service public class ImageServiceImpl implements ImageService { private static final Logger LOGGER = LoggerFactory.getLogger(ImageServiceImpl.class ) ; @Override public String getImgBase64Data (String fileSrc) { File img = new File(fileSrc); if (!img.exists()) { return null ; } try (InputStream in = new FileInputStream(img)) { byte [] data = new byte [in.available()]; in.read(data); BASE64Encoder encoder = new BASE64Encoder(); return encoder.encode(data); } catch (IOException e) { LOGGER.error("invoke ImageService.getImgBase64Data error: {}" , e.getMessage(), e); throw new ServiceException(e.getMessage(), e); } } }
解析模板内容实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 import com.company.exception.ServiceException;import freemarker.template.Configuration;import freemarker.template.Template;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Service;import org.springframework.util.Assert;import java.io.File;import java.io.StringWriter;import java.nio.charset.StandardCharsets;import java.util.Map;@Service public class TemplateServiceImpl implements TemplateService { private static final Logger LOGGER = LoggerFactory.getLogger(TemplateServiceImpl.class ) ; @Override public String getTemplateContent (String templatePath, String templateName, Map<String, Object> params) { try { LOGGER.info("start building template content. path: 【{}】; name: 【{}】; params: 【{}】" , templatePath, templateName, params); Assert.hasText(templatePath, "template path cannot be null or empty" ); Assert.hasText(templateName, "template name cannot be null or empty" ); String resourcePath = TemplateServiceImpl.class .getResource (File .separator ).getPath () ; Configuration configuration = new Configuration(Configuration.VERSION_2_3_28); configuration.setDefaultEncoding(StandardCharsets.UTF_8.name()); String standardTemplatePath = templatePath.endsWith(File.separator) ? templatePath.concat(File.separator) : templatePath; configuration.setDirectoryForTemplateLoading(new File(resourcePath.concat(standardTemplatePath))); Template template = configuration.getTemplate(templateName); StringWriter writer = new StringWriter(); template.process(params, writer); String content = writer.toString(); LOGGER.info("finish building template content." ); return content; } catch (Exception e) { LOGGER.error("invoke TemplateService.getStringFromVm error: {}" , e.getMessage(), e); throw new ServiceException(e.getMessage(), e); } } }
单元测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 import ImageService;import TemplateService;import com.fasterxml.jackson.databind.ObjectMapper;import org.apache.commons.io.IOUtils;import org.junit.Test;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import javax.annotation.Resource;import java.io.File;import java.io.FileOutputStream;import java.io.InputStream;import java.io.OutputStream;import java.nio.charset.StandardCharsets;import java.util.Map;import org.junit.runner.RunWith;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.test.context.junit4.SpringRunner;@RunWith (SpringRunner.class ) @SpringBootTest public class TemplateTest { private static final Logger LOGGER = LoggerFactory.getLogger(TemplateTest.class ) ; @Resource private TemplateService templateService; @Resource private ImageService imageService; @Test public void generateWordFromTemplate () { String templatePath = "freemarker_template/" ; String templateName = "contract.ftl" ; ContractInfo contractInfo = new ContractInfo(); contractInfo.setLandlordName("地头蛇" ); contractInfo.setLandlordIdNo("100011232132112" ); contractInfo.setLandlordAddress("上海市青浦区" ); contractInfo.setLandlordPhoneNo("13032389090" ); contractInfo.setTenantName("打工人" ); contractInfo.setTenantIdNo("340323199901013217" ); contractInfo.setTenantAddress("安徽省蚌埠市" ); contractInfo.setTenantPhoneNo("15656997878" ); contractInfo.setYear("2020" ); contractInfo.setMonth("01" ); contractInfo.setDay("01" ); String imgBase64Data = imageService.getImgBase64Data("C:\\house.jpg" ); contractInfo.setImgBase64Data(imgBase64Data); ObjectMapper objectMapper = new ObjectMapper(); Map<String, Object> params = objectMapper.convertValue(contractInfo, Map.class ) ; String content = templateService.getTemplateContent(templatePath, templateName, params); File file = new File("租房合同-打工人.doc" ); try (InputStream in = IOUtils.toInputStream(content, StandardCharsets.UTF_8); OutputStream out = new FileOutputStream(file)) { byte [] data = new byte [1024 ]; int len; while (-1 != (len = in.read(data, 0 , data.length))) { out.write(data, 0 , len); } out.flush(); } catch (Exception e) { LOGGER.error("下载租房合同失败; errMsg: {}" , e.getMessage(), e); } } }
注意: 通过这种方式导出的 Word
文档,本质上还是 xml
文档,因此必须使用 .doc
后缀,具体请查看MsOffice Word docx 研究 。
运行起来,导出租房合同-打工人.doc
。
四、总结 通过将目标模板转换为 ftl
文档,再解析得到目标文档的办法,理论上可以应对任何复杂程度的文档导出需求。但这种好办法也有弊端:ftl
文档包含太多的内联样式、复杂标签等,可读性太差。当模板发生变化时,手动替换太多的模板参数将会是一种灾难。
参考: