当前位置: 首页 > news >正文

【spring boot 使用apache poi 生成和处理word 文档】

导入maven依赖

<!-- pom.xml -->
<properties><java.version>8</java.version><poi.version>5.2.4</poi.version>
</properties><dependencies><!-- Spring Boot Starters --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- Apache POI 基础包 --><dependency><groupId>org.apache.poi</groupId><artifactId>poi</artifactId><version>${poi.version}</version></dependency><!-- Apache POI for Word / PPT / Excel 核心依赖--><dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxml</artifactId><version>${poi.version}</version></dependency><!-- 可选:如果需要处理旧版Word文档(.doc) --><dependency><groupId>org.apache.poi</groupId><artifactId>poi-scratchpad</artifactId><version>${poi.version}</version></dependency><!-- 测试 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
</dependencies>

Controller

package com.example.wordservice;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.io.IOException;@RestController
@RequestMapping("/api/word")
public class WordController {@Autowiredprivate WordDocumentService wordService;@GetMapping("/download")public ResponseEntity<InputStreamResource> downloadWordDocument() throws IOException {return wordService.createSimpleDocument();}
}

Service

package com.example.wordservice;import org.apache.poi.xwpf.usermodel.*;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;@Service
public class WordDocumentService {/*** 创建简单的Word文档*/public ResponseEntity<InputStreamResource> createSimpleDocument() throws IOException {// 创建Word文档//每一份word文档底层都是XML//XWPF的全称是:XML Word Processing FormatXWPFDocument document = new XWPFDocument();// 创建段落XWPFParagraph title = document.createParagraph();title.setAlignment(ParagraphAlignment.CENTER);//创建段落中的文本运行单元//只有文本运行单元才会存文本XWPFRun titleRun = title.createRun();titleRun.setText("项目报告");//设置样式:字体大小、颜色、斜体、等//是对底层API的封装,封装为高级APItitleRun.setBold(true);titleRun.setFontSize(16);// 创建第二个段落XWPFParagraph content = document.createParagraph();content.setAlignment(ParagraphAlignment.LEFT);content.setSpacingBefore(200);XWPFRun contentRun = content.createRun();contentRun.setText("这是使用Spring Boot和Apache POI生成的Word文档。");// 转换为字节流//ByteArrayOutputStream 无需关闭ByteArrayOutputStream out = new ByteArrayOutputStream();document.write(out);//可以改为try-with-resourcedocument.close();//ByteArrayInputStream 无需关闭ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());// 设置HTTP响应头HttpHeaders headers = new HttpHeaders();headers.add("Content-Disposition", "attachment; filename=report.docx");return ResponseEntity.ok().headers(headers).contentType(MediaType.APPLICATION_OCTET_STREAM).body(new InputStreamResource(in));}
}

复制文本运行

   /*** 文本运行是Word文档中最基本的文本格式单位。它代表一段具有相同格式的连续文本。* 复制文本运行的格式(字体名称和大小、字体颜色、粗体、斜体、下划线、上下标、背景色、字符间距等等)* 这个方法就是专门复制一个Run的所有格式属性* @param sourceRun 源文本运行* @param targetRun 目标文本运行*/private static void copyRunFormatting(XWPFRun sourceRun, XWPFRun targetRun) {// 直接复制整个格式属性对象【底层级别的复制】//CTR 即:Content Run 底层叫法,高级叫法是XWPFRun ,二者等价//CTRPr  即:Content Run Properties 文本运行属性,即样式CTRPr sourceRPr = sourceRun.getCTR().getRPr();if (sourceRPr != null) {CTR targetCTR = targetRun.getCTR();CTRPr targetRPr = targetCTR.isSetRPr() ? targetCTR.getRPr() : targetCTR.addNewRPr();targetRPr.set(sourceRPr.copy());}}     

跨文档复制时,对文本中的指定文字进行标红

 /*** 快速标红方法*/public static void quickHighlight(XWPFParagraph newPara, String text, XWPFRun sourceRun) {// 定义关键词和替换模式String[] keywords = {"中共", "党中央/国务院"};String processedText = text;for (String keyword : keywords) {// 用特殊标记包围关键词,便于后续处理processedText = processedText.replace(keyword, "§RED§" + keyword + "§END§");}// 分割处理String[] parts = processedText.split("§RED§|§END§");for (int i = 0; i < parts.length; i++) {if (parts[i].isEmpty()) continue;XWPFRun newRun = newPara.createRun();copyRunFormatting(sourceRun, newRun);// 设置基本格式// newRun.setFontFamily("仿宋");//设置字体大小为16pt//newRun.setFontSize(16);// 奇数索引是标红文本(因为分割后格式:普通文本,标红文本,普通文本, ...)if (i % 2 == 1) {//设置为红色newRun.setColor("FF0000");//newRun.setBold(true);//设置加粗}//设置文本if(parts[i].startsWith("(")){addIndentAndSetText(newRun,parts[i]);}else{newRun.setText(parts[i]);}//如果是最后一个//添加换行if(i==parts.length-1){// 设置整个段落左缩进两个字符//run没有设置缩进的方法// newPara.setIndentationLeft(400);  // 左缩进400单位 ≈ 两个字符//添加换行newRun.addBreak();}}}

📚 InputStreamResource 详解

InputStreamResource 是 Spring Framework 中的一个类,用于将 输入流(InputStream) 包装成 Spring 的 Resource 对象,便于在 Web 响应中返回文件数据。

🔍 核心概念

1. 什么是 InputStreamResource?

// InputStreamResource 是 Spring 对 InputStream 的包装
public class InputStreamResource extends AbstractResource {private final InputStream inputStream;// 它包装了一个输入流,使其可以作为 Resource 返回
}

2. 在文件下载中的角色

@GetMapping("/download")
public ResponseEntity<InputStreamResource> downloadWordDocument() throws IOException {// 1. 生成Word文档到字节数组ByteArrayOutputStream out = new ByteArrayOutputStream();XWPFDocument document = createDocument();document.write(out);document.close();// 2. 创建输入流ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());// 3. 包装成 InputStreamResourceInputStreamResource resource = new InputStreamResource(in);return ResponseEntity.ok().header("Content-Disposition", "attachment; filename=document.docx").body(resource);
}

🎯 为什么使用 InputStreamResource?

与传统方式的对比

传统方式
// 方式1:直接写入HttpServletResponse
@GetMapping("/download")
public void download(HttpServletResponse response) throws IOException {response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");response.setHeader("Content-Disposition", "attachment; filename=document.docx");XWPFDocument document = createDocument();document.write(response.getOutputStream());document.close();
}// 方式2:先保存到临时文件
@GetMapping("/download")
public ResponseEntity<FileSystemResource> download() throws IOException {File file = File.createTempFile("document", ".docx");//自行使用try-with-resourceFileOutputStream out = new FileOutputStream(file);XWPFDocument document = createDocument();document.write(out);document.close();out.close();//file.delete();return ResponseEntity.ok().body(new FileSystemResource(file));
}
Spring推荐方式(使用InputStreamResource)
// 方式3:使用InputStreamResource(内存操作,性能好)
@GetMapping("/download")
public ResponseEntity<InputStreamResource> download() throws IOException {ByteArrayOutputStream out = new ByteArrayOutputStream();XWPFDocument document = createDocument();document.write(out);document.close();ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());InputStreamResource resource = new InputStreamResource(in);return ResponseEntity.ok().header("Content-Disposition", "attachment; filename=document.docx").contentType(MediaType.APPLICATION_OCTET_STREAM).body(resource);
}

🔧 详细工作流程

完整的数据流

public class WordDocumentService {public ResponseEntity<InputStreamResource> generateReport() throws IOException {// 📝 1. 创建Word文档(内存中)XWPFDocument document = new XWPFDocument();document.createParagraph().createRun().setText("报告内容");// 💾 2. 写入字节输出流ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();document.write(byteArrayOutputStream);document.close();// 🔄 3. 转换为输入流ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());// 📦 4. 包装为Spring ResourceInputStreamResource resource = new InputStreamResource(byteArrayInputStream);// 🌐 5. 构建HTTP响应HttpHeaders headers = new HttpHeaders();headers.add("Content-Disposition", "attachment; filename=report.docx");headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);return ResponseEntity.ok().headers(headers).contentLength(byteArrayOutputStream.size()) // 可选:设置内容长度.body(resource);}
}

📊 与其他Resource类型的对比

Resource类型适用场景优点缺点
InputStreamResource动态生成的文件
内存中的数据
无需临时文件
性能好
数据需完全加载到内存
FileSystemResource已存在的文件
大文件下载
支持大文件
内存占用小
需要创建临时文件
ByteArrayResource小文件
已知数据
简单直接所有数据在内存中
ClassPathResource资源文件
模板文件
从classpath读取只读,不能修改

🛠️ 实际应用示例

示例1:动态Word报告

@RestController
@RequestMapping("/api/reports")
public class ReportController {@Autowiredprivate ReportService reportService;@GetMapping("/word")public ResponseEntity<InputStreamResource> generateWordReport(@RequestParam String reportType,@RequestParam String startDate,@RequestParam String endDate) throws IOException {// 生成报告数据ReportData data = reportService.getReportData(reportType, startDate, endDate);// 创建Word文档ByteArrayOutputStream out = new ByteArrayOutputStream();try (XWPFDocument document = createWordReport(data)) {document.write(out);}// 创建响应ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());String filename = String.format("%s报告_%s_%s.docx", reportType, startDate, endDate);return ResponseEntity.ok().header("Content-Disposition", "attachment; filename=" + filename).contentType(MediaType.APPLICATION_OCTET_STREAM).body(new InputStreamResource(in));}private XWPFDocument createWordReport(ReportData data) {XWPFDocument document = new XWPFDocument();// 构建Word文档内容...return document;}
}

示例2:模板填充下载

@Service
public class TemplateService {public ResponseEntity<InputStreamResource> fillTemplate(Map<String, String> data) throws IOException {// 1. 读取模板文件ClassPathResource templateResource = new ClassPathResource("templates/report-template.docx");// 2. 处理模板ByteArrayOutputStream out = new ByteArrayOutputStream();try (XWPFDocument document = new XWPFDocument(templateResource.getInputStream())) {//todo 实现自己的填充逻辑...fillTemplateData(document, data);document.write(out);}// 3. 返回结果ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());return ResponseEntity.ok().header("Content-Disposition", "attachment; filename=filled-report.docx").contentType(MediaType.APPLICATION_OCTET_STREAM).body(new InputStreamResource(in));}
}

⚠️ 注意事项

1. 资源清理

// Spring会自动管理InputStreamResource的资源清理
// 但最好确保你的InputStream是可关闭的
public ResponseEntity<InputStreamResource> safeDownload() throws IOException {ByteArrayOutputStream out = new ByteArrayOutputStream();try (XWPFDocument document = createDocument()) { // 使用try-with-resourcesdocument.write(out);}ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());// ByteArrayInputStream不需要显式关闭,但这是个好习惯return ResponseEntity.ok().body(new InputStreamResource(in));
}

2. 大文件处理

// 对于大文件,考虑使用FileSystemResource避免内存溢出
public ResponseEntity<Resource> downloadLargeFile() throws IOException {File tempFile = File.createTempFile("large-document", ".docx");try (FileOutputStream out = new FileOutputStream(tempFile);XWPFDocument document = createLargeDocument()) {document.write(out);}// 使用FileSystemResource,支持大文件FileSystemResource resource = new FileSystemResource(tempFile);// 设置响应完成后删除临时文件return ResponseEntity.ok().header("Content-Disposition", "attachment; filename=large-document.docx").body(resource);
}

💡 总结

返回 InputStreamResource 的含义:

  1. 包装动态数据:将内存中生成的Word文档包装成可下载的资源
  2. Spring标准做法:符合Spring的Resource抽象,便于统一处理
  3. 无需临时文件:所有操作在内存中完成,性能更好
  4. 自动资源管理:Spring框架负责关闭流和清理资源
  5. 灵活的HTTP响应:可以方便地设置文件名、Content-Type等头部信息

简单来说: InputStreamResource 让动态生成的文件数据能够以 流式方式 返回给客户端,同时享受Spring框架的资源管理便利性。

📊 两种 ContentType 的详细区别

🔍 核心概念对比

特性APPLICATION_OCTET_STREAMWordprocessingML Document
类型通用二进制流特定文件类型
含义“这是一个二进制文件,具体类型未知”“这是一个Word 2007+文档”
使用场景通用文件下载
类型不确定的文件
明确的Word文档下载
浏览器行为总是触发下载可能尝试预览(如果支持)

🎯 具体区别分析

1. MediaType.APPLICATION_OCTET_STREAM

// 通用二进制流类型
Content-Type: application/octet-stream// 浏览器行为:总是下载
// 用途:当服务器不知道文件确切类型,或希望强制下载时使用

2. WordprocessingML Document

// 具体的Word文档类型  
Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document// 浏览器行为:可能尝试预览(如Edge、Chrome)
// 用途:明确告诉浏览器这是Word文档

🌐 浏览器行为差异

测试示例

@RestController
public class DownloadController {// 方式1:使用通用二进制流@GetMapping("/download-generic")public ResponseEntity<InputStreamResource> downloadGeneric() {// 浏览器:总是弹出下载对话框return ResponseEntity.ok().header("Content-Disposition", "attachment; filename=document.docx").contentType(MediaType.APPLICATION_OCTET_STREAM)  // 强制下载.body(resource);}// 方式2:使用具体Word类型@GetMapping("/download-specific")public ResponseEntity<InputStreamResource> downloadSpecific() {// 浏览器:可能直接在线打开(如果支持)return ResponseEntity.ok().header("Content-Disposition", "attachment; filename=document.docx").contentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.wordprocessingml.document"))  // 具体类型.body(resource);}// 方式3:使用具体类型但强制下载@GetMapping("/download-specific-force")public ResponseEntity<InputStreamResource> downloadSpecificForce() {// 浏览器:明确类型但仍强制下载return ResponseEntity.ok().header("Content-Disposition", "attachment; filename=document.docx")  // attachment强制下载.contentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.wordprocessingml.document")).body(resource);}// 方式4:使用具体类型允许预览@GetMapping("/preview")public ResponseEntity<InputStreamResource> preview() {// 浏览器:可能尝试在线预览return ResponseEntity.ok().header("Content-Disposition", "inline; filename=document.docx")  // inline允许预览.contentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.wordprocessingml.document")).body(resource);}
}

🔧 实际应用场景

场景1:通用文件下载服务

@Service
public class FileDownloadService {/*** 通用文件下载 - 不确定文件类型时使用*/public ResponseEntity<InputStreamResource> downloadFile(byte[] fileData, String filename) {// 当不知道具体文件类型,或希望总是触发下载时return ResponseEntity.ok().header("Content-Disposition", "attachment; filename=" + filename).contentType(MediaType.APPLICATION_OCTET_STREAM)  // 通用类型.body(new InputStreamResource(new ByteArrayInputStream(fileData)));}/*** 特定类型文件下载 - 知道确切类型时使用*/public ResponseEntity<InputStreamResource> downloadWordDocument(byte[] fileData, String filename) {// 明确知道这是Word文档,希望浏览器能正确识别return ResponseEntity.ok().header("Content-Disposition", "attachment; filename=" + filename).contentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.wordprocessingml.document"))  // 具体类型.body(new InputStreamResource(new ByteArrayInputStream(fileData)));}
}

场景2:智能内容类型选择

public class ContentTypeResolver {/*** 根据文件扩展名智能选择ContentType*/public static MediaType resolveContentType(String filename) {if (filename == null) {return MediaType.APPLICATION_OCTET_STREAM;}String extension = getFileExtension(filename).toLowerCase();switch (extension) {case "docx":return MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");case "doc":return MediaType.parseMediaType("application/msword");case "xlsx":return MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");case "pdf":return MediaType.parseMediaType("application/pdf");case "txt":return MediaType.TEXT_PLAIN;default:return MediaType.APPLICATION_OCTET_STREAM;  // 默认通用类型}}/*** 创建下载响应(智能ContentType)*/public static ResponseEntity<InputStreamResource> createDownloadResponse(byte[] data, String filename, boolean forceDownload) {MediaType contentType = resolveContentType(filename);String contentDisposition = forceDownload ? "attachment; filename=\"" + filename + "\"" : "inline; filename=\"" + filename + "\"";return ResponseEntity.ok().header("Content-Disposition", contentDisposition).contentType(contentType).contentLength(data.length).body(new InputStreamResource(new ByteArrayInputStream(data)));}private static String getFileExtension(String filename) {return filename.substring(filename.lastIndexOf(".") + 1);}
}

📋 完整的Office文件类型映射

public class OfficeMediaTypes {// Word文档public static final MediaType WORD_DOCX = MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");public static final MediaType WORD_DOC = MediaType.parseMediaType("application/msword");// Excel文档public static final MediaType EXCEL_XLSX = MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");public static final MediaType EXCEL_XLS = MediaType.parseMediaType("application/vnd.ms-excel");// PowerPoint文档public static final MediaType POWERPOINT_PPTX = MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.presentationml.presentation");public static final MediaType POWERPOINT_PPT = MediaType.parseMediaType("application/vnd.ms-powerpoint");/*** 获取推荐的ContentType配置*/public static ContentTypeConfig getConfig(String filename, boolean forceDownload) {MediaType mediaType = resolveMediaType(filename);String disposition = forceDownload ? "attachment" : "inline";return new ContentTypeConfig(mediaType, disposition);}public static class ContentTypeConfig {public final MediaType mediaType;public final String disposition;public ContentTypeConfig(MediaType mediaType, String disposition) {this.mediaType = mediaType;this.disposition = disposition;}}
}

⚠️ 注意事项

1. 浏览器兼容性考虑

public class DownloadStrategy {/*** 安全的内容类型策略*/public MediaType getSafeContentType(String userAgent, String filename) {// 检测老旧浏览器if (isLegacyBrowser(userAgent)) {// 老旧浏览器可能不认识具体的Office类型,使用通用类型更安全return MediaType.APPLICATION_OCTET_STREAM;}// 现代浏览器使用具体类型return OfficeMediaTypes.resolveMediaType(filename);}/*** 根据场景选择最佳策略*/public ResponseEntity<InputStreamResource> createOptimalResponse(byte[] data, String filename, DownloadContext context) {MediaType contentType;if (context.isForceDownload()) {// 场景:明确要求下载 → 使用通用类型确保下载contentType = MediaType.APPLICATION_OCTET_STREAM;} else if (context.isKnownOfficeFile(filename)) {// 场景:已知Office文件且允许预览 → 使用具体类型contentType = OfficeMediaTypes.resolveMediaType(filename);} else {// 场景:未知文件类型 → 使用通用类型contentType = MediaType.APPLICATION_OCTET_STREAM;}return buildResponse(data, filename, contentType, context.isForceDownload());}
}

2. 实际项目推荐

// 对于Word文档生成项目,推荐这样做:
@GetMapping("/download-report")
public ResponseEntity<InputStreamResource> downloadReport() {byte[] documentData = generateWordReport();// 最佳实践:具体类型 + 强制下载return ResponseEntity.ok().header("Content-Disposition", "attachment; filename=report.docx")  // 强制下载.contentType(MediaType.parseMediaType(                              // 具体类型"application/vnd.openxmlformats-officedocument.wordprocessingml.document")).body(new InputStreamResource(new ByteArrayInputStream(documentData)));
}

💡 总结与建议

选择策略:

使用 APPLICATION_OCTET_STREAM 当:

  • 文件类型不确定
  • 希望强制下载(不预览)
  • 兼容老旧浏览器
  • 通用文件下载服务

使用具体类型当:

  • 明确知道文件类型
  • 希望浏览器能正确识别
  • 现代浏览器环境
  • 可能希望在线预览的场景

最终推荐:

对于你的Word文档生成项目,推荐使用具体类型

.contentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.wordprocessingml.document"))

这样既能正确标识文件类型,又通过attachment确保下载行为。

http://www.dtcms.com/a/552861.html

相关文章:

  • 橙米网站建设做网站查询违章
  • AI用于自动化办公指南
  • 从“手动试错”到“自动化闭环”:实车OTA测试的效率革命
  • 智能电器:重构生活的科技力量
  • OpenAI拟借AI估值重构浪潮冲击1.1万亿美元IPO——基于市场情绪因子与估值量化模型的深度分析
  • redis事务与Lua脚本
  • 【技术选型】前端框架:Vue vs React - 组合式API与Hooks的哲学之争
  • 网站建设网网站建设全网营销客户资源
  • Python 数据可视化:用 Matplotlib 绘制多维度对比图表
  • 【axf文件解析与J-Link通讯实战(五)】PySide6图形界面与数据可视化集成
  • Android 权限管理:适配 Android 14 运行时权限申请(含后台定位)
  • 涡阳网站优化wordpress进入后台空白
  • 【实战案例】火语言 RPA 采集小说站已完结书名(自动翻页判断),保存到Excel 全流程(附完整脚本)
  • 基于微信小程序的背单词系统x1o5sz72(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
  • 力扣hot100-------11、盛最多水的容器(java版)
  • Visual Basic 菜单编辑器
  • 本地部署轻量级持续集成工具 Drone CI 并实现外部访问
  • gitlab-ci中cicd+helm实现devops自动化部署到k8s
  • 网站欢迎页面怎么做江门建站公司模板
  • 评论回复网站怎么做郑州百度搜索优化
  • Rust开发之使用derive宏自动实现Trait(Clone、Debug)
  • 15. setState的更新是异步的吗
  • Qwen2.5-VL开源,斩获多榜单冠军!
  • Prometheus和Grafana简介
  • 基于深度学习的医疗器械分类编码映射系统:设计篇
  • Rust开发之自定义错误类型(实现Error trait)
  • 【Java Web学习 | 第三篇】CSS(2) - 元素显示模式
  • 10月31日
  • Mybatis-Plus实现MySQL分表
  • 兵团住房和城乡建设局网站网站设计标杆企业