导入maven依赖
xml
<!-- 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
java
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 {
@Autowired
private WordDocumentService wordService;
@GetMapping("/download")
public ResponseEntity<InputStreamResource> downloadWordDocument() throws IOException {
return wordService.createSimpleDocument();
}
}
Service
java
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 Format
XWPFDocument document = new XWPFDocument();
// 创建段落
XWPFParagraph title = document.createParagraph();
title.setAlignment(ParagraphAlignment.CENTER);
//创建段落中的文本运行单元//只有文本运行单元才会存文本
XWPFRun titleRun = title.createRun();
titleRun.setText("项目报告");
//设置样式:字体大小、颜色、斜体、等
//是对底层API的封装,封装为高级API
titleRun.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-resource
document.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));
}
}
复制文本运行
java
/**
* 文本运行是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());
}
}
跨文档复制时,对文本中的指定文字进行标红
java
/**
* 快速标红方法
*/
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?
java
// InputStreamResource 是 Spring 对 InputStream 的包装
public class InputStreamResource extends AbstractResource {
private final InputStream inputStream;
// 它包装了一个输入流,使其可以作为 Resource 返回
}
2. 在文件下载中的角色
java
@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. 包装成 InputStreamResource
InputStreamResource resource = new InputStreamResource(in);
return ResponseEntity.ok()
.header("Content-Disposition", "attachment; filename=document.docx")
.body(resource);
}
🎯 为什么使用 InputStreamResource?
与传统方式的对比
传统方式
java
// 方式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-resource
FileOutputStream 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)
java
// 方式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);
}
🔧 详细工作流程
完整的数据流
java
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 Resource
InputStreamResource 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报告
java
@RestController
@RequestMapping("/api/reports")
public class ReportController {
@Autowired
private 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:模板填充下载
java
@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. 资源清理
java
// Spring会自动管理InputStreamResource的资源清理
// 但最好确保你的InputStream是可关闭的
public ResponseEntity<InputStreamResource> safeDownload() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (XWPFDocument document = createDocument()) { // 使用try-with-resources
document.write(out);
}
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
// ByteArrayInputStream不需要显式关闭,但这是个好习惯
return ResponseEntity.ok().body(new InputStreamResource(in));
}
2. 大文件处理
java
// 对于大文件,考虑使用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 的含义:
- 包装动态数据:将内存中生成的Word文档包装成可下载的资源
- Spring标准做法:符合Spring的Resource抽象,便于统一处理
- 无需临时文件:所有操作在内存中完成,性能更好
- 自动资源管理:Spring框架负责关闭流和清理资源
- 灵活的HTTP响应:可以方便地设置文件名、Content-Type等头部信息
简单来说: InputStreamResource 让动态生成的文件数据能够以 流式方式 返回给客户端,同时享受Spring框架的资源管理便利性。
📊 两种 ContentType 的详细区别
🔍 核心概念对比
| 特性 | APPLICATION_OCTET_STREAM |
WordprocessingML Document |
|---|---|---|
| 类型 | 通用二进制流 | 特定文件类型 |
| 含义 | "这是一个二进制文件,具体类型未知" | "这是一个Word 2007+文档" |
| 使用场景 | 通用文件下载 类型不确定的文件 | 明确的Word文档下载 |
| 浏览器行为 | 总是触发下载 | 可能尝试预览(如果支持) |
🎯 具体区别分析
1. MediaType.APPLICATION_OCTET_STREAM
java
// 通用二进制流类型
Content-Type: application/octet-stream
// 浏览器行为:总是下载
// 用途:当服务器不知道文件确切类型,或希望强制下载时使用
2. WordprocessingML Document
java
// 具体的Word文档类型
Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document
// 浏览器行为:可能尝试预览(如Edge、Chrome)
// 用途:明确告诉浏览器这是Word文档
🌐 浏览器行为差异
测试示例
java
@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:通用文件下载服务
java
@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:智能内容类型选择
java
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文件类型映射
java
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. 浏览器兼容性考虑
java
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. 实际项目推荐
java
// 对于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文档生成项目,推荐使用具体类型:
java
.contentType(MediaType.parseMediaType(
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"))
这样既能正确标识文件类型,又通过attachment确保下载行为。