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

news2024/11/24 16:03:45

需求:某些合同,被制作成模板,以 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;
	}

	@Override
	public void write(byte[] data, int offset, int length) {
		try {
			out.write(data, offset, length);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	@Override
	public 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;
	}

	@Override
	public boolean isReady() {
		return false;
	}

	@Override
	public String toString() {
		return out.toString();
	}

	@Override
	public 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) {
				@Override
				public ServletOutputStream getOutputStream() {
					return stream;
				}

				@Override
				public 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.coloradmin.cn/o/613735.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

创新书荐|《影响力经济》如何在社交媒体上寻求可信的影响力

在这个越来越不可预测的经济体中&#xff0c;贫富悬殊、大规模裁员等让许多人都觉得踏上稳定的职业道路遥不可及&#xff0c;自由职业听起来是最好的选择。宾夕法尼亚大学研究人员Emily Hund在她的新书《影响力经济》很好地讲述了年轻创意人士如何将互联网从分散的网页集合转变…

【目标检测——YOLO系列】YOLOv1 —《You Only Look Once: Unified, Real-Time Object Detection》

YOLOv1 —《You Only Look Once: Unified, Real-Time Object Detection》 论文地址&#xff1a;1506.02640] You Only Look Once: Unified, Real-Time Object Detection (arxiv.org) 代码地址&#xff1a;pjreddie/darknet: Convolutional Neural Networks (github.com) 1、Y…

log4j 2自动配置的优先级顺序

log4j 2按照下面优先级由高到低的顺序查找使用日志的配置&#xff1a; 1、系统变量log4j2.configurationFile中指明的配置文件&#xff1b; 2、类路径上的log4j2-test.properties配置文件&#xff1b; 3、类路径上的log4j2-test.yaml 或者 log4j2-test.yml配置文件&#xff1b;…

【C++】C++中的I/O类总结——上篇

title: 【C】C-中的I/O类总结 tags: C description: ’ ’ categories: C date: 2023-06-05 00:36:59 引入 #include <iostream>int main(){std::cout<<"Hello World!"<<std::endl;}我们在学习C时&#xff0c;往往都是从上面这段程序开始的 也就…

React - Mobx

Mobx 简介 mobx是一个可以和React良好配合的集中状态管理工具&#xff0c;和Redux解决的问题相似&#xff0c;都可以独立组件进行集中状态管理 优势 简单 编写无模板的极简代码精准描述你的意图 轻松实现最优渲染 依赖自动追踪&#xff0c;实现最小渲染优化 架构自由 可…

Flutter - 一行命令解决多个pubspec.yaml文件的依赖项问题

文章目录 前言开发环境Flutter内置命令一行命令实现1. 命令使用2. 命令解释3. 命令扩展 最后 前言 项目为了模块化&#xff0c;创建了一堆Package和Plugin&#xff0c;这么做没什么问题&#xff0c;但是遇到Flutter SDK目录路径变化或者其他一些情况导致需要重新获取依赖项时就…

50+常用的广告联盟术语 (常用缩写)

广告联盟术语是指与广告联盟业务有关的行话和缩写。这些术语通常用于描述商业模型、营销策略、流量源、收益模型等方面的概念。了解广告联盟术语对于广告主、联盟会员、广告服务提供商等参与者都非常重要&#xff0c;因为它们可以帮助他们更好地理解广告联盟业务&#xff0c;提…

IDEA 2022.3.3 创建SpringBoot项目

目录 步骤01&#xff1a;快速创建项目 步骤02&#xff1a;选择依赖 步骤03&#xff1a;pom文件中版本问题 ​步骤04&#xff1a;启动测试 4.1、认识引导类 4.2、创建Controller类进行测试 可能遇到的问题及解决方案 附件1&#xff1a;pom文件源码 附件2&#xff1a;项…

华为OD机试题【食堂供餐】【2023 B卷 100分】

文章目录 &#x1f3af; 前言&#x1f3af; 题目描述&#x1f3af; 解题思路&#x1f4d9; Python代码实现&#x1f4d7; Java代码实现&#x1f4d8; C语言代码实现 &#x1f3af; 前言 &#x1f3c6; 《华为机试真题》专栏含2023年牛客网面经、华为面经试题、华为OD机试真题最…

Python使用正则表达式识别代码中的中文、英文和数字实例演示

Python 正则表达式识别代码中的中文、英文和数字 识别中文识别英文识别数字拓展 在文本处理和数据分析中&#xff0c;有时候需要从代码中提取出其中包含的中文、英文和数字信息。正则表达式是一种强大的工具&#xff0c;可以帮助我们实现这一目标。本文将分三个部分详细介绍如何…

chatgpt赋能python:Python如何倒序输出一组数

Python如何倒序输出一组数 Python是一种广泛使用的高级编程语言&#xff0c;由于其易读性和简洁性&#xff0c;Python已成为Web开发、数据分析以及人工智能等方向的首选语言。而在程序编写过程中&#xff0c;倒序输出一组数也是一个经常用到的操作。在本文中&#xff0c;我们将…

ActiveReportsJS 4.0.2 Crack ActiveReportsJS New

ActiveReportsJS - 高级 JavaScript 报告解决方案 ActiveReportsJS 是一个强大的 Web 应用程序报告工具&#xff0c;它允许开发人员和报告作者轻松地在他们的应用程序中设计和显示报告。凭借广泛的功能&#xff0c;例如向下钻取、运行时数据过滤和参数驱动的报告&#xff0c;以…

基于时间的访问控制列表(ACL)配置实验

基于时间的访问控制列表&#xff08;ACL&#xff09;配置实验 【实验目的】 掌握基于时间的ACL配置。认识给予时间的ACL的作用。验证配置。 【实验拓扑】 实验拓扑如下图所示。 设备参数如下表所示。 设备 接口 IP地址 子网掩码 默认网关 R1 S0/3/0 192.168.1.1 255…

24万字智慧城市时空信息云平台 大数据一体化 解决方案word

本资料来源公开网络&#xff0c;仅供个人学习&#xff0c;请勿商用&#xff0c;如有侵权请联系删除篇幅有限&#xff0c;无法完全展示&#xff0c;喜欢资料可转发评论&#xff0c;私信了解更多信息。 第二章 XX新型智慧城市总体设计 2.1 新型智慧城市核心技术 2.2 新型智慧城…

chatgpt赋能python:Python如何倒着循环:一步步教你倒序遍历序列

Python如何倒着循环&#xff1a;一步步教你倒序遍历序列 Python是一种高级编程语言&#xff0c;因其语法简单易学&#xff0c;常被用于数据分析、机器学习、自然语言处理等领域。在实际开发中&#xff0c;我们经常需要遍历序列。有时需要倒着循环序列&#xff0c;本文将详细介…

Roop:Colab脚本使用方法!

​AI领域人才辈出&#xff0c;突然就跳出一个大佬“s0md3v”&#xff0c;开源了一个单图就可以进行视频换脸的项目。 项目主页给了一张换脸动图非常有说服力&#xff0c;真是一图胜万言。 快速在本地配置一个环境&#xff0c;验证了一下&#xff0c;确实还不错。主要是&#xf…

使用ChatGPT生成思维导图(附永久免费镜像网址)

前言 思维导图&#xff08;The Mind Map&#xff09;&#xff0c;是表达发散性思维的有效图形思维工具。思维导图运用图文并重的技巧&#xff0c;把各级主题的关系用相互隶属与相关的层级图表现出来&#xff0c;把主题关键词与图像、颜色等建立记忆链接 &#xff0c;可以应用于…

Python-web开发学习笔记(3):CSS基础

&#x1f680; Python-web开发学习笔记系列往期文章&#xff1a; &#x1f343; Python-web开发学习笔记&#xff08;1&#xff09;--- HTML基础 &#x1f343; Python-web开发学习笔记&#xff08;2&#xff09;--- HTML基础 &#x1f343; Python-web开发学习笔记&#xff08…

网络层概述及提供的两种服务

1.网络层概述及提供的两种服务 笔记来源&#xff1a; 湖科大教书匠&#xff1a;网络层概述 湖科大教书匠&#xff1a;网络层提供的两种服务 声明&#xff1a;该学习笔记来自湖科大教书匠&#xff0c;笔记仅做学习参考 1.1 网络层概述 网络层的主要任务是实现网络互连&#xf…

Linux 高级篇-日志管理

Linux 高级篇-日志管理 基本介绍 日志文件是重要的系统信息文件&#xff0c;其中记录了许多重要的系统事件&#xff0c;包括用户的登录信息、系统的启动信息、系统的安全信息、邮件相关信息、各种服务相关信息等。日志对于安全来说也很重要&#xff0c;它记录了系统每天发生的…