Windows/Linux环境下安装并使用Libreoffice(SpingBoot 整合Libreoffice)&Linux字体库新增字体

现有功能需求需将word模板转成pdf的方式,进行在线预览或打印,实现这个需求有几种当时,如下:
1、使用jacob:word模板需要创建书签,以替换书签的方式来填充数据。然后转成pdf的格式。使用简单方便,但是只能在windows环境下使用,不能在linux环境使用
2、使用aspose.words:通过创建word模板的一个域的标识,来替换相应的数据,具体百度,也是使用特别方便的一个组件,基本上手就能用,能在windows和Linux下使用,可惜是商用的,项目需要发布上线的话还是先获取版权的好,个人使用的话倒是没所谓。
3、使用POI:word模板需要创建特殊的标识,如:{name},来标识需要替换的数据位置,操作word文档简单,可在windows和Linux下使用,但是使用POI将word转成pdf时,会出现pdf与word模板格式不一致的问题,格式会错乱。

综上考虑实现word转pdf,并能在Linux环境下使用,采用POI+Libreoffice的方式实现,其中POI实现替换word模板中特殊标识去替换数据,使用Libreoffice是将替换好的word文档转成pdf,转成的pdf格式和word模板的一致。

文末附上实现的代码案例,使用Libreoffice需先本机安装

windows环境下安装Libreoffice

准备好安装包,双击安装

在这里插入图片描述
安装好的目录如下:program
在这里插入图片描述

安装完后需要编写一个 Libreoffice的启动脚本

Libreoffice的启动脚本命名为:startLibreOffice.bat
脚本位置随意,调用的时候需要用到,配置文件写明位置即可
编辑脚本如下:其中E:\Install\program\soffice.exe为我的Libreoffice安装路径文件启动位置,个人安装目录不同自行修改

taskkill /f /fi "IMAGENAME eq soffice.exe"
taskkill /f /fi "IMAGENAME eq soffice.bin"
ping -n 5 127.0.0.1
E:\grFile\Install\program\soffice.exe -headless -nologo -nofirststartwizard -accept="socket,host=127.0.0.1,port=8100;urp;"
echo "start complete"
exit

到此,windows环境下Libreoffice安装完毕
遇到个问题,多次试验,安装的文件名尽量不要使用中文以及含空格的文件名

linux环境下安装Libreoffice

准备好Linux版本的安装包,上传到linux服务器上

在这里插入图片描述

将Libreoffice的安装包上传到 /opt 目录下
打指令

1.	cd /opt
2.	tar -zxvf LibreOffice_5.3.6_Linux_x86-64_rpm.tar.gz  解压到当前路径
3.	sudo yum install ./LibreOffice_5.3.6_Linux_x86-64_rpm/RPMS/*.rpm 安装
4.	cd /opt/libreoffice5.3/program,这里就是程序目录了

开始调试是否安装完成,运行以下指令

/opt/libreoffice5.3/program/soffice.bin -headless --nologo --nofirststartwizard --accept="socket,host=127.0.0.1,port=8100;urp;"
运行上述命令,报错一个解决一个,直到成功为止
1.	执行sudo updatedb命令,如果没有这个命令,就需要先安装, yum install mlocate
2.	报错找不到libcairo.so.2: yum install cairo-devel
3.	报错找不到libcups.so.2: yum install libXinerama cups-libs
4.	报错找不到libGL.so.1: yum install libGL
5.	报错找不到libSM.so.6: yum install libSM
6.	如果需要删除,则yum remove libcairo.so.2
可能有缺包情况:lib*.so.*,则直接yum install即可
记住要刷新缓存:sudo updatedb
如果不能用,就得安装下: yum install mlocate

调试完成后,将Libreoffice放在后台运行,输入指令

nohup /opt/libreoffice5.3/program/soffice.bin -headless --nologo --nofirststartwizard --accept="socket,host=127.0.0.1,port=8100;urp;" &

运行后,查看是否运行,输入指令

ps aux | grep soffice
或者
netstat -tnlp检查是否启动成功。
如果需要关闭,则记下第二列的编码,运行 kill -9 编码

到此,Linux环境下Libreoffice安装完毕
Linux环境下不需要写启动脚本,直接后台运行,挂掉了参考上面指令后台启动
遇到个问题,多次试验,word转成pdf后,pdf的所有中文内容出现正方形的小框框。这个是因为Linux系统的字体库没有对应字体,需要另外新建字体库

Linux字体库新增字体

可以将windows系统下的字体直接复制粘贴上传到Linux系统下
windows系统下的字体位置:C:\Windows\Fonts
输入指令步骤如下:

1.	yum -y install fontconfig
2.	cd /usr/share,如果有fontconfig和fonts,则说明安装成功
3.	打开这个fonts文件夹下,新建一个叫chinese文件夹
4.	然后将准备好的字体文件全部拷进去(windows系统的可以)
5.	chmod -R 755 /usr/share/fonts/chinese,给予该文件夹权限
6.	yum -y install ttmkfdir
7.	ttmkfdir -e /usr/share/X11/fonts/encodings/encodings.dir
8.	vi /etc/fonts/fonts.conf,进入这个配置文件,
9.	在Font directory list配置项下,添加<dir>/usr/share/fonts/chinese</dir>
10.	退出后执行[fc-cache]刷新缓存
11.	此时重启下libreOffice比较保险

至此,Linux字体库新增字体完成

环境安装完成,实现功能

maven导入依赖

<!-- 连接libreOffice驱动包 -->
<dependency>
	<groupId>com.artofsolving</groupId>
	<artifactId>jodconverter</artifactId>
	<version>2.2.2</version>
</dependency>
<dependency>
	<groupId>org.openoffice</groupId>
	<artifactId>bootstrap-connector</artifactId>
	<version>0.1.1</version>
</dependency>

application.properties配置信息

windows环境下的配置信息
libreOffice5Bat为libreoffice的启动脚本位置,详情查看安装时的准备

#libreOffice5
libreOffice5Bat=E\:/libreoffice/startLibreOffice.bat
libreOfficeTempPath=E\:/temp/
libreOfficeUrl=127.0.0.1
libreOfficePort=8100

Linux环境下的配置信息

#libreOffice5
libreOfficeTempPath=/apps/tmp/
libreOfficeUrl=127.0.0.1
libreOfficePort=8100

实现功能的工具类

对word模板的操作类

import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.apache.poi.xwpf.usermodel.XWPFTable;
import org.apache.poi.xwpf.usermodel.XWPFTableCell;
import org.apache.poi.xwpf.usermodel.XWPFTableRow;

/**
 * 对word模板的操作,包含替换文本和列表向下扩展两种<br>
 * 本工具类的使用需要配合ExportWord.class,具体Demo看ExportWord类即可
 * @author 
 */
public class WordOperator {

	/**
	 * 模板替换标签开头
	 */
	private final static String LABEL_STR = "{";
	/**
	 * 模板替换标签结尾
	 */
	private final static String LABEL_END = "}";
	/**
	 * 加载word对象
	 */
	private XWPFDocument doc;
	
	/**
	 * 构造方法,把模板文档加载进来<br>
	 * 		因为微软只提供XWPFRun对象替换文本,而它在切割段落文本的时候却是非常没有规律的,故而本方法要求模板必须遵循一定的规范来编写:<br>
	 * 		1.模板里除了标签外,其余地方禁止使用"{"和"}"字符串;<br>
	 * 		2.不允许出现"{}"这种写法,即标签必须包含key;
	 */
	public WordOperator(InputStream is) throws Exception {
		doc = new XWPFDocument(is);
	}
	
	/**
	 * 要替换标签的KV对
	 */
	private Map<String, String> replaceMap;
	
	/**
	 * 在word模板文档里,用{key}标签来表示要替换的内容,把key对应的value存储于replaceMap里,即可完成替换
	 */
	public WordOperator replaceText(Map<String, String> result) {
		this.replaceMap = result;
		// 替换文本内容的标签
		List<XWPFParagraph> paragraphList = doc.getParagraphs();
		replaceText(paragraphList);
		// 替换表格内部标签
		List<XWPFTable> tableList = doc.getTables();
		for (XWPFTable table : tableList) {
			for (int i = 0; i < table.getNumberOfRows(); i++) {
				XWPFTableRow row = table.getRow(i);
				List<XWPFTableCell> tableCellList = row.getTableCells();
				for (XWPFTableCell cell : tableCellList) {
					replaceText(cell.getParagraphs());
				}
			}
		}
		return this;
	}
	
	/**
	 * 把替换段落抽取出一个方法,在替换文本和替换表格里都可以调用
	 */
	private void replaceText(List<XWPFParagraph> paragraphList) {
		int labelLen = LABEL_STR.length();
		// 取出word模板里的全部段落,遍历
		for (int i = 0; i < paragraphList.size(); i++) {
			XWPFParagraph paragraph = paragraphList.get(i);
			// 拿出每一个段落,判断内容里是否包含字符串"{"和"}",只有两者同时存在了才执行替换标签的逻辑
			String paragraphText = paragraph.getText();
			if (!StringUtils.isBlank(paragraphText) && paragraphText.indexOf(LABEL_STR) != -1 && paragraphText.indexOf(LABEL_END) != -1) {
				// 每一个段落分很多小段文本,官方API只能否通过XWPFRun对象执行替换文本功能
				List<XWPFRun> runList = paragraph.getRuns();
				// 组装replaceMap的key
				String key = "";
				// 是否检测到有"{"字符串,有为true,没有为false
				boolean include = false;
				for (int j = 0; j < runList.size(); j++) {
					XWPFRun run = runList.get(j);
					String text = run.getText(0);
					//有些模板不太规范,可能存在大量空格无法识别,内容为null,空指针异常,跳过即可
					if(StringUtils.isBlank(text)) {
						continue;
					}
					System.out.println(text);
					// 取出每一个小段里标签头和尾的角标
					int labelStrIndex = text.indexOf(LABEL_STR);
					int labelEndIndex = text.indexOf(LABEL_END);
					// 只有{
					if (labelStrIndex != -1 && labelEndIndex == -1) {
						// 例如有"aaa{bbb",则aaa不动,{bbb去掉,同时把bbb累加到key里
						String textBefore = text.substring(0, labelStrIndex);
						String textAfter = text.substring(labelStrIndex + labelLen);
						run.setText(textBefore, 0);
						key += textAfter;
						include = true;
						// 只有}
					} else if (labelStrIndex == -1 && labelEndIndex != -1) {
						// 例如有"aaa}bbb",则aaa}去掉,bbb不动,同时把aaa累加到key里
						String textBefore = text.substring(0, labelEndIndex);
						String textAfter = text.substring(labelEndIndex + labelLen);
						key += textBefore;
						System.out.println(key);
						Object value = replaceMap.get(key);
						System.out.println(value);
						if (StringUtils.isBlank(value)) {
							value = " ";
						}
						key = "";
						run.setText(value + textAfter, 0);
						include = false;
						// 两个都没有
					} else if (labelStrIndex == -1 && labelEndIndex == -1) {
						// 例如有"aaa",如果之前有{了,则把内容累加到key里,同时去掉内容;如果没有{,则说明是普通文本,跳过不管
						if (include) {
							key += text;
							run.setText(" ", 0);
						}
						// 两个都存在,这种情况比较复杂。经过多次试验,XWPFRun切割内容,只会同时各出现1次而已
					} else {
						// {在前,}在后
						if (labelStrIndex < labelEndIndex) {
							// 例如有"aaa{bbb}ccc",则aaa不动,{bbb}去掉同时进行替换,ccc保留(因为有规范,所以这是一个独立完整的标签,且bbb绝对有值)
							String textBefore = text.substring(0, labelStrIndex);
							String textMiddle = text.substring(labelStrIndex + labelLen, labelEndIndex);
							String textAfter = text.substring(labelEndIndex + labelLen);
							Object value = replaceMap.get(textMiddle);
							if (StringUtils.isBlank(value)) {
								value = " ";
							}
							run.setText(textBefore + value + textAfter, 0);
							key = "";
							include = false;
							// }在前,{在后
						} else {
							// 例如有"aaa}bbb{ccc",则aaa}去掉,且进行替换;bbb不动,{ccc去掉,累加到key里去
							String textBefore = text.substring(0, labelEndIndex);
							String textMiddle = text.substring(labelEndIndex + labelLen, labelStrIndex);
							String textAfter = text.substring(labelStrIndex + labelLen);
							key += textBefore;
							Object value = replaceMap.get(key);
							if (StringUtils.isBlank(value)) {
								value = " ";
							}
							run.setText(value + textMiddle, 0);
							key = textAfter;
							include = true;
						}
					}
				}
			}
		}
	}
	
	/**
	 * 表格向下扩展数据行,因为POI对word格式支持较差,故我们做一些约定<br>
	 * 	1.约定表格要有一行模板行,用于扩展数据行的样式效仿;<br>
	 * 	2.模板表格里最好不要有合并行,如非常需要,则可以用隐藏单元格或设置白色边框的方式巧妙替代合并行;<br>
	 * 	3.模板行下方不允许出现其他行<br>
	 * @param tableIndex	表格角标,第1个表为0
	 * @param tempRow		模板行角标,第2行为1(模板行不是标题行,而是扩展数据行样式的效仿对象)
	 * @param isDelTempRow	true-删除模板行;false-不删除模板行
	 * @param rowlist		要插入的数据集合,格式为[行集合<列集合>,列集合的长度最好与模板行长度一致]
	 */
	public WordOperator insert2Table(int tableIndex, int tmpRowIndex, boolean isDelTmpRow, List<List<String>> rowlist) {
		// 因为需要对rowlist进行添加操作,故而我们重新做一个变量,防止因为改变而产生问题
		List<List<String>> dataList = new ArrayList<List<String>>();
		dataList.addAll(rowlist);
		// 根据角标取出表格对像
		XWPFTable table = doc.getTables().get(tableIndex);
		// 再根据行号获取模板行
		XWPFTableRow tmpRow = table.getRow(tmpRowIndex);
		// 不知道是什么原因,最后一列数据会跑到模板行上一行,故而这里在最末添加一组数据,解决这个bug
		dataList.add(new ArrayList<String>());
		// 遍历数据集合
		for (int i = 0, len = dataList.size(); i < dataList.size(); i++) {
			// 计算数据行编号
			int dataIndex = tmpRowIndex + 1 + i;
			// 按照模板行样式添加一行到数据行
			table.addRow(tmpRow, dataIndex);
			if (i < len - 1) {
				// 取出这一行
				XWPFTableRow row = table.getRow(dataIndex);
				// 取出这一行全部单元格
				List<XWPFTableCell> cellList = row.getTableCells();
				// 取出这一行对应的数据,数据与单元格角标位置是一致的,一一替换文本
				List<String> colList = dataList.get(i);
				for (int j = 0; j < cellList.size(); j++) {
					setCellText(cellList.get(j), colList, j);
				}
			}
		}
		// 删掉那一行为了bug而添加的空白行(因为空白行跑到模板行上一行,所以tmpRowIndex为该行角标)
		table.removeRow(tmpRowIndex);
		// 删除模版行(因为上面已经删除了空白行了,所以tmpRowIndex就变成了下一行模板行了)
		if (isDelTmpRow) {
			table.removeRow(tmpRowIndex);
		}
		return this;
	}
	
	/**
	 * 替换单元格文本
	 * @param cell		单元格
	 * @param colList	替换文本的集合(角标越位了,就置空)
	 * @param j			单元格角标,也就是文本集合的角标
	 */
	private boolean setCellText(XWPFTableCell cell, List<String> colList, int j) {
		// 无论一个单元格被切割成多少个run,只需要修改第一个即可,改完就return掉了
		List<XWPFParagraph> paragraphs = cell.getParagraphs();
		for (XWPFParagraph p : paragraphs) {
			List<XWPFRun> runs = p.getRuns();
			for (XWPFRun r : runs) {
				try {
					r.setText(colList.get(j), 0);
				} catch (Exception e) {
					r.setText(" ", 0);
				}
				return true;
			}
		}
		return false;
	}
	
	/**
	 * 给定输出流即可将word文档导出到
	 */
	public void write(OutputStream os) throws Exception {
		doc.write(os);
	}
	
}

导出word工具类(基于POI)

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.io.IOUtils;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.xmlbeans.XmlOptions;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBody;
import org.springframework.core.io.ClassPathResource;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * 导出word工具类(基于POI)
 * @author 
 */
public class ExportWord {
	/**
	 * 举个例子
	 */
	@RequestMapping("demo")
	public void demo(HttpServletRequest request, HttpServletResponse response) {
		try {
			// 替换标签{aa}和{bb}
			Map<String, String> map = new HashMap<String, String>();
			map.put("aa", "替换aa");
			map.put("bb", "替换bb");
			// 列表4列,从左向右装填数据,然后向下扩展两行数据行
			List<List<String>> rwoList = new ArrayList<List<String>>();
			List<String> l1 = new ArrayList<String>();
			l1.add("a1");
			l1.add("b1");
			l1.add("c1");
			l1.add("d1");
			rwoList.add(l1);
			List<String> l2 = new ArrayList<String>();
			l2.add("a2");
			l2.add("b2");
			l2.add("c2");
			l2.add("d2");
			rwoList.add(l2);
			// 加载第1个模板,执行替换标签、给第1个表格扩展数据(第二行为模板行,扩展后删除模板行)
			WordOperator wo = ExportWord.getWordOperator("demo/demo.docx");
			wo.replaceText(map).insert2Table(1, 1, true, rwoList);
			// 加载第2个模板,执行第2个表格扩展数据(第二行为模板行,扩展后仍然保留模板行)
			WordOperator wo2 = ExportWord.getWordOperator("demo/demo2.docx");
			wo2.insert2Table(1, 1, false, rwoList);
			// 以下6个方法,任选1个调用:
			// --> 下载word
			ExportWord.download_word(request, response, "导出文件", wo);
			// --> 下载word(多个模板合并)
			ExportWord.download_word(request, response, "导出文件", wo, wo2);
			// --> 下载pdf
			ExportWord.download_pdf(request, response, "导出文件", wo);
			// --> 下载pdf(多个模板合并)
			ExportWord.download_pdf(request, response, "导出文件", wo, wo2);
			// --> 预览pdf
			ExportWord.preview_pdf(request, response, "导出文件", wo);
			// --> 预览pdf(多个模板合并)
			ExportWord.preview_pdf(request, response, "导出文件", wo, wo2);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * 模板根路径
	 */
	public final static String FILE_PATH = "/dot/";
	
	/**
	 * 获取WordOperator对象
	 * @param templateName	word模板名字(必须放在/src/main/resources/dot下)
	 */
	public static WordOperator getWordOperator(String templateName) throws Exception {
		return new WordOperator(new FileInputStream(new ClassPathResource( FILE_PATH + templateName).getFile()));
	}
	
	/**
	 * 下载生成的word文档
	 * @param request		请求
	 * @param response		响应
	 * @param fileName		新文档名字(不带后缀名)
	 * @param wordOperator	文档操作对象
	 */
	public static void download_word(HttpServletRequest request, HttpServletResponse response, String fileName, WordOperator wordOperator) throws Exception {
		ServletOutputStream os = null;
		try {
			// 设置好响应头,然后将文件保存到输出流里即可
			response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
			response.setHeader("Content-Disposition", "attachment;filename=" + FileUtils.encodeFileName(FileUtils.addRandom(fileName) + ".docx", request));
			os = response.getOutputStream();
			wordOperator.write(os);
			os.flush();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			// 关闭输出流
			if (os != null) {
				os.close();
			}
		}
	}
	
	/**
	 * 下载生成的word文档(多文件合并)
	 * @param request		请求
	 * @param response		响应
	 * @param fileName		新文档名字(不带后缀名)
	 * @param woList		文档操作对象集合(合并顺序)
	 */
	public static void download_word(HttpServletRequest request, HttpServletResponse response, String fileName, List<WordOperator> woList) throws Exception {
		download_word(request, response, fileName, woList.toArray(new WordOperator[0]));
	}
	
	/**
	 * 下载生成的word文档(多文件合并)
	 * @param request		请求
	 * @param response		响应
	 * @param fileName		新文档名字(不带后缀名)
	 * @param woList		文档操作对象集合(合并顺序)
	 */
	public static void download_word(HttpServletRequest request, HttpServletResponse response, String fileName, WordOperator... woList) throws Exception {
		ServletOutputStream os = null;
		List<String> tempDocList = new ArrayList<String>();
		try {
			// 设置响应头
			response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
			response.setHeader("Content-Disposition", "attachment;filename=" + FileUtils.encodeFileName(FileUtils.addRandom(fileName) + ".docx", request));
			os = response.getOutputStream();
			// 一个wo对应一份文档,循环保存下来
			for (WordOperator wo : woList) {
				String tempDoc = getPath() +"/" + UUID.randomUUID().toString() + ".docx";
				wo.write(new FileOutputStream(tempDoc));
				tempDocList.add(tempDoc);
			}
			// 然后把上面保存的文档合并起来
			String tempDoc = getPath() +"/" + mergeDocx(tempDocList);
			tempDocList.add(tempDoc);
			// 将这份文档以流的形式保存到输出流中
			IOUtils.copy(new FileInputStream(new File(tempDoc)), os);
			os.flush();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			// 关闭输出流
			if (os != null) {
				os.close();
			}
			// 将全部临时文档删除掉
			for (String tempDoc : tempDocList) {
				FileUtils.delFile(tempDoc);
			}
		}
	}
	
	/**
	 * 下载生成的pdf文档
	 * @param request		请求
	 * @param response		响应
	 * @param fileName		新文档名字(不带后缀名)
	 * @param wordOperator	文档操作对象
	 */
	public static void download_pdf(HttpServletRequest request, HttpServletResponse response, String fileName, WordOperator wordOperator) throws Exception {
		ServletOutputStream os = null;
		String tempDoc = null;
		String tempPdf = null;
		try {
			// 设置响应头
			response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
			response.setContentType(MimeUtils.getByExtension("pdf") + ";charset=UTF-8");
			response.setHeader("Content-Disposition", "attachment;filename=" + FileUtils.encodeFileName(FileUtils.addRandom(fileName) + ".pdf", request));
			// 先将生成的word文档保存起来
			tempDoc = getPath() +"/" + UUID.randomUUID().toString() + ".docx";
			wordOperator.write(new FileOutputStream(tempDoc));
			// 然后将word转成pdf保存起来
			tempPdf = getPath() +"/" + UUID.randomUUID().toString() + ".pdf";
			PDFUtils.office2PDF(tempDoc, tempPdf);
			// 将这份文档以流的形式保存到输出流中
			os = response.getOutputStream();
			FileUtils.copyFile(new File(tempPdf), os);
			os.flush();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			// 关闭输出流
			if (os != null) {
				os.close();
			}
			// 将临时文档删除掉
			if (tempDoc != null) {
				FileUtils.delFile(tempDoc);
			}
			// 将临时文档删除掉
			if (tempPdf != null) {
				FileUtils.delFile(tempPdf);
			}
		}
	}
	
	/**
	 * 下载生成的pdf文档(多文件合并)
	 * @param request		请求
	 * @param response		响应
	 * @param fileName		新文档名字(不带后缀名)
	 * @param woList		文档操作对象集合(合并顺序)
	 */
	public static void download_pdf(HttpServletRequest request, HttpServletResponse response, String fileName, List<WordOperator> woList) throws Exception {
		download_pdf(request, response, fileName, woList.toArray(new WordOperator[0]));
	}
	
	/**
	 * 下载生成的pdf文档(多文件合并)
	 * @param request		请求
	 * @param response		响应
	 * @param fileName		新文档名字(不带后缀名)
	 * @param woList		文档操作对象集合(合并顺序)
	 */
	public static void download_pdf(HttpServletRequest request, HttpServletResponse response, String fileName, WordOperator... woList) throws Exception {
		List<String> tempDocList = new ArrayList<String>();
		ServletOutputStream os = null;
		try {
			// 设置响应头
			response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
			response.setContentType(MimeUtils.getByExtension("pdf") + ";charset=UTF-8");
			response.setHeader("Content-Disposition", "attachment;filename=" + FileUtils.encodeFileName(FileUtils.addRandom(fileName) + ".pdf", request));
			// 一个wo对应一份文档,循环保存下来
			for (WordOperator wo : woList) {
				String tempDoc = getPath() +"/" + UUID.randomUUID().toString() + ".docx";
				wo.write(new FileOutputStream(tempDoc));
				tempDocList.add(tempDoc);
			}
			// 然后把上面保存的文档合并起来
			String tempDoc = getPath() +"/" + mergeDocx(tempDocList);
			tempDocList.add(tempDoc);
			// 再把这份合并好的文档转成pdf
			String tempPdf = getPath() +"/" + UUID.randomUUID().toString() + ".pdf";
			PDFUtils.office2PDF(tempDoc, tempPdf);
			tempDocList.add(tempPdf);
			// 将这份文档以流的形式保存到输出流中
			os = response.getOutputStream();
			FileUtils.copyFile(new File(tempPdf), os);
			os.flush();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			// 关闭输出流
			if (os != null) {
				os.close();
			}
			// 将全部临时文档删除掉
			for (String tempDoc : tempDocList) {
				FileUtils.delFile(tempDoc);
			}
		}
	}
	
	/**
	 * 预览生成的pdf文档
	 * @param request		请求
	 * @param response		响应
	 * @param fileName		新文档名字(不带后缀名)
	 * @param wordOperator	文档操作对象
	 */
	public static void preview_pdf(HttpServletRequest request, HttpServletResponse response, String fileName, WordOperator wordOperator) throws Exception {
		String tempDoc = null;
		String tempPdf = null;
		ServletOutputStream os = null;
		try {
			// 设置响应头
			response.setContentType(MimeUtils.getByExtension("pdf") + ";charset=UTF-8");
			response.setHeader("Content-Disposition", "inline;filename=" + FileUtils.encodeFileName(FileUtils.addRandom(fileName) + ".pdf", request));
			// 先将生成的word文档保存起来
			tempDoc =getPath() +"/"+ UUID.randomUUID().toString() + ".docx";
			wordOperator.write(new FileOutputStream(tempDoc));
			// 然后将word转成pdf保存起来
			tempPdf = getPath() +"/"+UUID.randomUUID().toString() + ".pdf";
			PDFUtils.office2PDF(tempDoc, tempPdf);
			// 将pdf文档以流的形式输送给response
			os = response.getOutputStream();
			File pdfFile = new File(tempPdf);
			FileUtils.copyFile(pdfFile, os);
			os.flush();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			// 关闭输出流
			if (os != null) {
				os.close();
			}
			// 将word和pdf两个临时文件删除掉
			if (tempDoc != null) {
				FileUtils.delFile(tempDoc);
			}
			if (tempPdf != null) {
				FileUtils.delFile(tempPdf);
			}
		}
	}
	
	/**
	 * 预览生成的pdf文档(多文件合并)
	 * @param request		请求
	 * @param response		响应
	 * @param fileName		新文档名字(不带后缀名)
	 * @param woList		文档操作对象集合(合并顺序)
	 */
	public static void preview_pdf(HttpServletRequest request, HttpServletResponse response, String fileName, List<WordOperator> woList) throws Exception {
		preview_pdf(request, response, fileName, woList.toArray(new WordOperator[0]));
	}
	
	/**
	 * 预览生成的pdf文档(多文件合并)
	 * @param request		请求
	 * @param response		响应
	 * @param fileName		新文档名字(不带后缀名)
	 * @param woList		文档操作对象集合(合并顺序)
	 */
	public static void preview_pdf(HttpServletRequest request, HttpServletResponse response, String fileName, WordOperator... woList) throws Exception {
		List<String> tempDocList = new ArrayList<String>();
		ServletOutputStream os = null;
		try {
			// 设置响应头
			response.setContentType(MimeUtils.getByExtension("pdf") + ";charset=UTF-8");
			response.setHeader("Content-Disposition", "inline; filename=" + FileUtils.encodeFileName(FileUtils.addRandom(fileName) + ".pdf", request));
			// 一个wo对应一份文档,循环保存下来
			for (WordOperator wo : woList) {
				String tempDoc = getPath() +"/" + UUID.randomUUID().toString() + ".docx";
				wo.write(new FileOutputStream(tempDoc));
				tempDocList.add(tempDoc);
			}
			// 然后把上面保存的文档合并起来
			String tempDoc = getPath() +"/" + mergeDocx(tempDocList);
			tempDocList.add(tempDoc);
			// 再把这份合并好的文档转成pdf
			String tempPdf = getPath() +"/" + UUID.randomUUID().toString() + ".pdf";
			PDFUtils.office2PDF(tempDoc, tempPdf);
			tempDocList.add(tempPdf);
			// 将这份文档以流的形式保存到输出流中
			os = response.getOutputStream();
			FileUtils.copyFile(new File(tempPdf), os);
			os.flush();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			// 关闭输出流
			if (os != null) {
				os.close();
			}
			// 将全部临时文档删除掉
			for (String tempDoc : tempDocList) {
				FileUtils.delFile(tempDoc);
			}
		}
	}
	
	/**
	 * 合并若干个文档(按顺序合并)
	 * @param fileNames	需要合并的几个word文档全路径
	 * @return 返回合并好的文档名称,位置在DICK下
	 */
	private static String mergeDocx(List<String> fileNames) throws Exception {
		XmlOptions optionsOuter = new XmlOptions();
		optionsOuter.setSaveOuter();
		CTBody firstCTBody = null;
		XWPFDocument firstXWPFDocument = null;
		String prefix = "", firstMainPart = "", addPart = "", sufix = "";
		for (int i = 0; i < fileNames.size(); i++) {
			String fileName = fileNames.get(i);
			InputStream in = new FileInputStream(fileName);
			OPCPackage srcPackage = OPCPackage.open(in);
			XWPFDocument srcDocument = new XWPFDocument(srcPackage);
			CTBody srcBody = srcDocument.getDocument().getBody();
			if (i == 0) {
				firstXWPFDocument = srcDocument;
				firstCTBody = srcBody;
				String srcString = srcBody.xmlText();
				prefix = srcString.substring(0, srcString.indexOf(">") + 1);
				firstMainPart = srcString.substring(srcString.indexOf(">") + 1, srcString.lastIndexOf("<"));
				sufix = srcString.substring(srcString.lastIndexOf("<"));
			} else {
				String appendString = srcBody.xmlText(optionsOuter);
				addPart += appendString.substring(appendString.indexOf(">") + 1, appendString.lastIndexOf("<"));
			}
		}
		CTBody makeBody = CTBody.Factory.parse(prefix + firstMainPart + addPart + sufix);
		firstCTBody.set(makeBody);
		String tmpFileName = UUID.randomUUID().toString() + ".docx";
		OutputStream dest = new FileOutputStream(getPath() +"/" + tmpFileName);
		firstXWPFDocument.write(dest);
		return tmpFileName;
	}
	
	/**
	 * 创建当前项目绝对路径下的临时文件保存位置
	 */
	public static String getPath() throws IOException {
		File directory = new File("");
		//获取当前项目所在的绝对路径
		String courseFile = directory.getCanonicalPath() ; 
		File file=new File(courseFile+"/temp");
		//创建临时文件保存位置
		if(!file.exists()){
			file.mkdir();
		}
		String path=file.getCanonicalPath();
		return path;
	}
}

PDF文档工具类(libreOffice)

import java.io.File;
import java.net.ConnectException;

import com.artofsolving.jodconverter.DocumentConverter;
import com.artofsolving.jodconverter.openoffice.connection.OpenOfficeConnection;
import com.artofsolving.jodconverter.openoffice.connection.SocketOpenOfficeConnection;
import com.artofsolving.jodconverter.openoffice.converter.OpenOfficeDocumentConverter;
import com.grkj.Global;
import com.grkj.common.utils.StringUtils;

/**
 * PDF文档工具类
 * @author 
 */
public class PDFUtils {

	/**
	 * 连接libreOffice服务的地址和端口号
	 * 读取properties文件的配置信息
	 */
	private final static String LIBREOFFICE_URL = Global.getConfig("libreOfficeUrl");
	private final static int LIBREOFFICE_PORT = new Integer(Global.getConfig("libreOfficePort"));
	
	/**
	 * WORD转PDF方法
	 * @param sourceFile	要被转化的word文档路径(如E:\\1.docx)
	 * @param destFile		转成之后pdf文档存放路径(如E:\\1.pdf)
	 */
	public static void office2PDF(String sourceFile, String destFile) {
		try {
			File inputFile = new File(sourceFile);
			File outputFile = new File(destFile);
			if (!outputFile.getParentFile().exists()) {
				outputFile.getParentFile().mkdirs();
			}
			OpenOfficeConnection connection = new SocketOpenOfficeConnection(LIBREOFFICE_URL, LIBREOFFICE_PORT);
			connection.connect();
			DocumentConverter converter = new OpenOfficeDocumentConverter(connection);
			converter.convert(inputFile, outputFile);
			connection.disconnect();
		} catch (ConnectException e) {
			try {
				// 如果报错的是连接不上,则找到重启libreoffice5重启bat脚本,执行下
				String libreOffice5Bat = Global.getConfig("libreOffice5Bat");
				if(!StringUtils.isBlank(libreOffice5Bat)) {
					String cmd = "cmd /k start " + libreOffice5Bat;
					Runtime.getRuntime().exec(cmd);
					// 脚本里是先杀线程,过五秒,再启动程序,所以这里等六秒,再重新执行主逻辑
					Thread.sleep(6000);
					office2PDF(sourceFile, destFile);
				}
			} catch (Exception e1) {
				e1.printStackTrace();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

demo

@RequestMapping(value="/getPrintFile")
public void getDownLoadPrintFiles(HttpServletRequest request, HttpServletResponse response) throws Exception {
		//需要替换掉模板里的特殊标识数据,key要和标识字符一致
		Map<String, String> result =mapper.getList();
		//word的操作类(POI),替换特殊标识字符内容如:{name}
		WordOperator op = ExportWord.getWordOperator("模板.docx").replaceText(result);;
		//下载word文档
		ExportWord.download_word(request, response,"模板", op);
		//word的操作类(POI),替换特殊标识字符内容如:{name}
		WordOperator op = ExportWord.getWordOperator("模板.docx").replaceText(result);
		//转为pdf文档
		ExportWord.preview_pdf(request, response,"模板", op);
		}
	}