Appearance
Spring MVC 文档视图:PDF 和 Excel 生成完全指南 📄
在现代 Web 应用开发中,我们经常需要为用户提供多种格式的数据输出。虽然 HTML 页面是最常见的展示方式,但有时用户需要下载 PDF 报告或 Excel 表格来进行离线查看或进一步处理。Spring MVC 为我们提供了优雅的解决方案来动态生成这些文档格式。
为什么需要文档视图? 🤔
NOTE
想象一下,如果没有文档视图功能,我们需要手动处理文档生成、HTTP 响应头设置、内容类型配置等繁琐工作。Spring MVC 的文档视图将这些复杂性封装起来,让我们专注于业务逻辑。
常见业务场景
- 财务报表:生成 PDF 格式的月度/年度财务报告
- 数据导出:将数据库查询结果导出为 Excel 文件
- 发票生成:为电商订单生成 PDF 发票
- 统计报告:生成包含图表的综合分析报告
核心原理与设计哲学 💡
Spring MVC 的文档视图基于模板方法模式设计,核心思想是:
- 统一的视图接口:所有文档视图都实现相同的
View
接口 - 内容类型自动处理:框架自动设置正确的
Content-Type
响应头 - 流式输出:直接将生成的文档流式传输给客户端
- 模板方法抽象:提供抽象基类,开发者只需实现文档构建逻辑
PDF 视图实现 📋
依赖配置
首先,我们需要添加 OpenPDF 依赖:
kotlin
dependencies {
implementation("com.github.librepdf:openpdf:1.3.30")
implementation("org.springframework.boot:spring-boot-starter-web")
}
xml
<dependency>
<groupId>com.github.librepdf</groupId>
<artifactId>openpdf</artifactId>
<version>1.3.30</version>
</dependency>
IMPORTANT
强烈推荐使用 OpenPDF 而不是过时的 iText 2.1.7,因为 OpenPDF 积极维护并修复了重要的安全漏洞。
创建 PDF 视图类
kotlin
import org.springframework.web.servlet.view.document.AbstractPdfView
import com.lowagie.text.Document
import com.lowagie.text.Paragraph
import com.lowagie.text.pdf.PdfWriter
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
/**
* 销售报告 PDF 视图
* 将销售数据渲染为 PDF 格式
*/
class SalesReportPdfView : AbstractPdfView() {
override fun buildPdfDocument(
model: Map<String, Any>,
doc: Document,
writer: PdfWriter,
request: HttpServletRequest,
response: HttpServletResponse
) {
// 从模型中获取销售数据
val salesData = model["salesData"] as List<SaleRecord>
val reportTitle = model["reportTitle"] as String
// 添加标题
doc.add(Paragraph(reportTitle).apply {
alignment = Paragraph.ALIGN_CENTER
})
// 添加销售记录
salesData.forEach { record ->
doc.add(Paragraph("${record.productName}: ${record.amount}"))
}
// 添加总计
val total = salesData.sumOf { it.amount }
doc.add(Paragraph("总计: $total").apply {
alignment = Paragraph.ALIGN_RIGHT
})
}
}
// 数据类定义
data class SaleRecord(
val productName: String,
val amount: Double,
val date: String
)
Controller 实现
kotlin
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.servlet.ModelAndView
@Controller
class ReportController {
@GetMapping("/reports/sales.pdf")
fun generateSalesReportPdf(): ModelAndView {
// 模拟从数据库获取销售数据
val salesData = listOf(
SaleRecord("笔记本电脑", 8999.0, "2024-01-15"),
SaleRecord("无线鼠标", 199.0, "2024-01-16"),
SaleRecord("机械键盘", 599.0, "2024-01-17")
)
// 准备模型数据
val model = mapOf(
"salesData" to salesData,
"reportTitle" to "2024年1月销售报告"
)
// 返回 PDF 视图
return ModelAndView(SalesReportPdfView(), model)
}
}
TIP
你也可以通过视图名称来引用 PDF 视图,只需在配置中注册视图解析器即可。
Excel 视图实现 📊
依赖配置
kotlin
dependencies {
implementation("org.apache.poi:poi:5.2.4")
implementation("org.apache.poi:poi-ooxml:5.2.4")
}
创建 Excel 视图类
kotlin
import org.springframework.web.servlet.view.document.AbstractXlsxView
import org.apache.poi.ss.usermodel.*
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
/**
* 用户数据 Excel 导出视图
* 支持 .xlsx 格式,具有更好的兼容性
*/
class UserDataExcelView : AbstractXlsxView() {
override fun buildExcelDocument(
model: Map<String, Any>,
workbook: Workbook,
request: HttpServletRequest,
response: HttpServletResponse
) {
// 设置文件名
response.setHeader("Content-Disposition", "attachment; filename=user_data.xlsx")
// 获取用户数据
val users = model["users"] as List<User>
// 创建工作表
val sheet = workbook.createSheet("用户数据")
// 创建标题样式
val headerStyle = workbook.createCellStyle().apply {
fillForegroundColor = IndexedColors.GREY_25_PERCENT.index
fillPattern = FillPatternType.SOLID_FOREGROUND
setBorderBottom(BorderStyle.THIN)
}
// 创建标题行
val headerRow = sheet.createRow(0)
val headers = arrayOf("ID", "姓名", "邮箱", "注册日期")
headers.forEachIndexed { index, header ->
headerRow.createCell(index).apply {
setCellValue(header)
cellStyle = headerStyle
}
}
// 填充数据行
users.forEachIndexed { rowIndex, user ->
val row = sheet.createRow(rowIndex + 1)
row.createCell(0).setCellValue(user.id.toDouble())
row.createCell(1).setCellValue(user.name)
row.createCell(2).setCellValue(user.email)
row.createCell(3).setCellValue(user.registrationDate)
}
// 自动调整列宽
for (i in 0 until headers.size) {
sheet.autoSizeColumn(i)
}
}
}
data class User(
val id: Long,
val name: String,
val email: String,
val registrationDate: String
)
流式 Excel 视图(大数据量优化)
当需要处理大量数据时,可以使用流式视图来优化内存使用:
kotlin
import org.springframework.web.servlet.view.document.AbstractXlsxStreamingView
/**
* 大数据量 Excel 导出视图
* 使用流式处理,适合大量数据导出
*/
class LargeDataExcelView : AbstractXlsxStreamingView() {
override fun buildExcelDocument(
model: Map<String, Any>,
workbook: Workbook,
request: HttpServletRequest,
response: HttpServletResponse
) {
response.setHeader("Content-Disposition", "attachment; filename=large_data.xlsx")
val sheet = workbook.createSheet("大数据")
val dataProvider = model["dataProvider"] as DataProvider
// 创建标题行
val headerRow = sheet.createRow(0)
dataProvider.getHeaders().forEachIndexed { index, header ->
headerRow.createCell(index).setCellValue(header)
}
// 流式处理数据
var rowIndex = 1
dataProvider.processData { record ->
val row = sheet.createRow(rowIndex++)
record.forEachIndexed { cellIndex, value ->
row.createCell(cellIndex).setCellValue(value.toString())
}
}
}
}
interface DataProvider {
fun getHeaders(): List<String>
fun processData(processor: (List<Any>) -> Unit)
}
实际业务场景示例 🏢
电商订单发票生成
完整的订单发票 PDF 生成示例
kotlin
/**
* 订单发票 PDF 视图
* 生成包含订单详情、客户信息和计费明细的发票
*/
class OrderInvoicePdfView : AbstractPdfView() {
override fun buildPdfDocument(
model: Map<String, Any>,
doc: Document,
writer: PdfWriter,
request: HttpServletRequest,
response: HttpServletResponse
) {
val order = model["order"] as Order
val customer = model["customer"] as Customer
// 设置文件名
response.setHeader("Content-Disposition",
"attachment; filename=invoice_${order.orderNumber}.pdf")
// 发票标题
doc.add(Paragraph("发票", Font(Font.HELVETICA, 18, Font.BOLD)).apply {
alignment = Paragraph.ALIGN_CENTER
})
// 客户信息
doc.add(Paragraph("\n客户信息:"))
doc.add(Paragraph("姓名: ${customer.name}"))
doc.add(Paragraph("地址: ${customer.address}"))
doc.add(Paragraph("电话: ${customer.phone}"))
// 订单信息
doc.add(Paragraph("\n订单信息:"))
doc.add(Paragraph("订单号: ${order.orderNumber}"))
doc.add(Paragraph("订单日期: ${order.orderDate}"))
// 创建商品明细表格
val table = Table(4)
table.addCell("商品名称")
table.addCell("数量")
table.addCell("单价")
table.addCell("小计")
order.items.forEach { item ->
table.addCell(item.productName)
table.addCell(item.quantity.toString())
table.addCell("¥${item.price}")
table.addCell("¥${item.quantity * item.price}")
}
doc.add(table)
// 总计
doc.add(Paragraph("\n总计: ¥${order.totalAmount}",
Font(Font.HELVETICA, 12, Font.BOLD)))
}
}
data class Order(
val orderNumber: String,
val orderDate: String,
val items: List<OrderItem>,
val totalAmount: Double
)
data class OrderItem(
val productName: String,
val quantity: Int,
val price: Double
)
data class Customer(
val name: String,
val address: String,
val phone: String
)
数据分析报表导出
kotlin
@RestController
class AnalyticsController {
@GetMapping("/analytics/export")
fun exportAnalytics(
@RequestParam startDate: String,
@RequestParam endDate: String,
@RequestParam format: String
): ModelAndView {
// 查询分析数据
val analyticsData = analyticsService.getAnalytics(startDate, endDate)
val model = mapOf(
"analyticsData" to analyticsData,
"dateRange" to "$startDate 至 $endDate",
"generatedAt" to LocalDateTime.now().toString()
)
return when (format.lowercase()) {
"pdf" -> ModelAndView(AnalyticsPdfView(), model)
"excel" -> ModelAndView(AnalyticsExcelView(), model)
else -> throw IllegalArgumentException("不支持的格式: $format")
}
}
}
最佳实践与优化建议 ⚡
1. 性能优化
WARNING
生成大型文档可能消耗大量内存和CPU资源,需要合理优化。
kotlin
/**
* 优化的文档视图基类
* 包含通用的性能优化措施
*/
abstract class OptimizedDocumentView : AbstractPdfView() {
companion object {
private val logger = LoggerFactory.getLogger(OptimizedDocumentView::class.java)
}
override fun buildPdfDocument(
model: Map<String, Any>,
doc: Document,
writer: PdfWriter,
request: HttpServletRequest,
response: HttpServletResponse
) {
val startTime = System.currentTimeMillis()
try {
// 设置缓存控制
response.setHeader("Cache-Control", "private, max-age=3600")
// 执行具体的文档构建
buildDocument(model, doc, writer, request, response)
} finally {
val duration = System.currentTimeMillis() - startTime
logger.info("文档生成耗时: ${duration}ms")
}
}
abstract fun buildDocument(
model: Map<String, Any>,
doc: Document,
writer: PdfWriter,
request: HttpServletRequest,
response: HttpServletResponse
)
}
2. 错误处理
kotlin
@ControllerAdvice
class DocumentExceptionHandler {
@ExceptionHandler(DocumentException::class)
fun handleDocumentException(ex: DocumentException): ResponseEntity<String> {
logger.error("文档生成失败", ex)
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("文档生成失败,请稍后重试")
}
}
3. 配置视图解析器
kotlin
@Configuration
class ViewConfiguration {
@Bean
fun pdfViewResolver(): BeanNameViewResolver {
return BeanNameViewResolver().apply {
order = 1
}
}
@Bean("salesReportPdf")
fun salesReportPdfView(): SalesReportPdfView {
return SalesReportPdfView()
}
@Bean("userDataExcel")
fun userDataExcelView(): UserDataExcelView {
return UserDataExcelView()
}
}
总结 🎯
Spring MVC 的文档视图功能为我们提供了强大而灵活的文档生成能力:
核心优势
- 统一抽象:通过抽象基类简化文档生成逻辑
- 自动处理:框架自动处理 HTTP 响应头和内容类型
- 灵活扩展:可以轻松支持新的文档格式
- 性能优化:支持流式处理大量数据
通过合理使用这些功能,我们可以轻松为用户提供丰富的文档导出体验,满足各种业务场景的需求。记住,选择合适的文档库版本和优化策略对于生产环境的性能至关重要! 🚀