简单易行的 Java 服务端生成动态 Word 文档下载

news2024/12/24 8:47:34

需求:某些合同,被制作成模板,以 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/614280.html

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

相关文章

【最新计算机、电子毕业设计 本科 大专 设计+源码】

2022年 - 2023年 最新计算机、电子毕业设计 本科 大专 设计源码 下载前必看&#xff1a;纯小白教程&#xff0c;unity两种格式资源的使用方法&#xff0c;1打开现有项目、2导入package 大专毕设源码&#xff1a;数媒专业、计算机专业、电子专业通用50多款大专毕设小游戏【源码】…

一文说清Task及其调度问题

ask对于.NET的重要性毋庸置疑。通过最近的一些面试人员经历&#xff0c;发现很多人对与Task及其调度机制&#xff0c;以及线程和线程池之间的关系并没有清晰的认识。本文采用最简单的方式模拟了Task的实现&#xff0c;旨在说明Task是什么&#xff1f;它是如何被调度执行的&…

JUC源码分析:ReentrantLock

ReentrantLock进行上锁的流程如下图所示&#xff0c;我们将按照下面的流程分析ReentrantLock上锁的流程。 先进入ReentrantLock.lock方法。 再进入内部类NonfairSync的lock方法。 点击acquire方法进入AbstractQueuedSynchronizer.acquire方法。 进入tryAcquire方法回到Reentra…

【小林计网笔记】 IP篇

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 在这里插入图片描述 一、IP 基本认识1、IP的作用2、IP与MAC的关系 二、IP 地址的基础知识1、IP地址的定义2、IP地址的分类1、ABC类地址2、DE类地址3、IP地址分类的优…

rtthread系统中lwgps软件包的使用

开发环境&#xff1a;gd32f450开发板 嵌入式操作系统&#xff1a;rtthread 软件包&#xff1a;lwgps gps定位模块&#xff1a;正点原子ATK-1218-BD lwgps软件包的介绍&#xff1a;此项目是完成开源的lwgps与rt-thread的匹配。原工程地址&#xff1a;https://github.com/MaJerle…

linux 系统-备份与恢复

linux 系统-备份与恢复 基本介绍 实体机无法做快照&#xff0c;如果系统出现异常或者数据损坏&#xff0c;后果严重&#xff0c; 要重做系统&#xff0c;还会造成数据丢失。所以我们可以使用备份和恢复技术 linux 的备份和恢复很简单&#xff0c; 有两种方式&#xff1a; 把…

论文浅尝 | 通过对比学习优化用于命名实体识别的双编码器

笔记整理&#xff1a;陆星宇&#xff0c;东南大学硕士&#xff0c;研究方向为自然语言处理 链接&#xff1a;https://arxiv.org/abs/2208.14565 动机 命名实体识别&#xff08;NER&#xff09;是识别与命名实体相关的文本片段并将其分类到预定义的实体类型&#xff08;如人物、…

MegEngine 使用小技巧:用 mperf 进行安卓 opencl 算子的 roofline 分析

前言 roofline 分析是一种简单评估当前计算任务对当前平台计算/访存能力的利用情况的方法&#xff0c;可以帮助分析算子的优化方向和优化潜力。mperf 实现了安卓 mali/adreno 两种 gpu 平台的 roofline 分析能力&#xff0c;下面以 mali 平台为例&#xff0c;简单介绍一下操作步…

【操作系统】什么是用户态和内核态?用户态和内核态是如何切换的?

【操作系统】什么是用户态和内核态&#xff1f;用户态和内核态是如何切换的&#xff1f; 参考资料&#xff1a; 用户态到内核态切换 什么是用户态和内核态&#xff1f; 「操作系统」什么是用户态和内核态&#xff1f;为什么要区分 一、什么是用户态和内核态&#xff1f; 1.1、…

案例23:基于Java宿舍管理系统设计和实现开题报告

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…

Echarts的x轴调整间隔,可以用 xAxis数组子项的axisLabel.interval

Echarts的x轴调整间隔,可以用 xAxis数组子项的axisLabel.interval https://echarts.apache.org/zh/option.html#xAxis.axisLabel.interval xAxis.axisLabel.interval auto 默认值是’auto’ 可设置为 : number 或 Function 数字或函数 坐标轴刻度标签的显示间隔&#xff0c;…

逍遥自在学C语言 | for循环详解

前言 C语言中的循环结构时&#xff0c;for循环是最常用的一种。它允许重复执行一段代码&#xff0c;直到满足特定条件为止。 本文将详细介绍for循环的用法&#xff0c;并提供相关的可编译运行的C代码示例。 一、人物简介 第一位闪亮登场&#xff0c;有请今后会一直教我们C语…

生产·成本

短期生产成本 给定生产技术&#xff0c;管理者必须选择如何生产&#xff08;即使用什么样的要素组合&#xff09;使生产成本最小。 衡量成本 边际成本 长期成本与规模 长期平均成本&#xff08;LAC&#xff09;和长期边际成本&#xff08;LMC&#xff09; 规模经济 随着产…

【Web网站服务】Apache网页优化

Apache网页优化 一、网页压缩1.1网页压缩步骤 二、网页缓存三、隐藏版本信息五、Apache防盗链 一、网页压缩 在企业中&#xff0c;部署Apache后只采用默认的配置参数&#xff0c;会引发网站很多问题&#xff0c;换言之默认配置是针对以前较低的服务器配置的&#xff0c;以前的…

chatgpt赋能python:Python如何拆分list

Python如何拆分list 在Python编程中&#xff0c;我们经常需要对list进行拆分操作。拆分list可以帮助我们更好地处理数据&#xff0c;以便进一步进行分析、计算或可视化呈现。本文将介绍Python中如何拆分list&#xff0c;并且提供一些常用的拆分方式。 按固定大小进行拆分 我…

【AIGC】12、DINO | 针对 DETR-like 检测器的提升

文章目录 一、背景二、方法2.1 Contrastive DeNoising Training2.3 Mixed Query Selection2.4 Look Forward Twice 三、效果 论文&#xff1a;DINO: DETR with Improved DeNoising Anchor Boxes for End-to-End Object Detection 代码&#xff1a;https://github.com/IDEACVR/…

【JUC基础】15. Future模式

目录 1、前言 2、什么是Future 2.1、传统程序调用 3、JDK中的Future 3.1、Future相关API 3.2、FutureTask 3.2.1、FutureTask实现 3.2.2、FutureTask相关API 3.3、CompletableFuture 3.3.1、thenApply 3.3.2、异步任务编排之thenCompose() 3.3.3、异步任务编排之th…

南京市某高校计算机科学与技术专业性能测试与Loadrunner—考试试卷分析

XXX科技学院试卷 20 /20 学年 第 学期 课程所属部门&#xff1a; 课程名称&#xff1a; 课程编号&#xff1a; 考试方式&#xff1a;&#xff08;A、B、开、闭&#xff09;卷 使用班级&#xff1a; …

数据安全--16--数据采集阶段安全防护措施

本博客地址&#xff1a;https://security.blog.csdn.net/article/details/131033616 一、引子 数据安全采集阶段的防护措施主要是从三个方面来开展的&#xff0c;第一个是从个人数据主体采集方面&#xff0c;第二个是从外部机构采集方面&#xff0c;以上两个方面基本涵盖了数…

读改变未来的九大算法笔记05_数字签名

1. 数字签名”&#xff08;Digital Signature&#xff09; 1.1. 单词数字化&#xff08;digital&#xff09;意味着其“由数字字符串组成” 1.2. 任何数字化的东西都能被拷贝 1.3. “签名”的全部意义在于能被读取&#xff0c;但不能被除了作者的任何人拷贝&#xff08;也就…