Java使用FreeMarker自动生成Word文档(带图片和表单)
Java使用FreeMarker自动生成Word文档(带图片和表单)
1 背景
近期工作中需要编写大量格式相同但数据不同的Word文档,因此研究了一下Java自动生成Word文档的技术。
Java自动生成Word文档的技术方案较多,本文使用的是Java + FreeMarker的方案,该方案分为两个步骤:创建FreeMarker格式的Word模板、FreeMarker根据模板生成Word文档。
2 目标效果
本文要使用Java + FreeMarker自动生成的Word文档的效果如下图所示:
3 创建Word模板
3.1 创建模板文档
使用Word创建一个名为WordTemplate.docx的文档,并按下图所示编辑模板文档的内容和样式
说明:
说明1:模板文档中${}是占位符,即生成Word文档时占位符会被真实的数据替换。例如${name}在生成文档时会被name这个属性的值替换 ,${userObj.name}在生成文档时会被userObj这个对象的name属性的值替换。
说明2:由于要在生成的Word文档中自动插入一张图片,因此,需要在模板文档中插入一张图片作为占位符,如上图所示。
3.2 转换模板文档
使用Word将模板文档另存为Word XML 文档(*.xml)格式,如下图所示:
3.3 处理模板文档中的占位符
转换后生成的是一个WordTemplate.xml模板文档,使用EditPlus等软件打开WordTemplate.xml文档(如下图),可以发现很不方便阅读,可以借助xml在线格式化工具(http://www.bejson.com/otherformat/xml/) 将WordTemplate.xml文档的内容进行格式化。
打开格式化后的WordTemplate.xml文档可以发现,Word在转换时会自动的将占位符分开(如下图1),因此需要把占位符之间多余的部分删除掉(如下图2)。每个占位符如果被分开了,就需要进行这样的处理,但是如果是一段文字被分开了,就不需要进行处理:
3.4 处理模板文档中的图片
模板文档在转换成xml格式时,图片的内容会被转换成很长的16进制的字符串,如下图所示:
将<pkg:binaryData></pkg:binaryData>标签中16进制字符串形式的图片内容替换成${userObj.photo}占位符(这里的userObj.photo是与Java程序中保持一致),替换后的效果如下图所示:
3.5 处理模板文档中的表单
在自动生成Word文档中的表单时,由于表头那一行只生成一次,而表单中的数据是循环生成的,因此,需要在xml格式的模板文档中表头那一行的后面添加如下的内容:
<#list userList as user>
并在对应的地方添加如下的内容:
</#list>
这里的userList是和Java程序中保存一致,而user是和xml模板文档中的占位符保持一致,处理后的效果如下图所示:
3.6 重命名模板文档
经过上述处理后,将WordTemplate.xml模板文档进行保存,并直接修改后缀名为ftl,即:WordTemplate.ftl。
至此,Word模板文档已经创建完成。
4 创建Java程序
4.1 版本说明
- Spring Boot: 2.1.13
- Freemarker: 2.3.28
- IDE: IDEA
- JDK: 1.8
4.2 创建项目
创建一个名为Word的Spring Boot项目,并添加maven依赖和相应的Java代码,最后的项目结构如下图所示:
pom.xml文件内容如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.office.word</groupId>
<artifactId>office-word</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Word</name>
<description>Java使用FreeMarker自动生成Word文档示例</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.28</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
User类程序如下所示:
package com.office.word.model;
/**
* 用户信息封装类
*/
public class User {
private String name;
private String sex;
private String photo;
private String birthday;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public String getPhoto() {
return photo;
}
public void setPhoto(String photo) {
this.photo = photo;
}
public String getBirthday() {
return birthday;
}
public void setBirthday(String birthday) {
this.birthday = birthday;
}
}
ImageUtil类程序如下所示:
package com.office.word.util;
import org.springframework.util.StringUtils;
import sun.misc.BASE64Encoder;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* 图片工具类
*/
public class ImageUtil {
/**
* 将图片内容转换成Base64编码的字符串
* @param imageFile 图片文件的全路径名称
* @return 转换成Base64编码的图片内容字符串
*/
public static String getImageBase64String(String imageFile) {
if (StringUtils.isEmpty(imageFile)) {
return "";
}
File file = new File(imageFile);
if (!file.exists()) {
return "";
}
InputStream is = null;
byte[] data = null;
try {
is = new FileInputStream(file);
data = new byte[is.available()];
is.read(data);
is.close();
} catch (IOException e) {
e.printStackTrace();
}
BASE64Encoder encoder = new BASE64Encoder();
return encoder.encode(data);
}
}
WordUtil类程序如下所示:
package com.office.word.util;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.Version;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.Map;
/**
* Word文档工具类
*/
public class WordUtil {
/**
* 使用FreeMarker自动生成Word文档
* @param dataMap 生成Word文档所需要的数据
* @param fileName 生成Word文档的全路径名称
*/
public static void generateWord(Map<String, Object> dataMap, String fileName) throws Exception {
// 设置FreeMarker的版本和编码格式
Configuration configuration = new Configuration(new Version("2.3.28"));
configuration.setDefaultEncoding("UTF-8");
// 设置FreeMarker生成Word文档所需要的模板的路径
configuration.setDirectoryForTemplateLoading(new File("E:/Word/Template/"));
// 设置FreeMarker生成Word文档所需要的模板
Template t = configuration.getTemplate("WordTemplate.ftl", "UTF-8");
// 创建一个Word文档的输出流
Writer out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File(fileName)), "UTF-8"));
//FreeMarker使用Word模板和数据生成Word文档
t.process(dataMap, out);
out.flush();
out.close();
}
}
WordApplication主启动类程序如下所示:
package com.office.word;
import com.office.word.model.User;
import com.office.word.util.ImageUtil;
import com.office.word.util.WordUtil;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Java使用FreeMarker生成Word文档主程序
*/
@SpringBootApplication
public class WordApplication {
public static void main(String[] args) throws Exception {
SpringApplication.run(WordApplication.class, args);
/**
* 自动生成Word文档
* 注意:生成的文档的后缀名需要为doc,而不能为docx,否则生成的Word文档会出错
*/
WordUtil.generateWord(getWordData(), "E:/Word/Document/User.doc");
}
/**
* 获取生成Word文档所需要的数据
*/
private static Map<String, Object> getWordData() {
/*
* 创建一个Map对象,将Word文档需要的数据都保存到该Map对象中
*/
Map<String, Object> dataMap = new HashMap<>();
/*
* 直接在map里保存一个用户的各项信息
* 该用户信息用于Word文档中FreeMarker普通文本处理
* 模板文档占位符${name}中的name即指定使用这里的name属性的值"用户1"替换
*/
dataMap.put("name", "用户1");
dataMap.put("sex", "男");
dataMap.put("birthday", "1991-01-01");
/**
* 将用户的各项信息封装成对象,然后将对象保存在map中,
* 该用户对象用于Word文档中FreeMarker表格和图片处理
* 模板文档占位符${userObj.name}中的userObj即指定使用这里的userObj属性的值(即user2对象)替换
*/
User user2 = new User();
user2.setName("用户2");
user2.setSex("女");
user2.setBirthday("1992-02-02");
// 使用FreeMarker在Word文档中生成图片时,需要将图片的内容转换成Base64编码的字符串
user2.setPhoto(ImageUtil.getImageBase64String("E:/Word/Images/photo.jpg"));
dataMap.put("userObj", user2);
/*
* 将多个用户对象封装成List集合,然后将集合保存在map中
* 该用户集合用于Word文档中FreeMarker表单处理
* 模板文档中使用<#list userList as user>循环遍历集合,即指定使用这里的userList属性的值(即userList集合)替换
*/
List<User> userList = new ArrayList<>();
User user3 = new User();
user3.setName("用户3");
user3.setSex("男");
user3.setBirthday("1993-03-03");
User user4 = new User();
user4.setName("用户4");
user4.setSex("女");
user4.setBirthday("1994-04-04");
userList.add(user3);
userList.add(user4);
dataMap.put("userList", userList);
return dataMap;
}
}
5 测试
5.1 准备
将要插入到Word文档中的图片(photo.jpg)复制到E:/Word/Images目录下,并将模板文件 WordTemplate.ftl 复制到E:/Word/Template目录下。
5.2 生成Word文档
直接运行WordApplication主启动类,程序运行成功后,即可以在E:/Word/Document目录下自动生成一个名为User.doc的文档,打开该文档,即为第2节中所示的目标效果。
6 踩坑
6.1 特殊符号
使用FreeMarker模板生成Word文档时,如果填充的数据字符串中含有特殊字符< 、>、&,那么生成的Word文档是无法打开的。因为这些字符在生成Word文档时被认为是FreeMarker模板的标签,如果这些字符不经过处理就直接用于生成Word文档,使用Word打开生成的文档就会报错,但以xml的方式打开,却会发现所有内容都是完整的,唯独上面三个特殊字符出问题。因此,在处理数据时需要对这三个特殊字符进行处理。
6.2 换行符
使用FreeMarker模板生成Word文档时,如果填充的数据字符串过长且当中使用"\n"进行换行,则生成的Word文档中并没有起到换行的作用。需要先将"\n"全部替换成"<w:p></w:p>",然后使用替换后的字符串数据生成Word文档,才能达到换行的效果。
6.3 内容是xml格式
使用本文方法生成的Word文档的内容实质上是xml格式的,因此,生成的Word文档即可以使用Word打开,也可以使用xml文档工具打开。如果使用Java程序去读取生成的Word文档的内容,则读出来的也是xml格式的内容。如果想要将内容转换成Word格式,则可以使用Word打开生成的文档然后另存为Word文档格式。
6.4 后缀是.doc
使用本文方法生成的Word文档的后缀必须是.doc格式,而不能是.docx格式,否则生成的Word文档无法打开。如果想要将后缀转换成.docx格式,则可以使用Word打开生成的文档然后另存为Word文档格式。
如果觉得本文对您有帮助,请关注博主的微信公众号,会经常分享一些Java和大数据方面的技术案例!