SpringBoot三步將Word轉為PDF的實戰(zhàn)指南
Word到PDF的奇幻之旅
Word文檔就像個穿著睡衣在家辦公的程序員——舒服但有點隨意。而PDF呢?就是穿上西裝打上領帶,準備去參加董事會的同一人——專業(yè)且紋絲不動!
這轉變過程好比:
- Word文檔:“哈!我的字體可以隨便換,邊距可以隨意調,圖片還能拖來拖去~”
- PDF:“閉嘴!現(xiàn)在開始我說了算,每個像素都給我站好崗!”
SpringBoot實現(xiàn)這個轉換,就像是請了個文檔變形金剛,把自由散漫的Word馴化成紀律嚴明的PDF士兵。下面就讓我?guī)阋娮C這場“格式馴化儀式”!
準備階段:裝備你的“變形工具箱”
第一步:Maven依賴大采購
<!-- pom.xml 里加入這些法寶 -->
<dependencies>
<!-- SpringBoot標準裝備 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Apache POI - Word文檔的“讀心術” -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.2.3</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.3</version>
</dependency>
<!-- OpenPDF - PDF的“打印機” -->
<dependency>
<groupId>com.github.librepdf</groupId>
<artifactId>openpdf</artifactId>
<version>1.3.30</version>
</dependency>
<!-- 文件類型檢測 - 避免把圖片當Word處理 -->
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
<version>2.7.0</version>
</dependency>
</dependencies>
第二步:配置屬性文件
# application.yml
word-to-pdf:
upload-dir: "uploads/" # Word文檔臨時停靠站
output-dir: "pdf-output/" # PDF成品倉庫
max-file-size: 10MB # 別想用《戰(zhàn)爭與和平》來考驗我
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
核心代碼:變身吧,Word君!
1. 文件上傳控制器(接待員)
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
@RestController
@RequestMapping("/api/doc-transform")
public class WordToPdfController {
@PostMapping("/word-to-pdf")
public void convertWordToPdf(
@RequestParam("file") MultipartFile wordFile,
HttpServletResponse response) throws IOException {
// 1. 檢查文件:別想用貓咪圖片冒充Word文檔!
if (!isWordDocument(wordFile)) {
response.getWriter().write("喂!這不是Word文檔,別騙我!");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
// 2. 臨時存放Word文件(像安檢前的暫存)
File tempWordFile = new File("temp_" + System.currentTimeMillis() + ".docx");
wordFile.transferTo(tempWordFile);
// 3. 開始變形!
byte[] pdfBytes = WordToPdfConverter.convert(tempWordFile);
// 4. 清理現(xiàn)場(像用完的變形金剛恢復原狀)
tempWordFile.delete();
// 5. 把PDF交給用戶
response.setContentType("application/pdf");
response.setHeader("Content-Disposition",
"attachment; filename=\"" +
wordFile.getOriginalFilename().replace(".docx", ".pdf") + "\"");
response.getOutputStream().write(pdfBytes);
System.out.println("轉換成功!又一個Word被成功馴化成PDF!");
}
private boolean isWordDocument(MultipartFile file) {
String fileName = file.getOriginalFilename().toLowerCase();
return fileName.endsWith(".docx") || fileName.endsWith(".doc");
}
}
2. 轉換器核心(真正的變形引擎)
import org.apache.poi.xwpf.usermodel.*;
import com.lowagie.text.*;
import com.lowagie.text.pdf.PdfWriter;
import java.io.*;
@Component
public class WordToPdfConverter {
public static byte[] convert(File wordFile) throws IOException {
ByteArrayOutputStream pdfOutputStream = new ByteArrayOutputStream();
try (FileInputStream fis = new FileInputStream(wordFile)) {
// 1. 打開Word文檔(像打開潘多拉魔盒)
XWPFDocument document = new XWPFDocument(fis);
// 2. 創(chuàng)建PDF文檔(準備新家)
Document pdfDocument = new Document();
PdfWriter.getInstance(pdfDocument, pdfOutputStream);
pdfDocument.open();
// 3. 逐段搬運內容(像螞蟻搬家)
System.out.println("開始搬運段落,共" + document.getParagraphs().size() + "段...");
for (XWPFParagraph para : document.getParagraphs()) {
if (para.getText().trim().isEmpty()) continue;
// 處理文本樣式
Font font = new Font();
if (para.getStyle() != null) {
switch (para.getStyle()) {
case "Heading1":
font = new Font(Font.HELVETICA, 18, Font.BOLD);
break;
case "Heading2":
font = new Font(Font.HELVETICA, 16, Font.BOLD);
break;
default:
font = new Font(Font.HELVETICA, 12, Font.NORMAL);
}
}
Paragraph pdfPara = new Paragraph(para.getText(), font);
pdfDocument.add(pdfPara);
pdfDocument.add(Chunk.NEWLINE); // 加個換行,喘口氣
}
// 4. 處理圖片(最難搬家的部分)
System.out.println("開始處理圖片,共" + document.getAllPictures().size() + "張...");
for (XWPFPictureData picture : document.getAllPictures()) {
try {
byte[] pictureData = picture.getData();
Image image = Image.getInstance(pictureData);
image.scaleToFit(500, 500); // 給圖片上個緊箍咒,別太大
image.setAlignment(Element.ALIGN_CENTER);
pdfDocument.add(image);
pdfDocument.add(Chunk.NEWLINE);
} catch (Exception e) {
System.err.println("圖片" + picture.getFileName() + "太調皮,轉換失敗: " + e.getMessage());
}
}
// 5. 處理表格(Excel表示:我也想來湊熱鬧)
for (XWPFTable table : document.getTables()) {
com.lowagie.text.Table pdfTable =
new com.lowagie.text.Table(table.getNumberOfRows());
for (XWPFTableRow row : table.getRows()) {
for (XWPFTableCell cell : row.getTableCells()) {
pdfTable.addCell(cell.getText());
}
}
pdfDocument.add(pdfTable);
}
pdfDocument.close();
document.close();
System.out.println("轉換完成!生成PDF大小: " +
(pdfOutputStream.size() / 1024) + " KB");
} catch (Exception e) {
System.err.println("轉換過程出現(xiàn)意外: " + e.getMessage());
throw new IOException("轉換失敗,Word文檔可能被施了魔法", e);
}
return pdfOutputStream.toByteArray();
}
}
3. 異常處理(變形失敗的救護車)
@ControllerAdvice
public class DocumentConversionExceptionHandler {
@ExceptionHandler(IOException.class)
public ResponseEntity<String> handleIOException(IOException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("文檔轉換失敗,可能原因:\n" +
"1. Word文檔被外星人加密了\n" +
"2. 文件太大,服務器舉不動了\n" +
"3. 網(wǎng)絡連接在打瞌睡\n" +
"錯誤詳情: " + e.getMessage());
}
@ExceptionHandler(InvalidFormatException.class)
public ResponseEntity<String> handleInvalidFormat(Exception e) {
return ResponseEntity.badRequest()
.body("喂!你上傳的是Word文檔嗎?\n" +
"我猜你上傳的是:\n" +
"□ 貓咪圖片 \n" +
"□ Excel表格 \n" +
"□ 心靈雞湯文本 \n" +
"請上傳正經的.docx或.doc文件!");
}
}
4. 進度監(jiān)控(變形過程直播)
@Component
public class ConversionProgressService {
private Map<String, Integer> progressMap = new ConcurrentHashMap<>();
public void startConversion(String fileId) {
progressMap.put(fileId, 0);
System.out.println("開始轉換文件: " + fileId);
}
public void updateProgress(String fileId, int percent) {
progressMap.put(fileId, percent);
// 打印進度條(假裝很高級)
StringBuilder progressBar = new StringBuilder("[");
for (int i = 0; i < 20; i++) {
progressBar.append(i * 5 < percent ? "█" : "?");
}
progressBar.append("] ").append(percent).append("%");
System.out.println(fileId + " 轉換進度: " + progressBar.toString());
// 說點騷話鼓勵一下
if (percent == 50) {
System.out.println("轉換過半,堅持住!");
} else if (percent == 90) {
System.out.println("馬上完成,準備發(fā)射PDF!");
}
}
public void completeConversion(String fileId) {
progressMap.remove(fileId);
System.out.println(fileId + " 轉換完成,深藏功與名~");
}
}
前端調用示例(用戶操作界面)
<!DOCTYPE html>
<html>
<head>
<title>Word轉PDF變形工坊</title>
<style>
body { font-family: 'Comic Sans MS', cursive; padding: 20px; }
.container { max-width: 600px; margin: 0 auto; }
.drop-zone {
border: 3px dashed #4CAF50;
border-radius: 10px;
padding: 40px;
text-align: center;
background: #f9f9f9;
cursor: pointer;
}
.drop-zone:hover { background: #e8f5e9; }
.convert-btn {
background: linear-gradient(45deg, #FF6B6B, #4ECDC4);
color: white;
border: none;
padding: 15px 30px;
border-radius: 25px;
font-size: 18px;
cursor: pointer;
margin-top: 20px;
}
.progress-bar {
width: 100%;
height: 20px;
background: #ddd;
border-radius: 10px;
margin-top: 20px;
overflow: hidden;
display: none;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #4CAF50, #8BC34A);
width: 0%;
transition: width 0.3s;
}
</style>
</head>
<body>
<div class="container">
<h1>Word轉PDF變形工坊</h1>
<p>把你的Word文檔扔進來,還你一個乖巧的PDF!</p>
<div class="drop-zone" id="dropZone">
<h2>拖拽Word文件到這里</h2>
<p>或者 <label style="color: #2196F3; cursor: pointer;">點擊選擇文件
<input type="file" id="fileInput" accept=".docx,.doc" hidden>
</label></p>
</div>
<button class="convert-btn" onclick="convertToPdf()">
開始變形!
</button>
<div class="progress-bar" id="progressBar">
<div class="progress-fill" id="progressFill"></div>
</div>
<div id="status" style="margin-top: 20px;"></div>
</div>
<script>
const dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('fileInput');
let selectedFile = null;
// 拖拽功能
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.style.background = '#e8f5e9';
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.style.background = '#f9f9f9';
selectedFile = e.dataTransfer.files[0];
document.getElementById('status').innerHTML =
`已選擇: <strong>${selectedFile.name}</strong>`;
});
fileInput.addEventListener('change', (e) => {
selectedFile = e.target.files[0];
document.getElementById('status').innerHTML =
`已選擇: <strong>${selectedFile.name}</strong>`;
});
// 轉換函數(shù)
async function convertToPdf() {
if (!selectedFile) {
alert('請先選擇一個Word文件!');
return;
}
const formData = new FormData();
formData.append('file', selectedFile);
// 顯示進度條
const progressBar = document.getElementById('progressBar');
const progressFill = document.getElementById('progressFill');
progressBar.style.display = 'block';
// 模擬進度(實際項目可以用WebSocket)
let progress = 0;
const interval = setInterval(() => {
progress += 10;
progressFill.style.width = `${progress}%`;
if (progress >= 90) clearInterval(interval);
}, 300);
try {
const response = await fetch('/api/doc-transform/word-to-pdf', {
method: 'POST',
body: formData
});
clearInterval(interval);
progressFill.style.width = '100%';
if (response.ok) {
// 下載PDF
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = selectedFile.name.replace(/\.docx?$/i, '.pdf');
document.body.appendChild(a);
a.click();
a.remove();
document.getElementById('status').innerHTML =
'轉換成功!PDF已開始下載~';
// 3秒后重置
setTimeout(() => {
progressBar.style.display = 'none';
progressFill.style.width = '0%';
document.getElementById('status').innerHTML = '';
}, 3000);
} else {
const errorText = await response.text();
document.getElementById('status').innerHTML =
`轉換失敗: ${errorText}`;
}
} catch (error) {
document.getElementById('status').innerHTML =
`網(wǎng)絡錯誤: ${error.message}`;
}
}
</script>
</body>
</html>
高級功能擴展
批量轉換(群變模式)
@Service
public class BatchConversionService {
@Async // 異步處理,不卡界面
public CompletableFuture<List<File>> convertMultiple(List<MultipartFile> files) {
System.out.println("開始批量轉換,共" + files.size() + "個文件,沖鴨!");
List<File> pdfFiles = new ArrayList<>();
List<CompletableFuture<File>> futures = new ArrayList<>();
for (int i = 0; i < files.size(); i++) {
final int index = i;
CompletableFuture<File> future = CompletableFuture.supplyAsync(() -> {
try {
System.out.println("正在轉換第" + (index + 1) + "個文件...");
byte[] pdfBytes = WordToPdfConverter.convert(convertToFile(files.get(index)));
File pdfFile = new File("converted_" + index + ".pdf");
Files.write(pdfFile.toPath(), pdfBytes);
return pdfFile;
} catch (Exception e) {
System.err.println("第" + (index + 1) + "個文件轉換失敗: " + e.getMessage());
return null;
}
});
futures.add(future);
}
// 等待所有轉換完成
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
for (CompletableFuture<File> future : futures) {
try {
File pdf = future.get();
if (pdf != null) pdfFiles.add(pdf);
} catch (Exception e) {
// 忽略失敗的文件
}
}
System.out.println("批量轉換完成!成功: " + pdfFiles.size() +
"/" + files.size() + " 個文件");
return CompletableFuture.completedFuture(pdfFiles);
}
}
轉換記錄(變形檔案室)
@Entity
@Table(name = "conversion_records")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ConversionRecord {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String originalFileName;
private String pdfFileName;
private Long originalSize;
private Long pdfSize;
private LocalDateTime conversionTime;
private String status; // SUCCESS, FAILED, PROCESSING
private String errorMessage;
@PrePersist
protected void onCreate() {
conversionTime = LocalDateTime.now();
}
}
@Repository
public interface ConversionRecordRepository extends JpaRepository<ConversionRecord, Long> {
List<ConversionRecord> findByStatusOrderByConversionTimeDesc(String status);
}
部署與優(yōu)化建議
1. 性能優(yōu)化
# application.yml 添加
server:
tomcat:
max-threads: 200 # 增加線程數(shù)處理并發(fā)轉換
min-spare-threads: 20
spring:
task:
execution:
pool:
core-size: 10 # 異步任務線程池
max-size: 50
2. 內存管理
@Component
public class MemoryWatcher {
@Scheduled(fixedRate = 60000) // 每分鐘檢查一次
public void monitorMemory() {
long usedMemory = Runtime.getRuntime().totalMemory() -
Runtime.getRuntime().freeMemory();
long maxMemory = Runtime.getRuntime().maxMemory();
double usagePercentage = (double) usedMemory / maxMemory * 100;
if (usagePercentage > 80) {
System.out.println("內存警告:使用率 " +
String.format("%.1f", usagePercentage) + "%");
// 觸發(fā)垃圾回收
System.gc();
}
}
}
總結:Word轉PDF的奇幻旅程終點站
經過這一番折騰,我們成功打造了一個SpringBoot牌文檔變形金剛!總結一下這場冒險:
我們實現(xiàn)了什么
- 格式馴化:把自由的Word變成規(guī)矩的PDF
- 異步處理:大文件轉換不卡界面
- 進度監(jiān)控:實時查看轉換進度
- 錯誤處理:優(yōu)雅處理各種意外情況
- 批量操作:一次性馴化整個Word文檔家族
注意事項
- 字體問題:有些特殊字體PDF可能不認識,需要額外處理
- 復雜格式:Word里的高級排版(如文本框、藝術字)可能變形
- 內存消耗:大文檔轉換時注意內存溢出
- 并發(fā)限制:同時轉換太多文檔可能導致服務器喘不過氣
Word轉PDF就像給文檔穿上“防改鎧甲”,SpringBoot就是我們打造這副鎧甲的智能工廠。雖然過程中會遇到各種奇葩格式的“刺頭文檔”,但只要有耐心調試,最終都能把它們治理得服服帖帖!
到此這篇關于SpringBoot三步將Word轉為PDF的實戰(zhàn)指南的文章就介紹到這了,更多相關SpringBoot Word轉PDF內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Java?Post請求發(fā)送form-data表單參數(shù)詳細示例代碼
POST請求是一種常見的網(wǎng)絡通信操作,用于向服務器發(fā)送數(shù)據(jù),這種請求通常用于上傳文件或者提交包含大量數(shù)據(jù)的表單,這篇文章主要介紹了Java?Post請求發(fā)送form-data表單參數(shù)的相關資料,需要的朋友可以參考下2025-07-07
Spring?Boot?整合?Fisco?Bcos的案例分析(區(qū)塊鏈)
本篇文章介紹的?Spring?Boot?整合?Fisco?Bcos的案例,是在阿里云服務器上部署驗證的。大家可根據(jù)自己的電腦環(huán)境,對比該案例進行開發(fā)即可,具體案例代碼跟隨小編一起看看吧2022-01-01

