根据一个word模板,在程序中替换模板中的参数,然后根据这个模板导出word文件。

引入POI对word操作的依赖

1
2
3
4
5
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>3.17</version>
</dependency>

引入的版本一定要高一点,低版本会出现导出的word中图片无法显示的BUG


创建WordUtils类(使用poi导出word模板的操作都封装在这了)

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
@Component
public class WordUtils {

/**
* description:
* 导出word模板
* @param path:word模板路径
* @param params:模板中需要替换的参数可多个传递 比如 若想文字能够多行,在参数
* Map<String,Object>中的Object访如List<string>
* 若是传递图片 参数Map<String,Object>中的Object为Map<String,Obejct>
* 若是一个参数处要传入多张图片Object就为List<Map<String,Object>
* 在传入多张图片的时候 Map中设置style ,style=1 代表图片并排插入,
* style=2代表图片竖排插入,设置imgpath,即图片路径。
* 传递多张图片的时候imgpath和style两个参数必填,若是一张,imgpath必填
* @param filename:导出的word文件名
* @param response:
* @return void
*/
public void exportWord(String path, Map<String,Object> params, String filename, HttpServletResponse response) throws IOException, InvalidFormatException {
InputStream is=new FileInputStream(path);
XWPFDocument doc=new XWPFDocument(is);
iteraTable(params,doc);
OutputStream os=response.getOutputStream();
//设置导出的内容是doc
response.setContentType("application/octet-stream; charset=utf-8");
response.setHeader("Content-disposition", "attachment; filename=" + filename);
doc.write(os);
close(os);
}

//开始遍历文档中的表格
private void iteraTable(Map<String,Object> params,XWPFDocument doc) throws IOException, InvalidFormatException {
List<XWPFTableRow> rows=null;
List<XWPFTableCell> cells=null;

List<XWPFTable> tables=doc.getTables();
//遍历这个word文档的所有table表格
for (XWPFTable table :
tables) {
rows=table.getRows();
//遍历这个表格的所有行
for (XWPFTableRow row :
rows) {
cells=row.getTableCells();
//遍历这一行的单元格
for (XWPFTableCell cell:
cells) {
//判断该单元格的内容是否是字符串字段
if(strMatcher(cell.getText()).find()){
//替换字符串 字符串可以多行 也可以一行
replaceInStr(cell,params,doc);
continue;
}
//判断该单元格内容是否是需要替换的图片
if(imgMatcher(cell.getText()).find()){
//把模板中的内容替换成图片 图片可以多涨
replaceInImg(cell,params,doc);
continue;
}
}
}
}
}

//返回模板中图片字符串的匹配Matcher类
private Matcher imgMatcher(String imgstr){
Pattern pattern=Pattern.compile("@\\{(.+?)\\}");
Matcher matcher=pattern.matcher(imgstr);
return matcher;
}

//返回模板中变量的匹配Matcher类
private Matcher strMatcher(String str){
Pattern pattern=Pattern.compile("\\$\\{(.+?)\\}");
Matcher matcher=pattern.matcher(str);
return matcher;
}


//替换模板Table中相应字段为对应的字符串值
private void replaceInStr(XWPFTableCell cell, Map<String,Object> params, XWPFDocument doc) {
//两种数据类型 一种是直接String 还有一种是List<String>
String key = cell.getText().substring(2, cell.getText().length() - 1);

Integer datatype = getMapStrDataTypeValue(params, key);

List<XWPFParagraph> parags = cell.getParagraphs();
//先清空单元格中所有的段落
for (int i = 0; i < parags.size(); i++) {
cell.removeParagraph(i);
}
if (datatype.equals(0)) {
return;
} else if (datatype.equals(2)) {
//如果类型是2 说明数据类型是List<String>
List<String> strs = (List<String>) params.get(key);
Iterator<String> iterator = strs.iterator();
while (iterator.hasNext()) {
XWPFParagraph para = cell.addParagraph();
XWPFRun run = para.createRun();
run.setText(iterator.next());
}
} else if (datatype.equals(3)) {
String str = (String) params.get(key).toString();
cell.setText(str);
}
}

//替换模板Table中相应字段为图片
private void replaceInImg(XWPFTableCell cell,Map<String,Object> params,XWPFDocument doc) throws IOException, InvalidFormatException {
//拿参数 判断一下是什么类型 怎么处理 一般来说Map中只有两种类型 一种是一个模板图片变量放一张图片 只有一个Map 一种是一个模板变量中放多涨图片List<Map> 还有一种是不存在的情况
String key=cell.getText().substring(2,cell.getText().length()-1);
Integer datatype=getMapImgDataType(params,key);
List<XWPFParagraph> parags=cell.getParagraphs();
//先清空单元格中所有的段落
for (int i=0;i<parags.size();i++){
cell.removeParagraph(i);
}
if(datatype.equals(0)){
return;
}else if(datatype.equals(1)){
//处理单张图片
XWPFParagraph parag=cell.addParagraph();
Map<String,Object> pic=(Map<String,Object>)params.get(key);
insertImg(pic,parag);
}else if(datatype.equals(2)) {
//处理多涨图片
List<Map<String,Object>> pics=(List<Map<String,Object>>)params.get(key);
Iterator<Map<String, Object>> iterator = pics.iterator();
XWPFParagraph para = cell.addParagraph();
Integer count = 0;
while (iterator.hasNext()) {
Map<String, Object> pic = iterator.next();
//图片并排插入
if (Integer.parseInt(pic.get("style").toString()) == 1) {
//不做处理 这段代码可注释
}
//图片竖排插入
if (Integer.parseInt(pic.get("style").toString()) == 2) {
if (count > 0) {
para = cell.addParagraph();
}
}
insertImg(pic, para);
count++;
}
}
}


//插入图片 run创建
private void insertImg(Map<String,Object> pic,XWPFParagraph para) throws IOException, InvalidFormatException {
if(pic.get("imgpath").toString()==null)
return;
String picpath=pic.get("imgpath").toString();
InputStream is=null;
BufferedImage bi=null;
if(picpath.startsWith("http")) {
is=HttpUtils.getFileStream(picpath);
bi=ImageIO.read(HttpUtils.getFileStream(picpath));
}else {
is = new FileInputStream(picpath);
bi=ImageIO.read(new File(picpath));
}
XWPFRun run =para.createRun();
//原图片的长宽
Integer width=bi.getWidth();
Integer heigh=bi.getHeight();
Double much=80.0/width;
//图片按宽80 比例缩放
run.addPicture(is,getPictureType(picpath.substring(picpath.lastIndexOf(".")+1)),"", Units.toEMU(80),Units.toEMU(heigh*much));
//图片原长宽
// run.addPicture(is,getPictureType(pic.get("picType").toString()),"",Units.toEMU(width),Units.toEMU(heigh));
close(is);
bi=null;
}

//插入图片 自定义 doc方法
// private static void insertImg(Map<String,Object> pic,XWPFParagraph para,CustomXWPFDocument doc) throws FileNotFoundException, InvalidFormatException {
// InputStream is=null;
// if(pic.get("imgpath").toString()==null)
// return;
// is=new FileInputStream(pic.get("imgpath").toString());
// byte[] bytes=inputStream2ByteArray(is,true);
// doc.addPictureData(bytes, getPictureType(pic.get("picType").toString()));
// doc.createPicture(doc.getAllPictures().size() - 1, 30, 360,para);
// close(is);
// }


//处理模板中的变量名字,去掉${} 然后根据这个变量名在参数map中查找对应的Value值
private Integer getMapStrDataTypeValue(Map<String,Object> params,String key){
if(params.get(key)==null){
return 0;
}else if(params.get(key) instanceof List){
return 2;
}else if(params.get(key) instanceof String){
return 3;
}else {
throw new RuntimeException("Str data type error!");
}

}

//从params参数中找到模板中key对应的value值
private Integer getMapImgDataType(Map<String,Object> params,String key){
if(params.get(key)==null){
return 0;
}
else if(params.get(key) instanceof Map){
return 1;
}else if(params.get(key) instanceof List){
return 2;
}else {
throw new RuntimeException("image data type error!");
}
}


/**
* 根据图片类型,取得对应的图片类型代码
*
* @param picType
* @return int
*/
private int getPictureType(String picType) {
int res = XWPFDocument.PICTURE_TYPE_PICT;
if (picType != null) {
if (picType.equalsIgnoreCase("png")) {
res = XWPFDocument.PICTURE_TYPE_PNG;
} else if (picType.equalsIgnoreCase("dib")) {
res = XWPFDocument.PICTURE_TYPE_DIB;
} else if (picType.equalsIgnoreCase("emf")) {
res = XWPFDocument.PICTURE_TYPE_EMF;
} else if (picType.equalsIgnoreCase("jpg") || picType.equalsIgnoreCase("jpeg")) {
res = XWPFDocument.PICTURE_TYPE_JPEG;
} else if (picType.equalsIgnoreCase("wmf")) {
res = XWPFDocument.PICTURE_TYPE_WMF;
}
}
return res;
}

/**
* 将输入流中的数据写入字节数组
*
* @param in
* @return
*/
public byte[] inputStream2ByteArray(InputStream in, boolean isClose) {
byte[] byteArray = null;
try {
//获得输入流中还有多少字节可读取
int total = in.available();
byteArray = new byte[total];
in.read(byteArray);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (isClose) {
try {
in.close();
} catch (Exception e2) {
e2.getStackTrace();
}
}
}
return byteArray;
}


/**
* 关闭输入流
*
* @param is
*/
private void close(InputStream is) {
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

/**
* 关闭输出流
*
* @param os
*/
private void close(OutputStream os) {
if (os != null) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

由于图片有时候并不是本地资源,而是别的服务器上的静态资源,所以创建一个HttpUtils类,写一个方法,通过图片的URL来获得图片的输入流。

HttpUtils.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class HttpUtils {
/**
* description:
* 通过图片URL地址得到文件流
* @Author any
* @Date 2020/8/14 11:51
* @param url:图片地址
* @return java.io.InputStream
*/
public static InputStream getFileStream(String url){
try {
URL httpUrl = new URL(url);
HttpURLConnection conn = (HttpURLConnection)httpUrl.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5 * 1000);
InputStream inStream = conn.getInputStream();//通过输入流获取图片数据
return inStream;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

}

word模板

图片的前缀是@,如果是文字那么前缀是#,然后参数名用{}包裹起来。

模板示例

在这里插入图片描述

测试代码

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
@RestController
public class MainTest {
@Autowired
private WordUtils wordUtils;

@RequestMapping("/test/mydocx")
public void exportWord(HttpServletResponse response) throws IOException, InvalidFormatException {
//模板路径
String path="F:\\testmodel\\model.docx";
//传入模板的参数
Map<String,Object> params=new HashMap<>();
//模板的导出文件名字
String filename="mydocxtest.docx";

//一个单元格内一行字
params.put("name","张三");
params.put("sex","男");

//一个单元格内多行字
List<String> hobby=new ArrayList<>();
hobby.add("1、打篮球");
hobby.add("2、打羽毛球");
hobby.add("3、游泳");
params.put("hobby",hobby);


//图片参数1 多张图片 图片多行竖排排列 style为2
List<Map<String,Object>> imgs1List=new ArrayList<>();
Map<String,Object> img=new HashMap<>();
img.put("style",2);
img.put("imgpath","F:\\mytestimg\\testimg1.jpg");
imgs1List.add(img);
img=new HashMap<>();
img.put("style",2);
//放一张服务器上的图片资源
img.put("imgpath","https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1597386736254&di=e25ddaabbe7e9259f00008f087cc8ab7&imgtype=0&src=http%3A%2F%2Fimg.mukewang.com%2Fszimg%2F5e3ae4bd096481b210320850.jpg");
imgs1List.add(img);
params.put("workimg",imgs1List);

//图片参数2 多张图片 一行内横排排列 style为1
List<Map<String,Object>> imgs2List=new ArrayList<>();
Map<String,Object> img2=new HashMap<>();
img2.put("style",1);
img2.put("imgpath","F:\\mytestimg\\sign1.png");
imgs2List.add(img2);
img2=new HashMap<>();
img2.put("style",1);
img2.put("imgpath","F:\\mytestimg\\sign2.png");
imgs2List.add(img2);
img2=new HashMap<>();
img2.put("style",1);
img2.put("imgpath","F:\\mytestimg\\sign3.png");
imgs2List.add(img2);
params.put("signimg",imgs2List);

//图片参数3 单张图片
Map<String,Object> img3=new HashMap<>();
img3.put("imgpath","F:\\mytestimg\\testimg1.jpg");
params.put("otherimg",img3);
wordUtils.exportWord(path,params,filename,response);

}
}

模板中的参数 一定要和程序中传入的参数的key值一样,比如模板参数#{name},那么程序中的map参数的key为name,这样就能对应起来了。

效果

在这里插入图片描述

备注

  • 新建word后另存为Word 2003 XML文档(*.xml)类型即为模板文件,可以修改后缀为.ftl
  • 模板备注后变量千万不要直接填写为变量的形式,模板会将其分割,可以先在word中设定变量,生成模板后再将变量改为{变量}的形式,模板会将其分割,可以先在word中设定变量,生成模板后再将变量改为{变量}的形式
  • 项目部署需要提前将模板文件保存在服务器的项目路径的/templates下,有时间可修改此种方式不依赖于本地文件