java生成word文档
最近得到一个需求:按用户提供的模板生成分析报告,并让用户可以在网页上导出。这个功能以前没做过,但是好像听说过freemarker。于是乎,开始了我的百度之旅。
一、word文档的本质
我也是最近才知道,word文档的本质原来是一个压缩文件。不信你看,将.docx文件修改文件后缀为.zip
然后解压缩得到了这些文件,这些就是组成word文档的所有文件。其中word文件夹下是主要内容
其中,document.xml中是关于文档内容的设置,相当于网页里面的html文件一样。_rels文件夹下的document.xml.rels文件是图片配置信息。media文件夹下是文档中所有图片的文件,其他的应该是类似于网页里面的CSS文件,设置样式的。所以document.xml就是我们要修改的了。这样的操作就相当于网页已经编写好了,只差从后台传送数据到前端展示了。
二、创建freemarker模板
freemarker是一个模板引擎,百度是这样介绍的:
FreeMarker是一款模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页、电子邮件、配置文件、源代码等)的通用工具。 它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。
说白了就是跟前端页面的变量绑定是一样的,用过vuejs的都知道,前端使用“{{name}}”双括号括起来的变量可以通过传值改变页面上数据。freemarker也是这样,通过在document.xml中使用“${name}”dollar符大括号括起来的变量也可以通过传值改变模板文件内容。清楚了这点就好办了。
将document.xml文件放到IDEA项目中的templates文件夹下,然后按Ctrl+Alt+L键格式化xml内容,将需要动态修改的地方用“${}”括起来,如下
有的时候变量会被拆分成两个,就要麻烦点把两个中间的多余部分全都删掉,然后在用符号括起来。这点相信大家都能理解。
以上模板创建就结束了,是不是很简单。
三、代码实现
模板搞定了,怎么根据模板生成文档呢?关键步骤来了。
1.首先我们要将模板中的变量赋值,生成新的文件。
2.将生成的文件写入压缩文件。上面已经说了,word文档本质就是压缩文件。
3.将.docx文档的其他内容也写入压缩文件。
4.将压缩文件写入word文档,这就是最后生成的文档。
导入依赖:
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.28</version>
</dependency>
主要用到下面的工具类:
public class FreeMarkUtils {
private static Logger logger = LoggerFactory.getLogger(FreeMarkUtils.class);
public static Configuration getConfiguration(){
//创建配置实例
Configuration configuration = new Configuration(Configuration.VERSION_2_3_28);
//设置编码
configuration.setDefaultEncoding("utf-8");
configuration.setClassForTemplateLoading(FreeMarkUtils.class, "/templates");//此处设置模板存放文件夹名称
return configuration;
}
/**
* 获取模板字符串输入流
* @param dataMap 参数,键为绑定变量名,值为变量值
* @param templateName 模板名称
* @return
*/
public static ByteArrayInputStream getFreemarkerContentInputStream(Map dataMap, String templateName) {
ByteArrayInputStream in = null;
try {
//获取模板
Template template = getConfiguration().getTemplate(templateName);
StringWriter swriter = new StringWriter();
//生成文件
template.process(dataMap, swriter);
in = new ByteArrayInputStream(swriter.toString().getBytes("utf-8"));//这里一定要设置utf-8编码 否则导出的word中中文会是乱码
} catch (Exception e) {
e.printStackTrace();
logger.error("模板生成错误!");
}
return in;
}
//outputStream 输出流可以自己定义 浏览器或者文件输出流
public static void createDocx(Map dataMap, OutputStream outputStream) {
ZipOutputStream zipout = null;
try {
//内容模板,传值生成新的文件输入流documentInput
ByteArrayInputStream documentInput = FreeMarkUtils.getFreemarkerContentInputStream(dataMap, "document.xml");
//最初设计的模板,原word文件生成File对象
File docxFile = new File(WordUtils.class.getClassLoader().getResource("templates/demo.docx").getPath());//模板文件名称
if (!docxFile.exists()) {
docxFile.createNewFile();
}
ZipFile zipFile = new ZipFile(docxFile);//获取原word文件的zip文件对象,相当于解压缩了word文件
Enumeration<? extends ZipEntry> zipEntrys = zipFile.entries();//获取压缩文件内部所有内容
zipout = new ZipOutputStream(outputStream);
//开始覆盖文档------------------
int len = -1;
byte[] buffer = new byte[1024];
while (zipEntrys.hasMoreElements()) {//遍历zip文件内容
ZipEntry next = zipEntrys.nextElement();
InputStream is = zipFile.getInputStream(next);
if (next.toString().indexOf("media") < 0) {
zipout.putNextEntry(new ZipEntry(next.getName()));//这步相当于创建了个文件,下面是将流写入这个文件
if ("word/document.xml".equals(next.getName())) {//如果是word/document.xml由我们输入
if (documentInput != null) {
while ((len = documentInput.read(buffer)) != -1) {
zipout.write(buffer, 0, len);
}
documentInput.close();
}
} else {
while ((len = is.read(buffer)) != -1) {
zipout.write(buffer, 0, len);
}
is.close();
}
}else{//这里设置图片信息,针对要显示的图片
zipout.putNextEntry(new ZipEntry(next.getName()));
while ((len = is.read(buffer)) != -1) {
zipout.write(buffer, 0, len);
}
is.close();
}
}
}
} catch (Exception e) {
e.printStackTrace();
logger.error("word导出失败:"+e.getStackTrace());
}finally {
if(zipout!=null){
try {
zipout.close();
} catch (IOException e) {
logger.error("io异常");
}
}
if(outputStream!=null){
try {
outputStream.close();
} catch (IOException e) {
logger.error("io异常");
}
}
}
}
}
四、插入图片
通过传值已经可以基本完成生成文档的功能了,但是用户还要求要在文档中生成统计分析图。知道文档本质的我马上想出了办法,但是这个就稍微有点麻烦了。刚刚说了,media文件夹下存放的是文档中所有的图片。我可以在word文档要生成统计图的地方先用图片占好位置,调好大小。然后解压后看这个图片在media文件夹中叫什么名字。最后在代码生成的时候,跳过原文件图片的写入替换成我生成的图片就可以了。核心代码如下:
while (zipEntrys.hasMoreElements()) {
ZipEntry next = zipEntrys.nextElement();
InputStream is = zipFile.getInputStream(next);
if (next.toString().indexOf("media") < 0) {
...省略
}else{//这里设置图片信息,针对要显示的图片
zipout.putNextEntry(new ZipEntry(next.getName()));
if(next.getName().indexOf("image1.png")>0){//例如要用一张图去替换模板中的image1.png
if(dataMap.get("image1")!=null){
byte[] bys = Base64Util.decode(dataMap.get("image1").toString());
zipout.write(bys,0,bys.length);
}else{
while ((len = is.read(buffer)) != -1) {
zipout.write(buffer, 0, len);
}
is.close();
}
}
}
}
注意:这个Base64Util是一个将图片文件转化为base64编码字符串和将base64编码字符串转换为字节数组的工具类。这里涉及到图片文件的本质,图片的本质是一个二进制文件,二进制文件可以转换成字节数组,而字节数组又可以和字符串互相转换。这个知道就好,这里我把生成的图片的字符串存入了集合中,写入文档的时候将字节数组写入。这样原图片就被替换成了生成的图片了。这里直接将新的图片文件写入也是一样的效果。