Java开发之生成word文档

标签: 无 分类: 未分类 创建时间:2024-04-26 03:00:10 更新时间:2024-11-15 10:49:43

1.前言

参考文章:
【1】.Java根据word模板生成word文件 XDocReport +FreeMarker,该技术组合既简单又高效可实现word模板的编辑,docx和doc均可处理
【2】.java 实现根据word模板生成word文件 word转pdf 生产wode文档用的是freemarker方式,网上推荐的方式有 itexpdf(失真很厉害用不成);poi包方式(各种包的报错,Poi引用的一些类包里没有,将其他包引入后这些包又有引用类没有,解决不完,用不成); docx4j(比上面两种好一些,但是也失真厉害达不到生产使用的标准,生成pdf的时间也长)aspose-words(pj 版包,效果不错)
【3】.java 根据word模板生成word文件 Java可以使用Apache POI库来生成Word文件,并且也可以使用freemarker等模板引擎来实现根据Word模板生成Word文件的功能。CTRL+f9生成域——》鼠标右键编辑域——》选择邮件合并—–》在域代码后面加上英文${跟代码内的一致}。
【4】.Java Apache POI根据 word 模板 template 生成新的 word 文档 要操作 Office Word 文档,就需要先知道是如何进行操作的。Office2003以上,Word可以以XML文本格式存储,这样可以使用外部程序创建Word文件,而不需要使用Word的对象。也能够自由的打开分析Word文件,或者发布到自己的Web页面,或者其他更多应用。

2.编辑模版

首先需要准备模版,在 wps 中,依次点击 插入”—“文档部件”—“域”,选择 “邮件合并”,即可。刚开始我插入域的时候,遇到了 “错误!未指定书签”,这玩意用 wps,用 wrod编辑都是一样的,问题还不好找,我修改了域名称,结果无法确定了。

后来我知道了,需要直接在 域代码后面添上变量的内容,比如添加一个 name 变量,需要填写: MERGEFIELD ${name}

3.引入依赖

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
<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>

3.编写测试代码

编写测试代码,填入相关参数。

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
public void exportXcbg(HttpServletResponse response){

InputStream ins = null;
ByteArrayOutputStream out = null;
try {
String fileName="xxx.docx";
//获取Word模板,模板存放路径
ins = new FileInputStream(resources+File.separator+fileName);
//注册xdocreport实例并加载FreeMarker模板引擎
IXDocReport report = XDocReportRegistry.getRegistry().loadReport(ins, TemplateEngineKind.Freemarker);
//创建xdocreport上下文对象,用于存放具体数据
IContext context = report.createContext();
//创建要替换的文本变量
context.put("name", "李梅");

// 渲染
out=new ByteArrayOutputStream();
report.process(context, out);
// 设置强制下载不打开
response.setContentType("application/octet-stream");
response.addHeader("Content-Disposition", "attachment;fileName=" + new String(fileName.getBytes("UTF-8"),"iso-8859-1"));
response.setContentLength(out.size());

ServletOutputStream servletOutputStream=response.getOutputStream();
out.writeTo(servletOutputStream);

// 输出
servletOutputStream.flush();
servletOutputStream.close();
} catch (Exception e) {
log.error("生成word发生异常", e);
} finally {
try {
if (ins != null){
ins.close();
}
if (out != null){
out.close();
}
} catch (IOException e) {
log.error("文件流关闭失败", e);
}
}
}
参考文章:
【1】.java IXDocReport导出word文档 这里是使用了 TemplateEngineKind.Velocity 模版
【2】.通过IXDocReport 根据word文档模板 生成word文档包含循环图片以及循环数据(表格中)
【3】.java IXDocReport文档流下载 这里提供了一个方法,就是生成了 ByteArrayOutputStrem,并提供了下载。

4.插入图片

(1) 首先编辑模版,在模版中插入一张占位图,然后选中图片,选择 “插入” -> 选择 “书签”,插入一张书签,不需要加入 $ 符号。

(2) 编写代码
创建图片元数据,我加入的是xchdztt这个书签,然后将相关的字段进行填充。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
String fileName="xxx.docx";
//获取Word模板,模板存放路径
ins = new FileInputStream(resources+File.separator+fileName);
//注册xdocreport实例并加载FreeMarker模板引擎
IXDocReport report = XDocReportRegistry.getRegistry().loadReport(ins, TemplateEngineKind.Freemarker);
//创建xdocreport上下文对象,用于存放具体数据
IContext context = report.createContext();

// 插入图片
FieldsMetadata metadata = report.createFieldsMetadata();
InputStream fins = new FileInputStream(new File(resources+File.separator+"word_1.png"));
metadata.addFieldAsImage("xchdztt");
context.put("xchdztt", new ByteArrayImageProvider(fins));
report.setFieldsMetadata(metadata);
参考文章:
【1】.Java根据word模板生成word文件 这里还有插入图片的操作

5.列表循环

(1) 编辑模版,插入域,编辑列表头: MERGEFIELD [#list lonlist as item]

编辑列表尾:MERGEFIELD [/#list]

中间就用:MERGEFIELD ${item.lat},调用列表中的变量

(2) 编写代码

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
String fileName="xxx.docx";
//获取Word模板,模板存放路径
ins = new FileInputStream(resources+File.separator+fileName);
//注册xdocreport实例并加载FreeMarker模板引擎
IXDocReport report = XDocReportRegistry.getRegistry().loadReport(ins, TemplateEngineKind.Freemarker);
//创建xdocreport上下文对象,用于存放具体数据
IContext context = report.createContext();

// 插入列表
List<JSONObject> lonlist=new ArrayList<>();
JSONObject temp=new JSONObject();
temp.put("lon",123.123);
temp.put("lat",30.12);
lonlist.add(temp);

temp=new JSONObject();
temp.put("lon",112.33);
temp.put("lat",31.12);
lonlist.add(temp);

temp=new JSONObject();
temp.put("lon",123.083);
temp.put("lat",33.12);
lonlist.add(temp);

context.put("lonlist",lonlist);
参考文章:
【1】.java IXDocReport 循环 这里用了 #foreach 语法,我没有尝试过
【2】.java freemarker + word 模板 生成 word 文档 (变量替换,数据的循环,表格数据的循环,以及图片的替换) 循环的写法 [#list list as item] [/#list] 依然是要注意两端的中括号,最好两端在加引号括起来
【3】.基于xdocreport+freemarker导出复杂word文档

6.循环图片

(1) 修改模版
在需要添加列表循环的地方,增加了一个图片的书签,我命名为 listimg,和单独添加图片书签是一样的,列表的域代码 :[#list lonlist as item]。

(2) 编写代码
这里和普通的列表有两个区别,一个就是增加了 ByteArrayImageProvider 属性,另外一个就是使用了原数据图片转换: metadata.addFieldAsImage(“listimg”,”item.listimg”),

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
 // 插入列表
List<JSONObject> lonlist=new ArrayList<>();
JSONObject temp=new JSONObject();
temp.put("lon",123.123);
temp.put("lat",30.12);
InputStream fins1 = new FileInputStream(new File(resources+File.separator+"word_1.png"));
temp.put("listimg",new ByteArrayImageProvider(fins1));
lonlist.add(temp);

temp=new JSONObject();
temp.put("lon",112.33);
temp.put("lat",31.12);
InputStream fins2 = new FileInputStream(new File(resources+File.separator+"word_2.png"));
temp.put("listimg",new ByteArrayImageProvider(fins2));

lonlist.add(temp);

temp=new JSONObject();
temp.put("lon",123.083);
temp.put("lat",33.12);
InputStream fins3 = new FileInputStream(new File(resources+File.separator+"word_3.png"));
temp.put("listimg",new ByteArrayImageProvider(fins3));
lonlist.add(temp);

context.put("lonlist",lonlist);
// 设置图片 和 列表内容转换,这里很重要,
metadata.addFieldAsImage("listimg","item.listimg");
report.setFieldsMetadata(metadata);

// 渲染
out=new ByteArrayOutputStream();
report.process(context, out);

7.嵌套循环

除了一层循环之外,就是在循环内嵌套一层循环。

1
«[#list pinlist as pinitem]»«[#list pinlist as pinitem]»ddd«[/#list]»东经:«${pinitem.lon}»;北纬:«${pinitem.lat}»。«[/#list]»

生成的域代码如下:

这里有一个比较难的地方,就是关于嵌套循环列表中有图片的问题。我尝试了很多的方法,弄了很久,始终找不到到底如何生成一个嵌套的循环图片。

参考文章:
【1】.FreeMaker循环嵌套数据输出 也很简单,就是按照格式多写一个循环。
【2】.Word 报告模板
【3】.freemarker实现多层嵌套循环 直接就是 #list 嵌套,如果不知道有几层就多写几个list。

8.嵌套循环图片

(1) 模版
修改word模版,创建一个循环嵌套的列表域,如下。这里的图片标签,一定要定位到图片上,然后再添加书签 xczp。 我吃亏就吃亏在这里,我尝试了很多方法,弄了很多次,最后都是无法生成word文档,甚至处理了好几天,让我非常的苦恼。

1
2
3
4
5
MERGEFIELD [#list pinlist as pinitem]
MERGEFIELD [#list pinitem.imglist as imgitem]
图片书签【xczp】
MERGEFIELD [/#list]
MERGEFIELD [/#list]

(2) 代码
代码里面就是创建两个循环,一个是 pinlist 列表,一个是 pinitem.imglist 列表,其中关键的代码就是 metadata.addFieldAsImage(“xczp”,”imgitem.xczp”), 这里的 imgitem 要用内循环的变量,而不是外循环的其他的一些东西。我尝试过 pinitem.imglist.xczp,尝试过其他的东西,基本上也都是不行的。

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
// 变量生成打点列表
List<JSONObject> pinItemList=new ArrayList<>();
int dwNumber=4; // 点位编号开始
int imgNumber=3; // 图编号
int pinCount=pinList.size();

// 遍历打点数据
for(int count=0;count<pinCount;count++){
Pin pin=pinList.get(count);
JSONObject temp=new JSONObject();
temp.put("lon",pin.getLon()); // 经度
temp.put("lat",pin.getLat()); // 纬度
temp.put("wtms",pin.getWtms()); // 问题描述
temp.put("dwNumber",dwNumber); // 点位编号
dwNumber+=1;

try {

String dwtp=pin.getDwtp();
if(!dwtp.isEmpty()){
InputStream photoImg=MinioUtil.getMinioFile(MinioUtil.getBucketName(),dwtp);
temp.put("kjfb",new ByteArrayImageProvider(photoImg));
temp.put("imgNumber",imgNumber); // 点位编号
imgNumber+=1;
}

/**
* 现场照片
*/
String xczp=pin.getXczp();
String[] xczpArray=xczp.split(",");
int xczpCount=xczpArray.length;
JSONArray imgList=new JSONArray();
for (int i=0;i<xczpCount;i++){
String xczpPath=xczpArray[i];
if(!xczpPath.isEmpty()){
JSONObject xczpTemp=new JSONObject();
// 现场照片
InputStream photoImg=MinioUtil.getMinioFile(MinioUtil.getBucketName(),xczpPath);
xczpTemp.put("xczp",new ByteArrayImageProvider(photoImg));

// 内循环图片编号
xczpTemp.put("number",imgList.size()+1);
// 外循环图片编号
xczpTemp.put("imgnumber",imgNumber+imgList.size()+1);
// 加入列表
imgList.add(xczpTemp);
}
}
temp.put("imglist",imgList);
temp.put("imgNumber",imgNumber); // 点位编号
imgNumber+=1;
}catch (Exception e){
log.error("xczp error",e);
}
// 生成打点列表
pinItemList.add(temp);
}
// 现场照片
metadata.addFieldAsImage("xczp","imgitem.xczp");
// 空间分布照片
metadata.addFieldAsImage("kjfb","pinitem.kjfb");
report.setFieldsMetadata(metadata);
// 渲染列表
context.put("pinlist",pinItemList);

9.循环表格

我另外一个需求就是,根据列表中的内容,循环生成不同的表格。

(1)在表格的开头编辑域 @before-row[#list pinlist as pinitem]
(2)在表格的结尾编辑域 @after-row[/#list]

10.判断

IF 判断的写法,需要三个域,每一个的创建方式和上面相同 内容为 “[#if 1 == 1]” 文档内容 “ [#else]” 文档内容 “ [/#if]” , 注意要加中括号,两端最好在加上引号。

问题

(1) InputStream is not a zip.
这是我加载一个 doc 文档的时候出现的问题。

【解决方案】
将 doc 文档另存为 docx 文档。

参考文章:
【1】.XDocReport can’t create report from InputStream java.util.zip.ZipExeption So I suupose you are loading a bad document (perhaps a .doc which is binary files and not a zip which contains xml entries).

(2) Encountered “[/#if]”, but at this place only this can be closed: “#list”.

参考文章:
【1】.[Encountered “[/#if]”, but at this place only this can be closed: “#list”.]
小额赞助
本人提供免费与付费咨询服务,感谢您的支持!赞助请发邮件通知,方便公布您的善意!
**光 3.01 元
Sun 3.00 元
bibichuan 3.00 元
微信公众号
广告位
诚心邀请广大金主爸爸洽谈合作
每日一省
isNaN 和 Number.isNaN 函数的区别?

1.函数 isNaN 接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回 true,因此非数字值传入也会返回 true ,会影响 NaN 的判断。

2.函数 Number.isNaN 会首先判断传入参数是否为数字,如果是数字再继续判断是否为 NaN ,不会进行数据类型的转换,这种方法对于 NaN 的判断更为准确。

每日二省
为什么0.1+0.2 ! == 0.3,如何让其相等?

一个直接的解决方法就是设置一个误差范围,通常称为“机器精度”。对JavaScript来说,这个值通常为2-52,在ES6中,提供了Number.EPSILON属性,而它的值就是2-52,只要判断0.1+0.2-0.3是否小于Number.EPSILON,如果小于,就可以判断为0.1+0.2 ===0.3。

每日三省
== 操作符的强制类型转换规则?

1.首先会判断两者类型是否**相同,**相同的话就比较两者的大小。

2.类型不相同的话,就会进行类型转换。

3.会先判断是否在对比 null 和 undefined,是的话就会返回 true。

4.判断两者类型是否为 string 和 number,是的话就会将字符串转换为 number。

5.判断其中一方是否为 boolean,是的话就会把 boolean 转为 number 再进行判断。

6.判断其中一方是否为 object 且另一方为 string、number 或者 symbol,是的话就会把 object 转为原始类型再进行判断。

每日英语
Happiness is time precipitation, smile is the lonely sad.
幸福是年华的沉淀,微笑是寂寞的悲伤。