本文目录导读:

- 技术选型方案
- 实操案例1:使用Apache PDFBox实现PDF预览
- 实操案例2:使用JODConverter(OpenOffice)实现多格式转换
- 前端JavaScript实现(轻量级方案)
- 完整实操步骤
- 常见问题与优化
我来详细讲解Java在线预览的实操案例,以最常见的PDF、Word、Excel在线预览为例,我会提供完整的实操步骤和代码。
技术选型方案
常用方案对比
| 方案 | 优缺点 | 适用场景 |
|---|---|---|
| Apache POI + iText | 功能强大,但开发量大 | 需要复杂处理的场景 |
| OpenOffice/LibreOffice | 转换格式通用,但需安装软件 | 多种文档格式转换 |
| Aspose | 商业级,效果好 | 企业级应用 |
| 前端JS库 | 轻量级,无需后端 | 简单预览需求 |
阿里巴巴jw-project |
PDF预览效果好 | 纯Java解决方案 |
实操案例1:使用Apache PDFBox实现PDF预览
添加依赖(Maven)
<!-- PDF处理 -->
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>2.0.27</version>
</dependency>
<!-- 图片处理 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-imaging</artifactId>
<version>1.0-alpha2</version>
</dependency>
PDF转图片核心代码
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.PDFRenderer;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
public class PdfPreviewUtil {
/**
* PDF转图片(字节数组)
*/
public static byte[] pdfToImage(File pdfFile, int pageNumber, int dpi) throws IOException {
try (PDDocument document = PDDocument.load(pdfFile)) {
PDFRenderer renderer = new PDFRenderer(document);
// 渲染指定页面
BufferedImage image = renderer.renderImageWithDPI(pageNumber, dpi);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, "png", baos);
return baos.toByteArray();
}
}
/**
* 将PDF所有页面转为图片(用于分页预览)
*/
public static List<byte[]> pdfToImages(File pdfFile, int dpi) throws IOException {
List<byte[]> imageList = new ArrayList<>();
try (PDDocument document = PDDocument.load(pdfFile)) {
PDFRenderer renderer = new PDFRenderer(document);
for (int i = 0; i < document.getNumberOfPages(); i++) {
BufferedImage image = renderer.renderImageWithDPI(i, dpi);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, "png", baos);
imageList.add(baos.toByteArray());
}
}
return imageList;
}
}
Spring Boot控制器实现
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class PdfPreviewController {
// 文件存储路径
private final String filePath = "/data/files/";
/**
* 返回PDF预览页面
*/
@GetMapping("/preview/pdf/{fileId}")
public String previewPdf(@PathVariable String fileId, Model model) {
model.addAttribute("fileId", fileId);
return "pdf-preview"; // 返回模板页面
}
/**
* 获取PDF某一页的图片
*/
@GetMapping("/api/pdf/{fileId}/page/{pageNum}")
public ResponseEntity<Resource> getPdfPage(
@PathVariable String fileId,
@PathVariable int pageNum,
@RequestParam(defaultValue = "150") int dpi) {
try {
File pdfFile = new File(filePath + fileId + ".pdf");
if (!pdfFile.exists()) {
return ResponseEntity.notFound().build();
}
byte[] imageBytes = PdfPreviewUtil.pdfToImage(pdfFile, pageNum - 1, dpi);
ByteArrayResource resource = new ByteArrayResource(imageBytes);
return ResponseEntity.ok()
.contentType(MediaType.IMAGE_PNG)
.contentLength(imageBytes.length)
.body(resource);
} catch (Exception e) {
return ResponseEntity.status(500).build();
}
}
/**
* 获取PDF总页数
*/
@GetMapping("/api/pdf/{fileId}/pages")
public ResponseEntity<Map<String, Object>> getPdfPages(@PathVariable String fileId) {
try {
File pdfFile = new File(filePath + fileId + ".pdf");
if (!pdfFile.exists()) {
return ResponseEntity.notFound().build();
}
try (PDDocument document = PDDocument.load(pdfFile)) {
Map<String, Object> result = new HashMap<>();
result.put("totalPages", document.getNumberOfPages());
return ResponseEntity.ok(result);
}
} catch (Exception e) {
return ResponseEntity.status(500).build();
}
}
}
前端预览界面(Thymeleaf模板)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>PDF在线预览</title>
<style>
#pdfContainer {
display: flex;
flex-direction: column;
align-items: center;
}
.page-image {
max-width: 100%;
margin-bottom: 10px;
border: 1px solid #ddd;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
.controls {
position: fixed;
bottom: 0;
width: 100%;
background: white;
padding: 10px;
text-align: center;
box-shadow: 0 -2px 5px rgba(0,0,0,0.1);
}
</style>
</head>
<body>
<h1>PDF在线预览</h1>
<div id="pdfContainer">
<!-- 动态加载的页面图片 -->
</div>
<div class="controls">
<button onclick="prevPage()">上一页</button>
<span id="pageInfo">第 <span id="currentPage">1</span> / <span id="totalPages">0</span> 页</span>
<button onclick="nextPage()">下一页</button>
<input type="number" id="jumpPage" min="1" placeholder="跳转页">
<button onclick="jumpToPage()">跳转</button>
</div>
<script>
const fileId = [[${fileId}]];
let currentPage = 1;
let totalPages = 0;
// 获取总页数
async function getTotalPages() {
const response = await fetch(`/api/pdf/${fileId}/pages`);
const data = await response.json();
totalPages = data.totalPages;
document.getElementById('totalPages').textContent = totalPages;
}
// 加载页面
async function loadPage(pageNum) {
const response = await fetch(`/api/pdf/${fileId}/page/${pageNum}?dpi=150`);
const blob = await response.blob();
const imageUrl = URL.createObjectURL(blob);
const container = document.getElementById('pdfContainer');
container.innerHTML = `
<img src="${imageUrl}" class="page-image" alt="第${pageNum}页">
`;
document.getElementById('currentPage').textContent = pageNum;
}
// 上一页
function prevPage() {
if (currentPage > 1) {
currentPage--;
loadPage(currentPage);
}
}
// 下一页
function nextPage() {
if (currentPage < totalPages) {
currentPage++;
loadPage(currentPage);
}
}
// 跳转到指定页
function jumpToPage() {
const page = parseInt(document.getElementById('jumpPage').value);
if (page >= 1 && page <= totalPages) {
currentPage = page;
loadPage(currentPage);
}
}
// 初始化
getTotalPages().then(() => loadPage(1));
</script>
</body>
</html>
实操案例2:使用JODConverter(OpenOffice)实现多格式转换
这种方式可以处理Word、Excel、PPT等多种格式转换为PDF或图片。
安装OpenOffice/LibreOffice
# Ubuntu/Debian sudo apt-get install libreoffice # CentOS sudo yum install libreoffice # macOS brew install --cask libreoffice
添加依赖
<!-- JODConverter -->
<dependency>
<groupId>com.github.jodconverter</groupId>
<artifactId>jodconverter-spring-boot-starter</artifactId>
<version>4.4.6</version>
</dependency>
<dependency>
<groupId>com.github.jodconverter</groupId>
<artifactId>jodconverter-local</artifactId>
<version>4.4.6</version>
</dependency>
application.yml配置
jodconverter:
local:
enabled: true
office-home: /usr/lib/libreoffice # 根据实际安装路径修改
max-tasks-per-process: 10
port-numbers: 2002
task-execution-timeout: 60000
task-queue-timeout: 60000
文件转换服务
import com.github.jodconverter.core.DocumentConverter;
import com.github.jodconverter.core.document.DefaultDocumentFormatRegistry;
import com.github.jodconverter.core.document.DocumentFormat;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
@Service
public class FileConversionService {
@Autowired
private DocumentConverter converter;
/**
* Word/PPT/Excel转为PDF
*/
public File convertToPdf(File inputFile) throws Exception {
// 创建临时文件
File outputFile = File.createTempFile("converted_", ".pdf");
try (InputStream inputStream = new FileInputStream(inputFile);
OutputStream outputStream = new FileOutputStream(outputFile)) {
// 自动检测文件格式并转换
DocumentFormat inputFormat = getDocumentFormat(inputFile.getName());
DocumentFormat outputFormat = DefaultDocumentFormatRegistry.PDF;
converter.convert(inputStream, inputFormat, outputStream, outputFormat);
}
return outputFile;
}
/**
* 根据文件扩展名获取文档格式
*/
private DocumentFormat getDocumentFormat(String fileName) {
String extension = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
switch (extension) {
case "doc":
return DefaultDocumentFormatRegistry.DOC;
case "docx":
return DefaultDocumentFormatRegistry.DOCX;
case "xls":
return DefaultDocumentFormatRegistry.XLS;
case "xlsx":
return DefaultDocumentFormatRegistry.XLSX;
case "ppt":
return DefaultDocumentFormatRegistry.PPT;
case "pptx":
return DefaultDocumentFormatRegistry.PPTX;
case "txt":
return DefaultDocumentFormatRegistry.TXT;
default:
throw new IllegalArgumentException("不支持的文件格式: " + extension);
}
}
/**
* 获取文件预览URL
*/
public String getPreviewUrl(String fileId, String fileName) {
// 返回预览API地址
return "/preview/" + fileId + "?fileName=" + fileName;
}
}
统一预览控制器
@RestController
@RequestMapping("/preview")
public class UnifiedPreviewController {
@Autowired
private FileConversionService conversionService;
@Autowired
private PdfPreviewUtil pdfPreviewUtil;
private final String filePath = "/data/uploads/";
/**
* 文档预览(自动转换并预览)
*/
@GetMapping("/{fileId}")
public ResponseEntity<?> previewDocument(
@PathVariable String fileId,
@RequestParam String fileName) {
try {
File inputFile = new File(filePath + fileName);
if (!inputFile.exists()) {
return ResponseEntity.notFound().build();
}
String extension = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
// 如果是PDF直接预览
if ("pdf".equals(extension)) {
return previewPdf(fileId, fileName);
}
// 其他格式先转为PDF
File pdfFile = conversionService.convertToPdf(inputFile);
// 返回转换后的PDF预览URL
Map<String, Object> result = new HashMap<>();
result.put("status", "success");
result.put("previewUrl", "/preview/pdf/" + pdfFile.getName());
result.put("downloadUrl", "/download/" + fileName);
result.put("originalName", fileName);
return ResponseEntity.ok(result);
} catch (Exception e) {
return ResponseEntity.status(500).body("预览失败: " + e.getMessage());
}
}
private ResponseEntity<?> previewPdf(String fileId, String fileName) {
Map<String, Object> result = new HashMap<>();
result.put("status", "success");
result.put("previewType", "pdf");
result.put("pages", "/api/pdf/" + fileId + "/pages");
result.put("pageUrl", "/api/pdf/" + fileId + "/page/{pageNum}");
return ResponseEntity.ok(result);
}
}
前端JavaScript实现(轻量级方案)
如果不想后端处理太复杂,也可以使用纯前端库:
PDF.js实现
<!DOCTYPE html>
<html>
<head>PDF预览 - PDF.js</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.12.313/pdf.min.js"></script>
<style>
canvas {
border: 1px solid #ddd;
margin: 10px auto;
display: block;
}
</style>
</head>
<body>
<div class="controls">
<button onclick="prevPage()">上一页</button>
<span>第 <span id="currentPage">1</span> / <span id="totalPages">0</span> 页</span>
<button onclick="nextPage()">下一页</button>
</div>
<div id="pdfContainer">
<canvas id="pdfCanvas"></canvas>
</div>
<script>
let pdfDoc = null;
let pageNum = 1;
let pageRendering = false;
let pageNumPending = null;
const scale = 1.5;
const canvas = document.getElementById('pdfCanvas');
const ctx = canvas.getContext('2d');
// 加载PDF
pdfjsLib.getDocument('/api/pdf/sample.pdf').promise.then(function(pdfDoc_) {
pdfDoc = pdfDoc_;
document.getElementById('totalPages').textContent = pdfDoc.numPages;
renderPage(pageNum);
});
// 渲染页面
function renderPage(num) {
pageRendering = true;
pdfDoc.getPage(num).then(function(page) {
const viewport = page.getViewport({scale: scale});
canvas.height = viewport.height;
canvas.width = viewport.width;
const renderContext = {
canvasContext: ctx,
viewport: viewport
};
const renderTask = page.render(renderContext);
renderTask.promise.then(function() {
pageRendering = false;
if (pageNumPending !== null) {
renderPage(pageNumPending);
pageNumPending = null;
}
});
});
document.getElementById('currentPage').textContent = num;
}
function queueRenderPage(num) {
if (pageRendering) {
pageNumPending = num;
} else {
renderPage(num);
}
}
function prevPage() {
if (pageNum <= 1) return;
pageNum--;
queueRenderPage(pageNum);
}
function nextPage() {
if (pageNum >= pdfDoc.numPages) return;
pageNum++;
queueRenderPage(pageNum);
}
</script>
</body>
</html>
使用Viewer.js实现Office文档预览
<!DOCTYPE html>
<html>
<head>Office文档预览</title>
<script src="https://cdn.jsdelivr.net/npm/docsify/lib/docsify.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/docsify/themes/vue.css">
</head>
<body>
<div id="app">文档加载中...</div>
<script>
// 使用微软Office Online预览
function previewOfficeDoc(url, type) {
if (type === 'docx' || type === 'xlsx' || type === 'pptx') {
// 使用微软Office Online Viewer
const viewerUrl = `https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(url)}`;
window.open(viewerUrl, '_blank');
} else if (type === 'pdf') {
// 使用PDF.js
window.open('/pdf-viewer?file=' + encodeURIComponent(url), '_blank');
}
}
</script>
</body>
</html>
完整实操步骤
步骤1:创建Spring Boot项目
# 使用Spring Initializr或手动创建 mvn archetype:generate -DgroupId=com.example -DartifactId=file-preview -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false
步骤2:添加依赖(pom.xml)
<!-- 基础依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- PDF处理 -->
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>2.0.27</version>
</dependency>
<!-- 文档转换 -->
<dependency>
<groupId>com.github.jodconverter</groupId>
<artifactId>jodconverter-spring-boot-starter</artifactId>
<version>4.4.6</version>
</dependency>
<dependency>
<groupId>com.github.jodconverter</groupId>
<artifactId>jodconverter-local</artifactId>
<version>4.4.6</version>
</dependency>
步骤3:核心配置
server:
port: 8080
spring:
servlet:
multipart:
enabled: true
max-file-size: 50MB
max-request-size: 50MB
file:
upload-dir: ./uploads
preview-cache: ./preview-cache
jodconverter:
local:
enabled: true
office-home: /usr/lib/libreoffice
步骤4:启动测试
# 1. 启动Spring Boot应用
mvn spring-boot:run
# 2. 上传文件测试
curl -X POST -F "file=@/path/to/test.pdf" http://localhost:8080/upload
# 3. 访问预览页面
http://localhost:8080/preview/pdf/{fileId}
常见问题与优化
性能优化
// 使用缓存避免重复转换
@Cacheable(value = "previewCache", key = "#fileId")
public byte[] getPreviewImage(String fileId, int pageNum) {
// ... 转换逻辑
}
// 异步转换
@Async
public CompletableFuture<File> convertAsync(File inputFile) {
File pdfFile = convertToPdf(inputFile);
return CompletableFuture.completedFuture(pdfFile);
}
安全考虑
// 文件路径安全
public String validateFilePath(String fileName) {
// 防止路径穿越攻击
if (fileName.contains("..") || fileName.contains("/")) {
throw new SecurityException("非法文件名");
}
return fileName;
}
// 预览权限控制
@PreAuthorize("hasPermission(#fileId, 'preview')")
public ResponseEntity<?> previewDocument(String fileId) {
// ... 实现
}
大文件处理
// 限制转换页数
public byte[] pdfToImage(File pdfFile, int maxPages) throws IOException {
try (PDDocument document = PDDocument.load(pdfFile)) {
int pagesToRender = Math.min(document.getNumberOfPages(), maxPages);
// 只渲染前几页
}
}
// 流式处理
public void streamPreview(File file, HttpServletResponse response) {
response.setContentType("application/pdf");
try (InputStream is = new FileInputStream(file);
OutputStream os = response.getOutputStream()) {
IOUtils.copy(is, os);
}
}
在线预览的核心思路是:
- 后端解析:将文档转换为前端可显示格式(图片/PDF)
- 前端展示:使用HTML/CSS/JavaScript渲染显示
- 分页加载:按需加载,避免一次性加载大文件
选择方案时考虑:
- 简单场景:使用PDF.js或前端JS库
- 企业级场景:使用OpenOffice/LibreOffice转换
- 高性能场景:使用Aspose等商业库
记得添加缓存机制和并发控制,特别是对于频繁预览的文档!