本文目录导读:

- 技术选型方案
- 实操案例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等商业库
记得添加缓存机制和并发控制,特别是对于频繁预览的文档!
版权声明:足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播