【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 {
@Autowired
private 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 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));
  }
  }

复制文本运行

/**
* 文本运行是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. 包装成 InputStreamResource
  InputStreamResource 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-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)
// 方式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 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报告

@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:模板填充下载

@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-resources
  document.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确保下载行为。

posted @ 2025-11-27 14:37  yangykaifa  阅读(30)  评论(0)    收藏  举报