前言
在日常的开发工作中,我们时常会遇到导出Word文档报表的需求,比如公司的财务报表、医院的患者统计报表、电商平台的销售报表等等。
导出Word方式多种多样,通常有以下几种方式:
1. 使用第三方Java工具类库Hutool的Word工具类,参考网址为https://www.hutool.cn/docs/#/poi/Word生成-Word07Writer;
2. 利用Apache POI和FreeMarker模板引擎;
3. 第三方报表工具。
上面的几种方式虽然可以实现Word文档的导出,但有以下缺点:
第一种方式操作简单,但也只能生成简单的Word文档,无法生成有表格的Word文档;
第二种方式可以生成复杂的Word文档,但是还要进行Word转xml,xml转ftl的双重转换,不适合内容经常变更的Word文档;
第三种方式有时候不适合对格式要求严格的文档。
那么,有没有既简单又高效的导出Word的方法呢?答案是肯定有的。接下来我就来介绍一种用Java语言实现的,通过XDocReport和FreeMarker模板引擎生成Word文档的方法。
准备环境
开发语言:
Java7及以上的版本。
开发工具:
Eclipse/Idea。
第三方依赖库:
XDocReport、POI、Freemarker。
模板语言:
FreeMarker。
Word编辑器:
Microsoft 365或其他版本较高的Word编辑器。
示例Word模板
制作模板
Word模板如上图,可以看到,结构比较简单,包括两个部分,第一部分是纯文字和数字,第二部分主要是表格。我们在实际的开发过程中生成的报表几乎都是动态生成的,所以模板中的数字和表格里的数据都要替换成我们后台的实际数据。
替换Word模板中的动态变量,我们需要掌握两个知识点:
1.Word文档中的Word域,word域是引导Word在文档中自动插入文字、图形、页码或其他信息的一组代码。在这里我们可以把 Word域理解成标识符,这个标识符表示Word文档中要被替换的内容;
2.FreeMarker模板下的变量表达式,比如用${city}替换Word示例模板中的北京市。
了解了以上两个概念后,我们就可以动手编辑Word模板了,步骤如下:
1. 首先在Word模板中选中要替换的文本,在这儿拿标题中的"北京市"为例,然后键盘使用 Ctrl + F9 组合键将其设置为域,此时文本会被"{}"包围,接着鼠标右键选择【编辑域(E)...】:
2. 在弹出的对话框中,类别选择“邮件合并”,域名选择 "MergeField",域属性中的域名填入模版表达式${city},点击【确定】按钮:
3. 编辑后的效果如下:
4. 掌握替换文本的方法后,我们可以把Word模板第一部分需要替换的内容都替换成模板变量:
Word模板中表格数据的处理
表格中的数据实质上就是对集合的遍历。
表格数据的处理其实和上面对文本内容的处理是类似的,只不过要在Word模板中加上集合的变量,Java代码中也要有对集合进行特对的处理(这个在后面的代码展示部分会说)。
具体操作步骤如下:
1. 选定表格中要替换的文本,然后键盘使用 Ctrl + F9 组合键将其设置为域,接着鼠标右键选择【编辑域(E)...】:
2. 在弹出的对话框中,类别选择“邮件合并”,域名选择 "MergeField",域属性中的域名填入模版表达式${goods.num},点击【确定】按钮;
3. 重复步骤2,替换表格中的其他文本内容:
后台代码
添加依赖到pom.xml文件
-
<dependency
>
-
<groupId
>org.apache.poi
<
/groupId
>
-
<artifactId
>poi
<
/artifactId
>
-
<version
>
4.1.1
<
/version
>
-
<
/dependency
>
-
<dependency
>
-
<groupId
>org.apache.poi
<
/groupId
>
-
<artifactId
>poi-ooxml
<
/artifactId
>
-
<version
>
4.1.1
<
/version
>
-
<
/dependency
>
-
<dependency
>
-
<groupId
>org.jxls
<
/groupId
>
-
<artifactId
>jxls
<
/artifactId
>
-
<version
>
2.6.0
<
/version
>
-
<exclusions
>
-
<exclusion
>
-
<groupId
>
ch.qos.logback
<
/groupId
>
-
<artifactId
>logback-core
<
/artifactId
>
-
<
/exclusion
>
-
<
/exclusions
>
-
<
/dependency
>
-
<dependency
>
-
<groupId
>org.jxls
<
/groupId
>
-
<artifactId
>jxls-poi
<
/artifactId
>
-
<version
>
1.2.0
<
/version
>
-
<
/dependency
>
-
<dependency
>
-
<groupId
>fr.opensagres.xdocreport
<
/groupId
>
-
<artifactId
>fr.opensagres.xdocreport.core
<
/artifactId
>
-
<version
>
2.0.2
<
/version
>
-
<
/dependency
>
-
<dependency
>
-
<groupId
>fr.opensagres.xdocreport
<
/groupId
>
-
<artifactId
>fr.opensagres.xdocreport.document
<
/artifactId
>
-
<version
>
2.0.2
<
/version
>
-
<
/dependency
>
-
<dependency
>
-
<groupId
>fr.opensagres.xdocreport
<
/groupId
>
-
<artifactId
>fr.opensagres.xdocreport.template
<
/artifactId
>
-
<version
>
2.0.2
<
/version
>
-
<
/dependency
>
-
<dependency
>
-
<groupId
>fr.opensagres.xdocreport
<
/groupId
>
-
<artifactId
>fr.opensagres.xdocreport.document.docx
<
/artifactId
>
-
<version
>
2.0.2
<
/version
>
-
<
/dependency
>
-
<dependency
>
-
<groupId
>fr.opensagres.xdocreport
<
/groupId
>
-
<artifactId
>fr.opensagres.xdocreport.template.freemarker
<
/artifactId
>
-
<version
>
2.0.2
<
/version
>
-
<
/dependency
>
-
<dependency
>
-
<groupId
>org.freemarker
<
/groupId
>
-
<artifactId
>freemarker
<
/artifactId
>
-
<version
>
2.3.23
<
/version
>
-
<
/dependency
>
-
<dependency
>
-
<groupId
>commons-io
<
/groupId
>
-
<artifactId
>commons-io
<
/artifactId
>
-
<version
>
2.5
<
/version
>
-
<
/dependency
>
编写Java代码
-
package com.tzsj.
test;
-
-
import java.io.
File;
-
import java.io.FileOutputStream;
-
import java.io.IOException;
-
import java.io.InputStream;
-
import java.util.ArrayList;
-
import java.util.List;
-
import org.junit.
Test;
-
import org.springframework.stereotype.Controller;
-
import org.springframework.web.bind.annotation.RequestMapping;
-
import fr.opensagres.xdocreport.core.XDocReportException;
-
import fr.opensagres.xdocreport.document.IXDocReport;
-
import fr.opensagres.xdocreport.document.registry.XDocReportRegistry;
-
import fr.opensagres.xdocreport.template.IContext;
-
import fr.opensagres.xdocreport.template.TemplateEngineKind;
-
import fr.opensagres.xdocreport.template.formatter.FieldsMetadata;
-
import io.renren.entity.Goods;
-
-
@Controller
-
@RequestMapping(
"/word")
-
public
class WordTest {
-
@
Test
-
public void
test() throws IOException, XDocReportException {
-
generateWord();
-
}
-
-
public void generateWord() throws IOException, XDocReportException {
-
/
/获取Word模板,模板存放路径在项目的resources目录下
-
InputStream ins
= this.getClass().getResourceAsStream(
"/模板.docx");
-
/
/注册xdocreport实例并加载FreeMarker模板引擎
-
IXDocReport
report
= XDocReportRegistry.getRegistry().loadReport(ins,
-
TemplateEngineKind.Freemarker);
-
/
/创建xdocreport上下文对象
-
IContext context
=
report.createContext();
-
-
/
/创建要替换的文本变量
-
context.put(
"city",
"北京市");
-
context.put(
"startDate",
"2020-09-17");
-
context.put(
"endDate",
"2020-10-16");
-
context.put(
"totCnt",
3638763);
-
context.put(
"totAmt",
"6521");
-
context.put(
"onCnt",
2874036);
-
context.put(
"onAmt",
"4768");
-
context.put(
"offCnt",
764727);
-
context.put(
"offAmt",
"1753");
-
context.put(
"typeCnt",
36);
-
-
List
<Goods
> goodsList
= new ArrayList
<Goods
>();
-
Goods goods
1
= new Goods();
-
goods
1.setNum(
1);
-
goods
1.setType(
"臭美毁肤");
-
goods
1.setSv(
675512);
-
goods
1.setSa(
"589");
-
Goods goods
2
= new Goods();
-
goods
2.setNum(
2);
-
goods
2.setType(
"女装");
-
goods
2.setSv(
602145);
-
goods
2.setSa(
"651");
-
Goods goods
3
= new Goods();
-
goods
3.setNum(
3);
-
goods
3.setType(
"手机");
-
goods
3.setSv(
587737);
-
goods
3.setSa(
"866");
-
Goods goods
4
= new Goods();
-
goods
4.setNum(
4);
-
goods
4.setType(
"家具家装");
-
goods
4.setSv(
551193);
-
goods
4.setSa(
"783");
-
Goods goods
5
= new Goods();
-
goods
5.setNum(
5);
-
goods
5.setType(
"食物饮品");
-
goods
5.setSv(
528604);
-
goods
5.setSa(
"405");
-
goodsList.
add(goods
1);
-
goodsList.
add(goods
2);
-
goodsList.
add(goods
3);
-
goodsList.
add(goods
4);
-
goodsList.
add(goods
5);
-
context.put(
"goods", goodsList);
-
-
/
/创建字段元数据
-
FieldsMetadata fm
=
report.createFieldsMetadat
a();
-
/
/Word模板中的表格数据对应的集合类型
-
fm.load(
"goods", Goods.
class,
true);
-
-
/
/输出到本地目录
-
FileOutputStream out
= new FileOutputStream(new
File(
"D://商品销售报表.docx"));
-
report.process(context, out);
-
}
-
-
}
Word模板中生成序号
给表格数据添加序号是通过后台代码生成的,比如上面的"goods1.setNum(1)"这段代码,其实也可以在Word模板中设置对应的域变量来实现序号的填充。
语法如下:
-
@before-row[#list sequence as item]
-
item?index
-
@after-row[/#list]
在表格中添加上面的表达式,XDocReport就会自动解析并生成序号,表格中的其他字段也需要进行相应的改动:
提示:
1. 序号的表达式要拆成三个域,如下图,要把这三部分分别设置成域;
设置完成的结果参考上面表格中的序号表达式,表达式中"item?index+1"是因为序号是从0开始的,所以要加1;
2. 表格中除序号的列需要改成item.xxx而不是之前的goods.xxx:
3. 生成效果如下:
建议:序号最好在后台生成,用序号表达式生成的序号列会占用比较大的空间,对资源有所浪费。
补充
1. JavaWeb项目中通常是通过浏览器下载的方式来下载Word文档,此时只需要把之前下载到本地的代码改成浏览器端下载的代码即可:
-
/
/输出到本地目录
-
/
/FileOutputStream out
= new FileOutputStream(new
File(
"D://商品销售报表.docx"));
-
/
/
report.process(context, out);
-
-
/
/浏览器端下载
-
response.setCharacterEncoding(
"utf-8");
-
response.setContentType(
"application/msword");
-
String fileName
=
"商品销售报表.docx";
-
response.setHeader(
"Content-Disposition",
"attachment;filename="
-
.concat(
String.valueOf(URLEncoder.encode(fileName,
"UTF-8"))));
-
report.process(context, response.getOutputStream());
2. Word模板中的表格的长度最好充满Word文档的左右两边,否则如果表格下面还有其他文本内容,下面的文本内容会自动填充到表格的缝隙处,而且会对下面的文本内容进行覆盖。
加餐
其实,导出Word模板,上面的模板和代码已经够用了,但也有少数模板需要添加图片和图形(比如饼状图)。
制作图片
图片的生成不使用编辑域,使用模板图片和Word的书签功能,而且需要在元数据中加入图片类型的代码,以下为具体步骤:
1. 在Word模版中需要插入图片的位置插入一张模版图片,然后鼠标单击模板图片插入一个书签,设置书签名称,比如img1, 最后点击【添加】按钮:
2. 如果需要插入多个图片,就在需要插入图片的位置插入多个模板图片并插入书签设置对应的书签名称即可,后台代码如下:
-
package com.tzsj.
test;
-
-
import java.io.
File;
-
import java.io.FileOutputStream;
-
import java.io.IOException;
-
import java.io.InputStream;
-
import org.junit.
Test;
-
import fr.opensagres.xdocreport.core.XDocReportException;
-
import fr.opensagres.xdocreport.document.IXDocReport;
-
import fr.opensagres.xdocreport.document.registry.XDocReportRegistry;
-
import fr.opensagres.xdocreport.template.IContext;
-
import fr.opensagres.xdocreport.template.TemplateEngineKind;
-
import fr.opensagres.xdocreport.template.formatter.FieldsMetadata;
-
-
public
class ImgTest {
-
-
@
Test
-
public void
test() throws IOException, XDocReportException {
-
generateWordForImg();
-
}
-
-
-
public void generateWordForImg() throws IOException, XDocReportException {
-
/
/获取Word模板,模板存放路径在项目的resources目录下
-
InputStream ins
= this.getClass().getResourceAsStream(
"/图片.docx");
-
/
/注册xdocreport实例并加载FreeMarker模板引擎
-
IXDocReport
report
= XDocReportRegistry.getRegistry().loadReport(ins,
-
TemplateEngineKind.Freemarker);
-
/
/创建xdocreport上下文对象
-
IContext context
=
report.createContext();
-
-
FieldsMetadata fm
=
report.createFieldsMetadat
a();
-
/
/元数据中加入图片
-
fm.addFieldAsImage(
"img1");
-
fm.addFieldAsImage(
"img2");
-
-
/
/获取图片
-
InputStream img
1
= this.getClass().getResourceAsStream(
"/11.jpg");
-
InputStream img
2
= this.getClass().getResourceAsStream(
"/33.jpg");
-
-
/
/把图片添加到上下文对象
-
context.put(
"img1", img
1);
-
context.put(
"img2", img
2);
-
-
/
/输出到本地目录
-
FileOutputStream out
= new FileOutputStream(new
File(
"D://图片报表.docx"));
-
report.process(context, out);
-
}
-
-
}
3. 导出效果如下:
制作图形
要在Word文档中生成柱状图、饼状图等图形,需要在项目中引入第三方绘图工具,在这里使用xchart来演示在Word中生成饼状图图形。
生成饼状图和生成图片的方法很类似,具体步骤如下:
1. 在Word模版中需要插入图片的位置插入一张模版图片,然后鼠标单击模板图片插入一个书签,设置书签名称,比如chart,最后点击【添加】按钮:
2. 编写代码:
2.1 在pom.xml文件中添加xchart的依赖:
-
<dependency
>
-
<groupId
>org.knowm.xchart
<
/groupId
>
-
<artifactId
>xchart
<
/artifactId
>
-
<version
>
3.5.4
<
/version
>
-
<
/dependency
>
2.2 后台代码:
-
package com.tzsj.
test;
-
-
import java.io.ByteArrayOutputStream;
-
import java.io.
File;
-
import java.io.FileOutputStream;
-
import java.io.IOException;
-
import java.io.InputStream;
-
import org.junit.
Test;
-
import org.knowm.xchart.BitmapEncoder;
-
import org.knowm.xchart.PieChart;
-
import org.knowm.xchart.PieChartBuilder;
-
import fr.opensagres.xdocreport.core.XDocReportException;
-
import fr.opensagres.xdocreport.document.IXDocReport;
-
import fr.opensagres.xdocreport.document.images.ByteArrayImageProvider;
-
import fr.opensagres.xdocreport.document.registry.XDocReportRegistry;
-
import fr.opensagres.xdocreport.template.IContext;
-
import fr.opensagres.xdocreport.template.TemplateEngineKind;
-
import fr.opensagres.xdocreport.template.formatter.FieldsMetadata;
-
-
public
class ChartTest {
-
-
@
Test
-
public void
test() throws IOException, XDocReportException {
-
generateWordForChart();
-
}
-
-
-
public void generateWordForChart() throws IOException, XDocReportException {
-
/
/获取Word模板,模板存放路径在项目的resources目录下
-
InputStream ins
= this.getClass().getResourceAsStream(
"/饼图.docx");
-
/
/注册xdocreport实例并加载FreeMarker模板引擎
-
IXDocReport
report
= XDocReportRegistry.getRegistry().loadReport(ins,
-
TemplateEngineKind.Freemarker);
-
-
/
/创建xdocreport上下文对象
-
IContext context
=
report.createContext();
-
-
FieldsMetadata fm
=
report.createFieldsMetadat
a();
-
/
/元数据中加入图片
-
fm.addFieldAsImage(
"chart");
-
-
PieChart chart
= new PieChartBuilder().width(
800).height(
620)
-
.title(
"销售饼图").build();
-
-
/
/给饼图设置对应的值
-
chart.addSeries(
"臭美毁肤",
589);
-
chart.addSeries(
"女装",
651);
-
chart.addSeries(
"手机",
866);
-
chart.addSeries(
"家居家装",
783);
-
chart.addSeries(
"食物饮品",
405);
-
-
/
/生成饼图
-
ByteArrayOutputStream baos
= new ByteArrayOutputStream();
-
BitmapEncoder.saveBitmap(chart, baos, BitmapEncoder.BitmapFormat.JPG);
-
-
/
/把饼图添加到上下文对象
-
context.put(
"chart", new ByteArrayImageProvider(baos.toByteArray()));
-
-
/
/输出到本地目录
-
FileOutputStream out
= new FileOutputStream(new
File(
"D://饼图报表.docx"));
-
report.process(context, out);
-
}
-
-
-
}
3. 导出效果如下:
总结
这就是用Java语言实现,结合XDocReport和FreeMarker模板引擎生成Word文档的方法。希望能给致力于开发的小伙伴带来一丝丝帮助。
如果您在浏览过程中遇到什么问题,请在下方评论区给我留言。
本人入驻的其他平台:
- 微信公众号:弹指时间 。
- 网易音乐人:弹指时间。