📝 需求分析
- springboot动态高效导出word文档
- 使用框架
poi-ooxml
、poi-tl
和jsoup
- 使用树结构维护 word 文档目录和标题,以及每个标题下的富文本内容
- 使用word文档作为模板,模板中使用占位符和循环来接收树结构里的内容
- 打印出树结构的数据填入word模板占位符的过程
- 计算要生成的word文档的页数,并写入文档中,然后导出文档
- 使用框架
🤔 实现方案
📋 框架选择与设计模式
- 框架:
- 使用
poi-ooxml
、poi-tl
、jsoup
- 使用
- 设计模式:
- 🔗 工厂模式: 创建不同类型的内容处理器(文本、图片、视频)
- 🔗 策略模式: 定义不同的富文本内容处理策略
- 🔗 建造者模式: 逐步构建复杂的文档结构
- 🔗 适配器模式: 整合外部库和API
📋 树结构与多线程处理
- 树结构: 维护文档的目录和标题及其富文本内容
- 多线程富文本处理:
- 🔗 工作窃取算法 (
ForkJoinPool
): 提高CPU的利用率 - 🔗 异步编程 (
CompletableFuture
): 提高响应性和性能
- 🔗 工作窃取算法 (
📋 Word模板处理
- 使用模板引擎处理含有占位符和循环逻辑的文档模板
📋 文档页数计算与导出
- 使用多线程计算文档页数并写入文档
- 导出最终文档
🛠️ 代码实现
📋 WORD 模版结构
[首页]
标题: {{RootTitle}}
简介: {{RootIntroduction}}
[目录]
{{#each Node}}
[{{Node.level}}级标题]: {{Node.title}}
{{#each Node.children}}
[{{ChildNode.level}}级标题]: {{ChildNode.title}}
... (递归遍历所有子节点,根据 NodeLevel 动态调整缩进或格式)
{{/each}}
{{/each}}
[正文内容]
{{#each Node}}
[{{Node.level}}级标题]: {{Node.title}}
[{{Node.level}}级标题内容]: {{Node.richText}}
{{#each Node.children}}
... (递归遍历并插入所有子节点的标题和内容)
{{/each}}
{{/each}}
📋 树结构
class DocumentNode {
String title;
String richText;
List<DocumentNode> children;
int level; // 节点的级别
// 构造器、getters、setters等
// ...
}
🌐 后端服务实现
🌌 文档生成服务
- 初始化文档
- 处理文档结构
public class DocumentNode {
private String title;
private String richText;
private List<DocumentNode> children;
private int level; // 节点的级别
// 构造器、getters、setters等
// ...
}
public interface DocumentBuilder {
void buildTitle(DocumentNode node, int level, XWPFDocument doc);
void buildContent(DocumentNode node, int level, XWPFDocument doc);
}
// 文档生成服务
public class DocumentGeneratorService {
private XWPFDocument document;
private DocumentBuilder documentBuilder;
public DocumentGeneratorService(DocumentBuilder documentBuilder) {
this.documentBuilder = documentBuilder;
this.document = new XWPFDocument();
}
public XWPFDocument generateDocument(DocumentNode rootNode) {
XWPFDocument document = new XWPFDocument();
try {
processNode(rootNode, 0, document);
return document;
} catch (Exception e) {
// 记录异常
e.printStackTrace();
// 提供用户友好的错误消息
XWPFParagraph errorParagraph = document.createParagraph();
XWPFRun errorRun = errorParagraph.createRun();
errorRun.setText("文档生成失败,请稍后重试。");
return document;
} finally {
// 关闭资源,确保资源清理
try {
document.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void processNode(DocumentNode node, int level) {
node.setLevel(level);
documentBuilder.buildTitle(node, level, document);
if (node.getRichText() != null && !node.getRichText().isEmpty()) {
documentBuilder.buildContent(node, level, document);
}
if (node.getChildren() != null) {
for (DocumentNode child : node.getChildren()) {
processNode(child, level + 1);
}
}
}
}
🌌 文档结构处理
- 解析树形结构
- 应用设计模式
public enum ContentType {
TEXT,
IMAGE,
VIDEO,
UNKNOWN // 未知内容类型
}
// 文档结构处理
public class DocumentStructureProcessor {
private DocumentBuilder documentBuilder;
private ContentHandlerFactory handlerFactory;
public DocumentStructureProcessor(DocumentBuilder documentBuilder, ContentHandlerFactory handlerFactory) {
this.documentBuilder = documentBuilder;
this.handlerFactory = handlerFactory;
}
public void processStructure(DocumentNode rootNode, CustomXWPFDocument document) {
processNode(rootNode, 0, document);
}
private void processNode(DocumentNode node, int level, CustomXWPFDocument document) {
documentBuilder.buildTitle(node, level, document);
if (node.getRichText() != null && !node.getRichText().isEmpty()) {
ContentType type = determineContentType(node);
ContentHandler handler = handlerFactory.getHandler(type);
handler.handle(node, document);
}
for (DocumentNode child : node.getChildren()) {
processNode(child, level + 1, document);
}
}
private ContentType determineContentType(DocumentNode node) {
// 假设您要根据文件扩展名来确定内容类型
String fileName = node.getFileName();
// 从文件名中提取扩展名(假设文件名中没有"."字符)
int lastDotIndex = fileName.lastIndexOf(".");
if (lastDotIndex != -1 && lastDotIndex < fileName.length() - 1) {
String extension = fileName.substring(lastDotIndex + 1).toLowerCase();
// 根据扩展名来确定内容类型
switch (extension) {
case "txt":
return ContentType.TEXT;
case "jpg":
case "png":
return ContentType.IMAGE;
case "mp4":
return ContentType.VIDEO;
default:
return ContentType.UNKNOWN; // 未知内容类型
}
}
// 如果无法确定内容类型,返回 UNKNOWN
return ContentType.UNKNOWN;
}
}
🛠️ 设计模式实现
🌌 工厂模式实现
- 实现内容处理器工厂
// 工厂模式实现
public class ContentHandlerFactory {
public ContentHandler getHandler(ContentType type) {
switch (type) {
case TEXT:
return new TextHandler();
case IMAGE:
return new ImageHandler();
case VIDEO:
return new VideoHandler();
// Add handlers for other content types
default:
throw new IllegalArgumentException("Unsupported content type");
}
}
}
🌌 策略模式实现
- 定义内容处理策略
// 内容处理器
public interface ContentHandler {
void handle(DocumentNode node, CustomXWPFDocument doc);
}
// 文本处理器
public class TextHandler implements ContentHandler {
@Override
public void handle(DocumentNode node, CustomXWPFDocument doc) {
// 假设 DocumentNode 包含要添加到文档的文本
String text = node.getRichText();
// 创建一个新段落
XWPFParagraph paragraph = doc.createParagraph();
// 创建一个运行,用于添加文本
XWPFRun run = paragraph.createRun();
// 添加文本内容
run.setText(text);
// 设置字体样式(示例)
run.setFontFamily("Arial");
run.setFontSize(12);
// 更多格式设置...
// 可以根据需要添加更多的格式化和样式设置
}
}
// 图片处理器
public class ImageHandler implements ContentHandler {
@Override
public void handle(DocumentNode node, CustomXWPFDocument doc) {
// 假设 DocumentNode 包含图片的路径
String imagePath = node.getImagePath();
// 创建一个新段落
XWPFParagraph paragraph = doc.createParagraph();
// 设置段落居中(根据需要调整)
paragraph.setAlignment(ParagraphAlignment.CENTER);
try {
// 加载图片
InputStream is = new FileInputStream(imagePath);
byte[] imageBytes = IOUtils.toByteArray(is);
// 添加图片到文档
// 注意:XWPFDocument.PICTURE_TYPE_JPEG 对应 JPEG 图片,根据实际图片格式更改
XWPFPictureData pictureData = doc.addPictureData(imageBytes, Document.PICTURE_TYPE_JPEG);
String blipId = doc.getAllPictures().get(doc.getAllPictures().size() - 1).getPackageRelationship().getId();
// 创建图片
XWPFRun run = paragraph.createRun();
run.addPicture(blipId, pictureData.getPackagePart().getContentType(), imagePath, Units.toEMU(200), Units.toEMU(200)); // 设置图片大小
is.close();
} catch (InvalidFormatException | IOException e) {
e.printStackTrace();
// 在实际应用中,应适当处理异常
}
}
}
// 视频处理器
public class VideoHandler implements ContentHandler {
@Override
public void handle(DocumentNode node, CustomXWPFDocument doc) {
// 假设 DocumentNode 包含视频的 URL 和描述
String videoUrl = node.getVideoUrl();
String videoDescription = node.getVideoDescription();
// 创建一个新段落
XWPFParagraph paragraph = doc.createParagraph();
// 设置段落样式(可选)
paragraph.setAlignment(ParagraphAlignment.CENTER);
// 创建一个运行来添加视频描述和链接
XWPFRun run = paragraph.createRun();
// 添加视频描述文本
run.setText(videoDescription + " (点击观看视频)");
// 添加超链接(假设您已经有了超链接的ID)
// 注意:创建超链接ID的逻辑需要在添加超链接之前进行
String hyperlinkId = doc.getPackagePart().addExternalRelationship(videoUrl,
XWPFRelation.HYPERLINK.getRelation()).getId();
run.getCTR().addNewHyperlink().setId(hyperlinkId);
// 设置文本样式(可选)
run.setUnderline(UnderlinePatterns.SINGLE);
run.setColor("0000FF"); // 蓝色
}
}
🌌 建造者模式实现
- 实现文档建造者
import org.apache.poi.xwpf.usermodel.*;
public interface DocumentBuilder {
void buildTitle(DocumentNode node, int level, XWPFDocument document);
void buildContent(DocumentNode node, int level, XWPFDocument document);
}
public class WordDocumentBuilder implements DocumentBuilder {
@Override
public void buildTitle(DocumentNode node, int level, XWPFDocument document) {
XWPFParagraph paragraph = document.createParagraph();
paragraph.setStyle("Heading" + level); // 设置标题级别样式,例如,Heading1、Heading2
XWPFRun run = paragraph.createRun();
run.setText(node.getTitle());
// 可以根据需要设置其他标题样式,例如字体、颜色等
}
@Override
public void buildContent(DocumentNode node, int level, XWPFDocument document) {
if (node.getRichText() != null && !node.getRichText().isEmpty()) {
XWPFParagraph paragraph = document.createParagraph();
XWPFRun run = paragraph.createRun();
run.setText(node.getRichText());
// 可以根据需要设置内容的样式,例如字体、颜色等
}
}
}
🌌 适配器模式实现
- 实现外部库和API的适配器
public interface ExternalLibraryAdapter {
void adapt(DocumentNode node, CustomXWPFDocument document);
}
public class ContentAdapter implements ExternalLibraryAdapter {
@Override
public void adapt(DocumentNode node, CustomXWPFDocument document) {
if (node.getType() == ContentType.TEXT) {
// 处理文本内容
processText(node, document);
} else if (node.getType() == ContentType.IMAGE) {
// 处理图像内容
processImage(node, document);
} else if (node.getType() == ContentType.VIDEO) {
// 处理视频内容
processVideo(node, document);
}
// 可以根据需要处理其他类型的内容
}
private void processText(DocumentNode node, CustomXWPFDocument document) {
// 处理文本内容的逻辑
}
private void processImage(DocumentNode node, CustomXWPFDocument document) {
// 处理图像内容的逻辑
}
private void processVideo(DocumentNode node, CustomXWPFDocument document) {
// 处理视频内容的逻辑
}
}
💻 多线程处理
🌌 工作窃取算法实现
- 配置和使用
ForkJoinPool
import java.util.concurrent.ForkJoinPool;
public class DocumentProcessor {
private final ForkJoinPool forkJoinPool = new ForkJoinPool();
public void processDocument(DocumentNode rootNode) {
forkJoinPool.submit(() -> processNode(rootNode));
}
private void processNode(DocumentNode node) {
// 对于每个子节点创建并提交一个任务
node.getChildren().forEach(child -> forkJoinPool.submit(() -> processNode(child)));
// 处理当前节点的内容
handleContent(node);
}
private void handleContent(DocumentNode node) {
// 处理节点内容的逻辑(例如,使用内容处理器)
}
}
🌌 异步编程实现
- 异步执行内容处理任务
import java.util.concurrent.CompletableFuture;
public class AsyncContentHandler {
public void handleAsync(DocumentNode node) {
CompletableFuture.runAsync(() -> {
// 实现具体的内容处理逻辑
processContent(node);
});
}
private void processContent(DocumentNode node) {
// 处理内容的逻辑(例如,调用适当的内容处理器)
}
}
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.CompletableFuture;
public class DocumentProcessingService {
private final DocumentProcessor documentProcessor;
private final AsyncContentHandler asyncContentHandler;
public DocumentProcessingService() {
this.documentProcessor = new DocumentProcessor();
this.asyncContentHandler = new AsyncContentHandler();
}
public void processDocumentAsync(DocumentNode rootNode) {
// 使用DocumentProcessor启动文档处理
documentProcessor.processDocument(rootNode);
// 异步处理文档内容
CompletableFuture<Void> contentHandlingFuture = CompletableFuture.runAsync(() -> {
handleDocumentContent(rootNode);
});
// 等待内容处理完成
contentHandlingFuture.join();
// 在这里可以执行其他操作或返回处理结果
}
private void handleDocumentContent(DocumentNode rootNode) {
// 递归处理文档内容,使用AsyncContentHandler处理内容
handleNodeContentAsync(rootNode);
}
private void handleNodeContentAsync(DocumentNode node) {
// 异步处理节点内容
asyncContentHandler.handleAsync(node);
// 递归处理子节点的内容
if (node.getChildren() != null) {
for (DocumentNode child : node.getChildren()) {
handleNodeContentAsync(child);
}
}
}
}
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
@Service
public class DocumentProcessingService {
private final AsyncContentHandler asyncContentHandler;
public DocumentProcessingService(AsyncContentHandler asyncContentHandler) {
// 依赖注入AsyncContentHandler
this.asyncContentHandler = asyncContentHandler;
}
/**
* 异步处理整个文档。
* 使用 @Async 注解确保方法在异步线程中执行。
*
* @param rootNode 文档的根节点
* @return CompletableFuture<Void> 异步操作的Future对象
*/
@Async
public CompletableFuture<Void> processDocumentAsync(DocumentNode rootNode) {
return CompletableFuture.runAsync(() -> {
handleDocumentContent(rootNode);
}).exceptionally(ex -> {
// 异常处理逻辑
// 在这里可以记录日志、发送警报或者进行其他错误处理操作
ex.printStackTrace();
return null;
});
}
/**
* 递归处理文档中的每个节点。
*
* @param rootNode 文档的根节点
*/
private void handleDocumentContent(DocumentNode rootNode) {
handleNodeContentAsync(rootNode);
}
/**
* 异步处理单个节点的内容,并递归处理其子节点。
*
* @param node 当前正在处理的文档节点
*/
private void handleNodeContentAsync(DocumentNode node) {
// 异步处理当前节点的内容
asyncContentHandler.handleAsync(node);
// 递归处理子节点的内容
if (node.getChildren() != null) {
for (DocumentNode child : node.getChildren()) {
handleNodeContentAsync(child);
}
}
}
}
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class AsyncContentHandler {
/**
* 异步处理单个文档节点。
* 使用 @Async 注解确保方法在异步线程中执行。
*
* @param node 需要处理的文档节点
*/
@Async
public void handleAsync(DocumentNode node) {
processContent(node);
}
/**
* 具体处理文档节点内容的逻辑。
* 这里可以根据业务需求实现具体的处理逻辑,例如解析文本、处理富文本等。
*
* @param node 需要处理的文档节点
*/
private void processContent(DocumentNode node) {
// 实际处理逻辑
// 例如:解析节点的富文本内容、提取信息、转换格式等
// 示例:System.out.println("Processing node: " + node.getTitle());
}
}
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import javax.annotation.PreDestroy;
public class DocumentProcessor {
private final ForkJoinPool forkJoinPool = new ForkJoinPool(Runtime.getRuntime().availableProcessors());
// 启动文档处理的入口方法
public CompletableFuture<Void> processDocument(DocumentNode rootNode) {
// 使用 ForkJoinPool 来异步执行任务
return CompletableFuture.supplyAsync(() -> processNode(rootNode, 0), forkJoinPool)
.exceptionally(ex -> {
// 异常处理逻辑
ex.printStackTrace();
return null;
});
}
// 递归处理每个节点,包括其子节点
private Void processNode(DocumentNode node, int depth) {
final int MAX_DEPTH = 10;
if (depth > MAX_DEPTH) {
return null; // 避免过深递归
}
CompletableFuture[] futures = node.getChildren().stream()
.map(child -> CompletableFuture.runAsync(() -> processNode(child, depth + 1), forkJoinPool))
.toArray(CompletableFuture[]::new);
CompletableFuture.runAsync(() -> processNode(child, depth + 1), forkJoinPool)
.exceptionally(ex -> {
logger.error("Error processing node", ex);
// 这里可以添加错误通知逻辑
return null;
});
// 等待所有子节点处理完成
CompletableFuture.allOf(futures).join();
handleContent(node);
return null;
}
// 处理单个节点的具体内容
private void handleContent(DocumentNode node) {
// 实现节点内容的处理逻辑
}
// 在应用关闭时关闭线程池
@PreDestroy
public void shutdown() {
forkJoinPool.shutdown(); // 关闭线程池
try {
if (!forkJoinPool.awaitTermination(60, TimeUnit.SECONDS)) {
forkJoinPool.shutdownNow(); // 如果线程池在指定时间内没有关闭,则强制关闭
}
} catch (InterruptedException e) {
forkJoinPool.shutdownNow(); // 在等待过程中被中断,强制关闭线程池
}
}
}
public class AsyncContentHandler {
private final ForkJoinPool forkJoinPool;
public AsyncContentHandler(ForkJoinPool forkJoinPool) {
this.forkJoinPool = forkJoinPool;
}
// 异步处理单个节点内容
public CompletableFuture<Void> handleAsync(DocumentNode node) {
return CompletableFuture.runAsync(() -> processContent(node), forkJoinPool)
.exceptionally(ex -> {
// 异常处理逻辑
ex.printStackTrace();
logger.error("Error handling content", ex);
// 可以在这里实现重试逻辑或故障恢复
return null;
});
}
// 处理内容的具体逻辑
private void processContent(DocumentNode node) {
// 实现节点内容的具体处理逻辑
}
}
public class DocumentProcessingService {
private final DocumentProcessor documentProcessor;
private final AsyncContentHandler asyncContentHandler;
public DocumentProcessingService(DocumentProcessor documentProcessor,
AsyncContentHandler asyncContentHandler) {
this.documentProcessor = documentProcessor;
this.asyncContentHandler = asyncContentHandler;
}
// 启动异步文档处理流程
public CompletableFuture<Void> processDocumentAsync(DocumentNode rootNode) {
CompletableFuture<Void> processingFuture = documentProcessor.processDocument(rootNode);
processingFuture.thenRun(() -> handleDocumentContent(rootNode))
.exceptionally(ex -> {
// 异常处理逻辑
ex.printStackTrace();
return null;
});
return processingFuture;
}
// 递归处理文档内容
private void handleDocumentContent(DocumentNode rootNode) {
handleNodeContentAsync(rootNode);
}
// 异步处理单个节点及其子节点
private void handleNodeContentAsync(DocumentNode node) {
asyncContentHandler.handleAsync(node)
.thenRun(() -> node.getChildren().forEach(this::handleNodeContentAsync))
.exceptionally(ex -> {
// 异常处理逻辑
ex.printStackTrace();
return null;
});
}
}
为了决定在您的功能中是使用 ForkJoinPool
还是 Spring Boot 自带的线程池(通常是通过 @Async
注解和任务执行器实现),我们需要考虑几个关键因素:
-
任务的性质:
- 如果您的任务可以被有效地分解为更小的子任务,并且这些子任务之间具有递归关系,那么
ForkJoinPool
是一个很好的选择。这是因为ForkJoinPool
专为这种类型的任务设计,能够有效利用工作窃取算法来平衡线程之间的工作负载。 - 如果您的任务是相对独立的,并且没有明显的递归分解结构,那么使用 Spring Boot 的默认线程池可能更合适。这些任务通常适合使用标准的线程池进行处理,无需
ForkJoinPool
的复杂性。
- 如果您的任务可以被有效地分解为更小的子任务,并且这些子任务之间具有递归关系,那么
-
集成和管理:
- 使用 Spring Boot 自带的线程池可以更好地与 Spring 生态系统集成,例如自动配置、事务管理、安全上下文等。
- 自定义线程池(如
ForkJoinPool
)可能需要更多的手动配置和管理,但提供了更多的控制和优化空间。
-
性能需求:
- 如果性能是一个重要因素,且您的任务适合并行处理,那么
ForkJoinPool
可能会提供更好的性能。 - 对于一般的并发任务,Spring Boot 默认的线程池通常足够高效。
- 如果性能是一个重要因素,且您的任务适合并行处理,那么
-
资源限制:
- 在资源受限的环境中(如容器化部署),控制线程数量变得尤为重要。在这种情况下,Spring Boot 的线程池管理可能更易于配置和调优。
根据您之前的描述,如果您的文档处理任务主要是处理独立的文档节点,并且没有明显的分解为小任务的需求,那么使用 Spring Boot 默认的线程池可能是更简单且有效的选择。这样可以充分利用 Spring 的管理特性,简化配置和开发工作。
📝 模板处理
🌌 模板引擎配置
- 设置和加载模板引擎
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import java.io.File;
import java.io.FileInputStream;
public class TemplateEngineConfigurator {
public XWPFDocument loadTemplate(String templatePath) {
try (FileInputStream fis = new FileInputStream(new File(templatePath))) {
return new XWPFDocument(fis);
} catch (Exception e) {
throw new RuntimeException("Error loading template", e);
}
}
}
🌌 模板填充实现
- 填充模板占位符
// 完整的 TemplateFiller
public class TemplateFiller {
// 字符串模板,包含占位符
private static final String TEMPLATE = "[{{NodeLevel}}级标题]: {{NodeTitle}}\n[{{NodeLevel}}级标题内容]: {{NodeRichText}}\n";
public void fillTemplate(XWPFDocument document, DocumentNode rootNode) {
processNode(document, rootNode, 0);
}
private void processNode(XWPFDocument document, DocumentNode node, int level) {
// 使用模板替换占位符
String filledTemplate = TEMPLATE
.replace("{{NodeLevel}}", String.valueOf(level))
.replace("{{NodeTitle}}", node.getTitle())
.replace("{{NodeRichText}}", node.getRichText());
// 创建段落并添加填充后的内容
XWPFParagraph paragraph = document.createParagraph();
XWPFRun run = paragraph.createRun();
run.setText(filledTemplate);
// 递归处理子节点
if (node.getChildren() != null) {
for (DocumentNode child : node.getChildren()) {
processNode(document, child, level + 1);
}
}
}
private void replacePlaceholder(XWPFDocument document, String placeholder, String replacement) {
for (XWPFParagraph paragraph : document.getParagraphs()) {
if (paragraph.getText().contains(placeholder)) {
replaceTextInParagraph(paragraph, placeholder, replacement);
}
}
}
private void replaceTextInParagraph(XWPFParagraph paragraph, String placeholder, String replacement) {
// 清除段落中的所有文本
paragraph.getRuns().clear();
// 添加新的文本替换占位符
XWPFRun run = paragraph.createRun();
run.setText(replacement);
}
}
📊 文档页数计算与导出
🌌 页数计算实现
- 多线程计算页数
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class PageNumberCalculator {
private ExecutorService executorService = Executors.newSingleThreadExecutor();
public Future<Integer> calculatePageCountAsync(XWPFDocument document) {
return executorService.submit(() -> {
// 实现页数计算的逻辑
// 注意:页数的计算可能需要将文档渲染到某种形式,具体取决于使用的库和技术
return calculatePageCount(document);
});
}
public int calculatePageCount(XWPFDocument document) {
try {
// 将 XWPFDocument 转换为 PDF
ByteArrayOutputStream out = new ByteArrayOutputStream();
PdfOptions options = PdfOptions.create();
PdfConverter.getInstance().convert(document, out, options);
// 读取生成的 PDF 内容
InputStream in = new ByteArrayInputStream(out.toByteArray());
PDDocument pdfDocument = PDDocument.load(in);
// 获取 PDF 的页数
int pageCount = pdfDocument.getNumberOfPages();
pdfDocument.close();
return pageCount;
} catch (Exception e) {
throw new RuntimeException("Error calculating page count", e);
}
}
}
🌌 文档导出实现
- 导出最终文档
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
public class DocumentExporter {
public void exportDocument(XWPFDocument document, String outputPath) throws ExportException {
try {
// 检查输出目录是否存在,如果不存在则创建
File outputDirectory = new File(outputPath).getParentFile();
if (!outputDirectory.exists()) {
boolean directoriesCreated = outputDirectory.mkdirs();
if (!directoriesCreated) {
throw new ExportException("Failed to create output directory.");
}
}
// 导出文档到指定路径
try (FileOutputStream out = new FileOutputStream(outputPath)) {
document.write(out);
}
System.out.println("Document exported to: " + outputPath);
} catch (IOException e) {
throw new ExportException("Error exporting document.", e);
}
}
public class ExportException extends Exception {
public ExportException(String message) {
super(message);
}
public ExportException(String message, Throwable cause) {
super(message, cause);
}
}
}
import org.apache.poi.xwpf.usermodel.*;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
public class POIWordRichTextExtended {
public static void main(String[] args) throws IOException {
String templatePath = "path/to/template.docx";
String outputPath = "path/to/output.docx";
String placeholder = "{{Content}}";
String htmlContent = "<p><b>粗体</b> <i>斜体</i> <u>下划线</u> <span style='color:red;'>红色文本</span> <img src='image_url.jpg'> <a href='video_url.mp4'>视频链接</a></p>";
XWPFDocument doc = new XWPFDocument(new FileInputStream(templatePath));
Document htmlDoc = Jsoup.parse(htmlContent);
Elements elements = htmlDoc.body().children();
for (XWPFParagraph paragraph : doc.getParagraphs()) {
for (XWPFRun run : paragraph.getRuns()) {
String text = run.getText(0);
if (text != null && text.contains(placeholder)) {
run.setText("", 0); // 清除占位符
for (Element element : elements) {
XWPFRun newRun = paragraph.createRun();
// 处理标签和样式
switch (element.tagName()) {
case "b":
newRun.setBold(true);
break;
case "i":
newRun.setItalic(true);
break;
case "u":
newRun.setUnderline(UnderlinePatterns.SINGLE);
break;
case "span":
String color = element.attr("style").replaceAll("[^#\\dA-Fa-f]", "");
newRun.setColor(color);
break;
case "img":
String imgUrl = element.attr("src");
// 下载图片
try (InputStream in = new URL(imgUrl).openStream()) {
// 添加图片到文档
byte[] pictureData = IOUtils.toByteArray(in);
int pictureType = XWPFDocument.PICTURE_TYPE_JPEG; // 根据实际图片类型调整
newRun.addPicture(new ByteArrayInputStream(pictureData), pictureType, imgUrl, Units.toEMU(100), Units.toEMU(100)); // 设置图片大小
} catch (Exception e) {
e.printStackTrace();
}
break;
case "a":
String videoUrl = element.attr("href");
newRun.setText("视频链接: ");
newRun.addHyperlink(videoUrl);
break;
}
if (!element.tagName().equals("img") && !element.tagName().equals("a")) {
newRun.setText(element.text());
}
}
}
}
}
FileOutputStream out = new FileOutputStream(outputPath);
doc.write(out);
out.close();
doc.close();
}
}