前言
在日常的开发工作中,我们时常会遇到导出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模板
制作模板
Word模板如上图,可以看到,结构比较简单,包括两个部分,第一部分是纯文字和数字,第二部分主要是表格。我们在实际的开发过程中生成的报表几乎都是动态生成的,所以模板中的数字和表格里的数据都要替换成我们后台的实际数据。
替换Word模板中的动态变量,我们需要掌握两个知识点:
1.Word文档中的Word域,word域是引导Word在文档中自动插入文字、图形、页码或其他信息的一组代码。在这里我们可以把Word域理解成标识符,这个标识符表示Word文档中要被替换的内容;
2.FreeMarker模板下的变量表达式,比如用${city}替换Word示例模板中的北京市。
4. 掌握替换文本的方法后,我们可以把Word模板第一部分需要替换的内容都替换成模板变量:
Word模板中表格数据的处理
表格中的数据实质上就是对集合的遍历。
表格数据的处理其实和上面对文本内容的处理是类似的,只不过要在Word模板中加上集合的变量,Java代码中也要有对集合进行特对的处理(这个在后面的代码展示部分会说)。
具体操作步骤如下:
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目录下InputStreamins=this.getClass().getResourceAsStream(“/模板.docx”); //注册xdocreport实例并加载FreeMarker模板引擎IXDocReportreport=XDocReportRegistry.getRegistry().loadReport(ins, TemplateEngineKind.Freemarker); //创建xdocreport上下文对象IContextcontext=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 goods1 = new Goods();goods1.setNum(1);goods1.setType(“臭美毁肤”);goods1.setSv(675512);goods1.setSa(“589”);Goods goods2 = new Goods();goods2.setNum(2);goods2.setType(“女装”);goods2.setSv(602145);goods2.setSa(“651”);Goods goods3 = new Goods();goods3.setNum(3);goods3.setType(“手机”);goods3.setSv(587737);goods3.setSa(“866”);Goods goods4 = new Goods();goods4.setNum(4);goods4.setType(“家具家装”);goods4.setSv(551193);goods4.setSa(“783”);Goods goods5 = new Goods();goods5.setNum(5);goods5.setType(“食物饮品”);goods5.setSv(528604);goods5.setSa(“405”);goodsList.add(goods1);goodsList.add(goods2);goodsList.add(goods3);goodsList.add(goods4);goodsList.add(goods5);context.put(“goods”, goodsList); //创建字段元数据FieldsMetadatafm=report.createFieldsMetadata(); //Word模板中的表格数据对应的集合类型fm.load(“goods”,Goods.class,true); //输出到本地目录FileOutputStreamout=newFileOutputStream(newFile(“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模板,上面的模板和代码已经够用了,但也有少数模板需要添加图片和图形(比如饼状图)。
制作图片
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.createFieldsMetadata(); //元数据中加入图片 fm.addFieldAsImage(“img1”); fm.addFieldAsImage(“img2”); //获取图片 InputStream img1 = this.getClass().getResourceAsStream(“/11.jpg”); InputStream img2 = this.getClass().getResourceAsStream(“/33.jpg”); //把图片添加到上下文对象 context.put(“img1”, img1); context.put(“img2”, img2); //输出到本地目录 FileOutputStream out = new FileOutputStream(new File(“D://图片报表.docx”)); report.process(context, out); }}
3. 导出效果如下:
制作图形
要在Word文档中生成柱状图、饼状图等图形,需要在项目中引入第三方绘图工具,在这里使用xchart来演示在Word中生成饼状图图形。
生成饼状图和生成图片的方法很类似,具体步骤如下:
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.createFieldsMetadata(); //元数据中加入图片 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文档的方法。希望能给致力于开发的小伙伴带来一丝丝帮助。