Java 服务端生成动态 Word 文档下载

news/2024/11/24 9:23:54/

需求:某些合同,被制作成模板,以 Word 格式保存,输入相关的内容参数最终生成 Word 文档下载。这是企业级应用中很常见的需求。

解决方案:无非是模板技术,界定不变和变的内容,预留插值的标记,替换为期待的最终内容。Office Word 2003 版本以上,Word 可以以 XML 文本格式存储,——只有是文本格式才使得我们这项模板技术成为可能。例如下面一个简单的 Word 文档的结构。

<?xml version="1.0"?>
<w:wordDocument xmlns:w="http://schemas.microsoft.com/office/word/2003/wordml"><w:body><w:p><w:r><w:t>Hello World.</w:t></w:r></w:p></w:body>
</w:wordDocument>

这个一个非常简单典型的 Word XML 结构,我们可以看到它具有 XML 文件的声明和命名空间。以 <w: 开始的标签则表示了其中为 Word 中的内容,不同的 bodyprt 等等属性方法表明了文档中不同的格式。可以用记事本创建一个文件,将上面的 XML 内容粘贴,并保存为 helloworld.xml,在Office Word中打开它,就能看到如上图所示的内容。

对此业界中常见的具体解决方案有:

  • Apache POI,也是通过 XML 操控技术来对 Word 文档编辑的。这是大多数人使用的方案,但文本并采用这方案
  • 利用后端的模板引擎技术,如 Freemarker 等。既然无非是模板,那么复用 Web MVC 上的模板技术理应没问题的,而现实中确实不少人那么做,完全可以不依赖 Web,只作纯粹的模板引擎,解析一切文本的模板。同时,那样就不用依赖 Apache POI 了。本文也是基于该原理,但不是基于 Freemark,而是传统的 JSP,那样的话连 Freemarker 都不用依赖了,更简单、轻量级
  • 前端生成 Word 文档。后端提供内容数据和 Word 文档,让前端完成模板替换。这个在前端的技术好像不太靠谱,还是得要后端来完成比较好,参见我转载的文章《原来,这才是 HTML+CSS 导出 Word 最佳方式!》

总之,整个过程可以简述为:先制作一份 word 文档,预留好模板的插值符,然后让后台识别 word 为 jsp 文件(需改后缀名为 .jsp)。然后输入内容数据,让 Servlet JSP 解析、替换模板,最后一步,劫持 Servlet 输出流(ServletOutputStream),不是返回到前端的 Response,而是文件流2,保存到服务器的磁盘文件上,然后告诉前端可以下载该文件。

例如下面截图,直接便是在 Word 编辑插值符,如 ${xxxx}

在这里插入图片描述
我们定义的占位符是 ${placeholder} 格式,实际上这是 EL 表达式;除了这个还有 JSP <%……%> 也是支持的。

模板的几个问题:

  • 将 Word 模板文件另存为 XML 格式,能够发现部分占位符不是作为一个整体存在,而是被分割到了不同的标签中,这样的话我们在处理文档对象、遍历文本的时候,就无法将其作为一个整体进行替换了。对此我们可以直接编辑 XML 文件,将被分割的占位符放到同一个 <w:t> 标签中
  • 如果需求需要插入图片也简单,在模板中找到图片标签的位置,然后将图片转成 base64 的字符串替换就好了
  • 模板虽然是 docx 格式的,但给 Sevlet 解析 JSP 就必须是 .jsp 后缀名了,要改下名

其中关键的技术点是 JSP 输出的“劫持”,不是输出到浏览器响应,而是保存到文件。达成这一技术点的是 ServletOutputStream 几个 write() 方法,我们可以继承父类 ServletOutputStream 覆盖 write() 方法来完成我们希望的逻辑。实际上笔者之前做过的代码生成器,就是使用这种技术的。

完整 ByteArrayServletOutputStream 类如下:

import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;/*** 自定义响应对象的输出流* * @author sp42 frank@ajaxjs.com**/
public class ByteArrayServletOutputStream extends ServletOutputStream {/*** 创建一个 ByteArrayServletOutputStream 对象*/public ByteArrayServletOutputStream() {}/*** 输出流*/private OutputStream out = new ByteArrayOutputStream();/*** * 创建一个 ByteArrayServletOutputStream 对象* * @param out 输出流*/public ByteArrayServletOutputStream(ByteArrayOutputStream out) {this.out = out;}@Overridepublic void write(byte[] data, int offset, int length) {try {out.write(data, offset, length);} catch (IOException e) {e.printStackTrace();}}@Overridepublic void write(int b) throws IOException {out.write(b);}/*** * @param _out*/public void writeTo(OutputStream _out) {ByteArrayOutputStream bos = (ByteArrayOutputStream) out;try {bos.writeTo(_out);} catch (IOException e) {e.printStackTrace();}}public OutputStream getOut() {return out;}@Overridepublic boolean isReady() {return false;}@Overridepublic String toString() {return out.toString();}@Overridepublic void setWriteListener(WriteListener writeListener) {}/*** 解析 JSP 模板到服务器磁盘上* * @param req* @param resp* @param tplJsp* @param saveTo*/public static void toDisk(HttpServletRequest req, HttpServletResponse resp, String tplJsp, String saveTo) {RequestDispatcher rd = req.getServletContext().getRequestDispatcher(tplJsp);try (ByteArrayServletOutputStream stream = new ByteArrayServletOutputStream();PrintWriter pw = new PrintWriter(new OutputStreamWriter(stream.getOut(), "UTF-8"));OutputStream out = new FileOutputStream(saveTo);) {rd.include(req, new HttpServletResponseWrapper(resp) {@Overridepublic ServletOutputStream getOutputStream() {return stream;}@Overridepublic PrintWriter getWriter() {return pw;}});pw.flush();stream.writeTo(out);} catch (IOException | ServletException e) {e.printStackTrace();}}
}

可见只有区区 120行代码即可完成,根本不需要 Apache POI、Freemarker “劳师动众”。

调用例子,我们写一个 Servlet 来测试下:

import java.io.IOException;import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;/*** Servlet implementation class DownloadContract*/
@WebServlet("/DownloadContract")
public class DownloadContract extends HttpServlet {private static final long serialVersionUID = 1L;/*** @see HttpServlet#HttpServlet()*/public DownloadContract() {super();}/*** @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse*      response)*/protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {HttpServletRequest req = (HttpServletRequest) request;HttpServletResponse resp = (HttpServletResponse) response;req.setAttribute("foo", 88888888); // 内容数据ByteArrayServletOutputStream.toDisk(req, resp, "/doc_tpl.jsp", "c:\\temp\\s.docx");response.getWriter().append("Served at: ").append(request.getContextPath());}
}

换成 Spring Boot 也是差不多的。至于最终下载的代码,这里就不给了,读者可以自行补上。最后,分享两个相关的开源项目,挺有意思的:

  • 如何用800行代码实现类似poi-tl的可视化Word模板
  • WordGO - 让Java生成word文档更容易

http://www.ppmy.cn/news/235140.html

相关文章

VC++ 线程同步之事件对象(event)

VC 线程同步之事件对象&#xff08;event&#xff09; Event是windows操作系统的一种内核对象&#xff0c;它可以用于进程间同步和线程间同步。 Event 方式是最具弹性的同步机制&#xff0c;因为他的状态完全由你去决定&#xff0c;不会像 Mutex 和 Semaphores 的状态会由类似…

VC++ 线程同步之互斥对象(mutex)

VC 线程同步之互斥对象&#xff08;mutex&#xff09; Mutex是内核对象&#xff0c;陷入内核时间性能相对较差&#xff08;与Critical Section相比&#xff09; 互斥量内核对象能够确保一个进程独占对一个资源的访问。互斥量与关键段&#xff08;线程同步方式&#xff09;的行…

Cisco wireless 802.1x认证

目录 一、实验环境二、WLC 9800配置&#xff1a;2.1 配置radius server和group 2.2 配置AAA&#xff1a;2.3 配置WLAN&#xff1a;2.4 配置policy&#xff1a;2.5 配置 policy tag&#xff08;关联wlan和policy&#xff09;&#xff1a;2.6 将tag关联至AP&#xff1a; 三、测试…

电子计算机X线体层摄影,电子计算机X线体层摄影检查诊断乳腺肿块的价值

上海医学1991年 T月(第14卷第 T期) 395 电子计算机X线体层摄影检查诊断乳腺肿块的价值 上海医科大学附属华山医院葺射 茎霎 沈天真陈克教 提要 本文报告了 100例女性乳腺肿块的电子计算机x线体层摄影(cT)检查资料&#xff0c;包括 5O佛 乳腺癌和 矗0侧各类乳腺良性病变&#xf…

5900x matlab,芯片工程师入手5900X平台,简单自用工具评测

芯片工程师入手5900X平台,简单自用工具评测 2021-02-15 22:13:17 23点赞 31收藏 37评论 如何才能快速换一种生活方式?参加#牛年Flag#征稿活动,征集你2021年的购物学习生活计划!>>点击查看活动详情< 创作立场声明:本文所测商品为自费购买并作为生产使用。坚持独立…

计算机知识讲稿,计算机基础知识讲稿.ppt

计算机基础知识讲稿.ppt 1 第一讲计算机基础知识 一 什么是计算机计算机 是一种用于存储和处理信息的通用机器 2 第一讲计算机基础知识 二 计算机发展史1946年2月 第一台计算机诞生在美国 ENIAC 用了18800个电子管 30吨重 耗电150KW 占地160平方米 运算速度 5000次 秒 ENIAC存储…

关于计算机图像基础知识的整理

1.色彩深度&#xff1a; 1位&#xff1a;2种颜色&#xff0c;单色光&#xff0c;黑白二色&#xff0c;用于compact Macintoshes。 2位&#xff1a;4种颜色&#xff0c;CGA&#xff0c;用于gray-scale早期的NeXTstation及color Macintoshes。 3位&#xff1a;8种颜色&#xff…

大学计算机基础笔记

目录 第一章&#xff1a;计算机基础 第二章 操作系统Windows7 第三章&#xff1a;Word 第四章 Excel电子表格软件 第五章 演示文稿软件 第六章 计算机网络 第7章 多媒体技术 标题第一章&#xff1a;计算机基础 1.1.1计算机诞生 1.世界上第一台电子计算机(数字积分计算机)…