1. 项目缘起:为什么“通用导出”是个技术活
最近在重构一个老项目的报表模块,产品经理提了个需求:“用户希望后台的列表数据,不仅能看,还能一键导出成Excel、Word或者PDF,最好还能打个包。” 听起来很合理,对吧?但当我打开旧代码,看到的是十几个Controller里散落着各种HSSFWorkbook、XWPFDocument、iText的硬编码,格式混乱,内存泄漏的警告时,我意识到这远不是加个按钮那么简单。这其实是很多Web后台系统都会遇到的经典问题:数据导出功能。
数据导出,本质上是一个将结构化数据(通常是数据库查询结果或内存对象)序列化为特定格式文件,并提供给用户下载的过程。它不像CRUD那样有Spring Data JPA这种“全家桶”,也不像前端组件那样有成熟的UI库。它处在业务逻辑和IO操作的交叉点,需要考虑性能、格式兼容性、用户体验和代码维护性。一个设计良好的通用导出功能,应该像瑞士军刀一样,接口统一但能力多样,能够应对“导出一万条记录到Excel 2003”、“将合同详情生成为PDF”、“批量下载图片并打包为ZIP”等五花八门的需求。
从网络上的讨论热度也能看出,这是开发者们常踩的坑。比如“数据太长从系统导出如何变数值”暴露了Excel数字格式处理的问题;“excel sumifs函数的使用”则暗示了用户可能希望对导出的数据进行二次分析;“springboot解决pdf xss攻击”提醒我们,即使是导出功能,安全性也不容忽视;而“poi设置word表格单元格宽度”、“pdf转word后边框线消失”这类问题,则直指不同文档格式渲染的兼容性痛点。
所以,这次我们不聊某个特定库的API调用,而是从架构层面,拆解一个高可用、易扩展的通用Web系统数据导出功能该如何设计与实现。目标很明确:设计一套方案,让后续无论是导出Excel 2003/2007、Word、PDF还是ZIP,都像调用一个简单服务一样轻松,同时保证性能稳健、内存安全。
2. 核心架构设计:从“散兵游勇”到“标准化部队”
面对多格式导出的需求,最糟糕的做法就是每个导出需求写一套独立的代码。这不仅造成大量重复,而且一旦底层库(比如Apache POI)升级,修改点将会遍布全项目。我们的目标是构建一个清晰的分层架构,将变与不变的部分分离。
2.1 总体架构与核心组件
我设计的核心架构分为五层,自顶向下分别是:Web层、服务编排层、导出引擎层、数据适配层和文件存储层。这个结构借鉴了模板方法模式和策略模式的思想。
[用户请求] -> [Web层: 接收参数] -> [服务编排层: 组装上下文] -> [导出引擎层: 选择执行器] -> [数据适配层: 获取并转换数据] -> [文件存储层: 生成物理文件] -> [返回下载流]Web层最简单,就是一个标准的Controller,它只做三件事:接收前端传来的导出参数(如格式、查询条件)、调用统一的导出服务、将服务返回的文件流写入HttpServletResponse。这里的关键是定义一套通用的请求参数DTO。
服务编排层是大脑。它根据传入的exportType(如EXCEL_2007,PDF)和templateCode(可选,指定使用哪个模板),创建一个ExportContext上下文对象。这个对象包含了本次导出任务的所有元信息:谁导的、导什么格式、用什么模板、查询条件是什么、甚至包括一些动态配置(如文件名、Sheet名)。它的职责是准备好“配方”,但不亲自“下厨”。
导出引擎层是厨房,也是核心。这里定义了一个ExportEngine接口,其核心方法是export(ExportContext context)。针对每种导出格式,我们提供一个具体的ExportExecutor(执行器),如Excel2007ExportExecutor、PdfExportExecutor。引擎层通过一个注册表(通常是一个Map,key是exportType)来管理这些执行器。它的职责就是找到正确的“厨师”(执行器),并把“配方”(上下文)交给他。
数据适配层负责提供“食材”。它定义了一个DataAdapter接口,核心方法是fetchData(ExportContext context)。不同的数据来源(如直接数据库查询、调用外部API、从缓存中获取)对应不同的适配器实现,如JdbcDataAdapter、ApiDataAdapter。执行器并不关心数据从哪里来,它只关心适配器返回的是一个结构化的数据列表(通常是List<Map<String, Object>>或List<YourEntity>)。这实现了数据获取逻辑与导出逻辑的解耦。
文件存储层负责处理生成的“成品”。最简单的就是直接使用ByteArrayOutputStream在内存中生成文件流,然后由Web层输出。但对于大文件或需要异步导出的场景,我们需要引入临时文件或对象存储(如MinIO、AWS S3)。这一层抽象出一个FileStorageService,提供store和fetch方法。执行器生成文件后,可以调用存储服务保存,并返回一个文件标识(如UUID),Web层再根据这个标识去获取文件流。这对于实现“异步导出”、“导出历史记录”功能至关重要。
2.2 核心模型定义:上下文(Context)与配置(Config)
要让这套架构运转起来,两个核心模型的设计是关键。
首先是ExportContext(导出上下文)。它贯穿整个导出流程,是各层之间传递信息的载体。我通常会把它设计成一个不可变对象(使用Builder模式构建),确保流程中的一致性。它的主要字段包括:
requestId/taskId: 唯一标识本次导出任务,用于日志追踪和异步查询。exportType: 枚举值,如EXCEL_XLS,EXCEL_XLSX,WORD_DOCX,PDF,ZIP。templateCode: 字符串,指向一个预定义的模板配置。可以为空,表示使用默认格式。queryParams:Map<String, Object>,存放前端传递的所有查询条件。operator: 操作人信息。dynamicConfig: 一个ExportConfig对象,存放本次导出特有的配置。
其次是ExportConfig(导出配置)。它定义了“如何导出”的细节,这部分配置可以来自数据库中的模板,也可以由前端动态指定。主要配置项有:
fileName: 输出文件的名称(不含后缀)。sheetName/documentTitle: 针对Excel或Word的标题。columns: 这是一个核心配置,定义了导出的列。每个列包含:字段名(对应数据对象的key)、列标题、列宽(Excel)、数据类型(String, Number, Date)、格式化模式(如yyyy-MM-dd)、单元格样式(如居中、加粗)等。通过配置columns,我们可以实现动态列导出,即同一份数据,根据不同场景展示不同的列。headerStyle,cellStyle: 定义表头和单元格的默认样式。pageSize/orientation: 针对PDF的页面配置。watermark: 水印信息。maxRowsPerSheet: Excel每个Sheet的最大行数限制(Excel 2003是65535,2007+是104万,但实际性能考虑通常设得更小)。
通过将可变的部分(数据、列配置、样式)抽象到ExportContext和ExportConfig中,我们的执行器(ExportExecutor)就可以变得非常通用和纯粹。
3. 分格式执行器实现详解
有了统一的架构和模型,接下来我们看看针对不同格式,ExportExecutor如何具体实现。这里会结合Apache POI、iText等常用库,分享一些实战中的关键点和坑。
3.1 Excel导出执行器:处理海量数据与格式兼容
Excel是导出需求中最常见的格式,我们通常需要支持老旧的.xls(Excel 2003)和现代的.xlsx(Excel 2007+)两种格式。Apache POI库是Java生态的不二之选,但直接用SXSSFWorkbook和HSSFWorkbook写业务代码会很快变得难以维护。
3.1.1 执行器设计与流式写入我的ExcelExportExecutor会实现统一的ExportExecutor接口。在export方法内部,首先根据exportType创建对应的工作簿对象(SXSSFWorkbook用于.xlsx,HSSFWorkbook用于.xls)。这里第一个重要选择是:对于.xlsx,必须使用SXSSFWorkbook进行流式写入。XSSFWorkbook会将整个工作簿保存在内存中,导出几万行数据就可能导致OOM。SXSSFWorkbook的原理是只在内存中保留一部分行(默认100行),之前的行会写入临时文件,完美支持海量数据导出。
public class ExcelExportExecutor implements ExportExecutor { @Override public ExportResult export(ExportContext context) throws ExportException { Workbook workbook; if (context.getExportType() == ExportType.EXCEL_XLSX) { // 使用SXSSFWorkbook,设置rowAccessWindowSize为100 workbook = new SXSSFWorkbook(100); ((SXSSFWorkbook) workbook).setCompressTempFiles(true); // 压缩临时文件 } else { workbook = new HSSFWorkbook(); } // ... 后续操作 } }3.1.2 动态列与样式管理接下来,根据context.getConfig().getColumns()来创建表头。这里不能简单循环创建Cell并设值。我们需要一个StyleManager来集中管理单元格样式。POI的CellStyle对象是隶属于Workbook的,创建过多会导致内存增长。我的做法是,根据数据类型(文本、数字、日期、百分比)和预设样式(标题样式、强调样式)创建有限的几种CellStyle,缓存起来,在需要时直接应用给单元格。这能显著减少内存占用并提升性能。
对于数据填充,通过DataAdapter获取到List<Map<String, Object>>格式的数据后,遍历列表,根据columns配置中定义的字段名,从Map中取出值,填入对应的单元格。这里要注意数据类型转换和格式化:从数据库出来的java.sql.Timestamp要转换成java.util.Date,数字类型要防止科学计数法(通过设置单元格格式为"0"或"0.00"),超长的文本要处理(Excel单个单元格限制32767字符)。
3.1.3 分Sheet与性能优化当数据量极大时(比如超过50万行),即使使用SXSSFWorkbook,全部放在一个Sheet里也会导致文件打开缓慢。我们需要实现自动分Sheet功能。在遍历数据时,设置一个计数器,当当前Sheet的行数(含表头)达到maxRowsPerSheet(例如10万)时,就创建新的Sheet,并重新创建表头。 另一个性能优化点是禁用公式计算和自动调整列宽。在导出完成后,可以调用sheet.autoSizeColumn(columnIndex),但这对于大数据量非常耗时。更好的做法是根据列标题长度和预估的数据长度,在配置中预设一个合理的列宽,或者在导出完成后,在后台线程进行异步调整并缓存结果供下次使用。
3.1.4 处理“数据太长变数值”问题这是网络热词中提到的典型问题。当一长串数字(如身份证号、银行卡号)被导出到Excel时,Excel会默认将其识别为数字,并以科学计数法显示,且超过15位的部分会被置零。解决方案是在配置该列时,明确指定其dataType为STRING,并且在POI中,在设置单元格值前,先设置单元格格式为文本格式(CellStyle的setDataFormat方法使用BuiltinFormats.getBuiltinFormat("text")),或者更直接地,在值前加上一个制表符"\t",强制Excel将其解释为文本。
// 在填充单元格值时 if (columnConfig.getDataType() == DataType.STRING) { // 方式一:设置单元格格式为文本 CellStyle textStyle = styleManager.getTextStyle(); cell.setCellStyle(textStyle); cell.setCellValue(String.valueOf(value)); // 方式二(简单粗暴):值前加制表符 // cell.setCellValue("\t" + String.valueOf(value)); }3.2 Word导出执行器:模板驱动与内容组装
Word导出通常用于生成格式固定的报告、合同等。与Excel的结构化表格不同,Word文档更注重段落、样式和混合内容。我推荐采用模板驱动的方式。
3.2.1 模板引擎的选择我们可以使用Apache POI的XWPFDocument来操作.docx文件,但直接通过API构建复杂文档非常繁琐。更好的方法是准备一个.docx文件作为模板,在需要动态填充的位置放置占位符,如${customerName}、${tableData}。然后使用像poi-tl(POI Template Lite)这样的模板引擎库。它语法简单,功能强大,支持文本、图片、表格、列表等元素的替换和循环。
在WordExportExecutor中,流程如下:
- 从类路径或文件系统加载模板文件(
.docx)。 - 根据
ExportContext中的查询参数,通过DataAdapter获取数据。数据可能是一个包含各种字段的Map(用于替换文本),也可能是一个List(用于循环生成表格行)。 - 构造一个
Map<String, Object>数据模型,键是模板中的占位符,值是实际数据或特定的渲染策略(如new MiniTableRenderData(headerList, rowList))。 - 调用
poi-tl的XWPFTemplate.compile(templateStream).render(dataModel).writeTo(outputStream)。 - 关闭流,返回结果。
3.2.2 处理表格与样式对于动态表格,poi-tl的MiniTableRenderData可以满足大部分需求。但要注意表格样式。模板中定义好的表格样式(如边框、底色、字体)在渲染后通常会被保留。如果对样式有精细要求,最好在Word模板中预先设计好表格样式并应用,而不是在代码中硬编码。
关于网络热词中提到的“poi设置word表格单元格宽度”问题,如果必须通过POI原生API调整,可以在渲染后获取XWPFTable对象,遍历行和列,使用table.getColumnWidth(i)和table.setColumnWidth(i, newWidth)来调整。但更建议在模板设计阶段就固定好列宽。
3.3 PDF导出执行器:精准排版与跨平台一致性
PDF导出常用于需要打印、归档或严格保持格式不变的场景。iText是Java领域最强大的PDF库,但它的API较为底层。对于简单的表格列表,我们可以借鉴Excel导出的思路,用iText画出来。但对于复杂的、包含混合布局(文字、图片、表格、水印)的报告,我依然推荐模板驱动。
3.3.1 基于HTML/CSS的模板方案我的首选方案是使用Flying Saucer(基于iText)或OpenHTMLToPDF这类库,它们可以将HTML+CSS渲染成PDF。这样做的好处是:
- 开发效率高:前端工程师可以直接用HTML/CSS设计模板,后端只需填充数据。
- 样式灵活:CSS能实现非常精细的排版控制。
- 维护方便:修改模板无需重新编译Java代码。
在PdfExportExecutor中:
- 准备一个HTML模板文件,使用Thymeleaf、FreeMarker等模板引擎的语法嵌入变量,如
<span th:text="${reportDate}"></span>。 - 通过
DataAdapter获取数据,并填充到模板引擎的数据模型中。 - 使用模板引擎将数据模型与HTML模板合并,生成最终的HTML字符串。
- 使用
Flying Saucer的ITextRenderer,将HTML字符串渲染到PDF文档,并写入输出流。
// 示例:使用Thymeleaf + Flying Saucer ITextRenderer renderer = new ITextRenderer(); // 加载HTML字符串 renderer.setDocumentFromString(processedHtml); renderer.layout(); renderer.createPDF(outputStream);3.3.2 解决字体与样式问题这是PDF导出的最大挑战。服务器上通常没有中文字体,直接渲染会导致中文不显示。解决方案是将字体文件(如simsun.ttf宋体)打包到项目的resources/fonts目录下,并在渲染前注册给ITextRenderer。
// 注册中文字体 String fontPath = this.getClass().getResource("/fonts/simsun.ttf").getPath(); renderer.getFontResolver().addFont(fontPath, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);另外,CSS中的某些属性(如position: fixed用于页眉页脚)可能需要特定版本的库才支持,需要仔细测试。
3.3.3 安全性考虑正如热词“springboot解决pdf xss攻击”所提示的,如果PDF内容包含用户输入,就必须防范XSS攻击。在使用HTML模板方案时,绝对不要将未经处理的用户输入直接拼接到HTML中。必须通过模板引擎的转义功能(Thymeleaf的th:text默认会转义)或者使用OWASP Java Encoder等工具对动态内容进行编码。
3.4 ZIP打包执行器:聚合多个文件
ZIP导出通常不是独立的,而是作为其他导出功能的“包装器”。例如,用户选择导出多张图片,或者同时导出Excel和PDF版本。ZipExportExecutor的设计思路略有不同。
它的export方法接收的ExportContext中,queryParams可能包含一个fileEntries列表,每个条目描述了要打包的文件(如名称、获取文件流的方式)。执行器的核心逻辑是:
- 创建一个
ZipOutputStream包装最终的输出流。 - 遍历
fileEntries,对于每个条目:- 通过条目中定义的“文件提供者”(可能是一个URL,一个本地文件路径,或者是调用另一个
ExportExecutor生成的内存流)获取InputStream。 - 在ZIP流中创建一个新的
ZipEntry,设置好文件名。 - 将输入流写入ZIP流。
- 通过条目中定义的“文件提供者”(可能是一个URL,一个本地文件路径,或者是调用另一个
- 关闭所有资源。
这里的关键是流式处理。我们不能等到所有文件都生成到内存或磁盘后再打包,而应该边生成边写入ZIP流,以最小化内存占用。这要求上游的“文件提供者”能够以流式方式提供数据。
4. 高级特性与生产环境考量
一个基础的导出框架实现后,要投入生产环境,还必须考虑性能、稳定性、用户体验等高级问题。
4.1 异步导出与任务管理
对于耗时较长的导出任务(如百万级数据导出),不能让用户在前端同步等待,否则会导致请求超时。必须实现异步导出。
- 任务提交:Web层接收到导出请求后,立即生成一个唯一的
taskId,提交任务到线程池或消息队列(如RabbitMQ、RocketMQ),并立即将taskId返回给前端。 - 后台执行:后台消费者从队列中取出任务,执行真正的导出逻辑,生成文件后存入
FileStorageService(如临时目录或对象存储),并将任务状态(完成、失败、进度)更新到数据库或缓存中。 - 状态查询与下载:前端轮询一个状态查询接口,传入
taskId。当查询到任务状态为“完成”时,返回可下载的URL或触发文件下载。
这就需要我们扩展ExportContext,包含任务状态信息,并建立一个ExportTaskService来管理任务的生命周期。
4.2 内存管理与资源泄漏防护
导出功能是内存泄漏的重灾区。
- 流必须关闭:对
SXSSFWorkbook、ZipOutputStream、各种InputStream/OutputStream,必须使用try-with-resources语句确保关闭。 - 清理临时文件:
SXSSFWorkbook和某些PDF库会生成临时文件。必须在finally块中或使用完工作簿后,调用workbook.dispose()或手动删除临时文件。 - 限制并发与超时:在服务编排层或使用线程池时,要设置导出任务的最大并发数和单个任务的超时时间,防止系统资源被耗尽。
- 监控与告警:在关键点(如开始导出、写入大量数据、完成导出)记录日志,并监控JVM堆内存和直接内存的使用情况。可以设置阈值,当单次导出数据量过大时,强制切换到异步模式或直接拒绝。
4.3 模板化与动态配置
将列定义、样式、文件命名规则等抽取到数据库或配置中心(如Apollo、Nacos),实现真正的动态配置。管理后台可以提供一个界面,让产品经理或运营人员自行配置新的导出模板,而无需开发介入。ExportContext中的templateCode就是用来查找这些配置的键。
4.4 文件存储与清理策略
对于异步导出生成的文件,不能一直存放在服务器上。FileStorageService需要实现清理策略,例如:
- 为每个文件设置过期时间(如生成后24小时)。
- 后台定时任务扫描并删除过期文件。
- 如果使用对象存储,可以利用其生命周期管理策略自动删除。
4.5 应对网络热词中的具体问题
- “数据太长从系统导出如何变数值”:已在Excel执行器部分阐述,核心是预定义为文本格式。
- “word表格导出pdf后边框线消失”:这通常是样式转换问题。如果Word转PDF是通过另存为或某些不完美的转换库实现的,边框样式可能丢失。最可靠的方法是,不要经过Word中转。如果最终需要PDF,应该直接用HTML+CSS或iText生成PDF。如果源数据是Word,则使用支持直接处理Word模板并输出PDF的库(如Aspose,但它是商业库),或者先将Word内容提取并重新用PDF模板渲染。
- “excel被选择的单元格显示内容”:这个需求可能是指导出时,高亮或标记出某些符合条件的单元格。这可以在
ExcelExportExecutor的数据填充循环中实现。根据业务规则判断当前单元格的值,如果满足条件,则动态创建一个特殊的CellStyle(如设置背景色为黄色)并应用给该单元格。 - “批量导出”:这通常由
ZipExportExecutor配合异步任务来完成。前端传递一个导出项的列表,后端为每一项创建一个子任务,最后打包。
5. 实战整合与代码结构示意
最后,让我们看一个高度简化的整合示例,展示如何将上述模块串联起来。假设我们使用Spring Boot框架。
首先,定义核心模型和枚举。
// 导出类型枚举 public enum ExportType { EXCEL_XLS("application/vnd.ms-excel", "xls"), EXCEL_XLSX("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "xlsx"), PDF("application/pdf", "pdf"), WORD("application/vnd.openxmlformats-officedocument.wordprocessingml.document", "docx"), ZIP("application/zip", "zip"); // ... 构造函数、getter } // 导出上下文 @Data @Builder public class ExportContext { private String taskId; private ExportType exportType; private String templateCode; private Map<String, Object> queryParams; private ExportConfig config; private Operator operator; // ... 其他上下文信息,如异步任务状态 } // 导出配置 @Data public class ExportConfig { private String fileName; private List<ColumnConfig> columns; // ... 其他样式、分页配置 }其次,定义执行器接口和服务。
// 执行器接口 public interface ExportExecutor { ExportResult export(ExportContext context) throws ExportException; } // 导出服务(编排层) @Service public class ExportService { @Autowired private Map<String, ExportExecutor> executorMap; // Spring会自动将ExportExecutor的实现类注入,key为bean name @Autowired private DataAdapterFactory dataAdapterFactory; public ExportResult doExport(ExportContext context) { // 1. 根据类型获取执行器 ExportExecutor executor = executorMap.get(context.getExportType().getExecutorBeanName()); if (executor == null) { throw new UnsupportedExportTypeException(); } // 2. (可选)根据templateCode从数据库加载完整Config,覆盖或补充context中的config // 3. 执行导出 return executor.export(context); } }然后,一个具体的Excel执行器实现。
@Component("excelXlsxExecutor") // Bean名称,用于被ExportService注入 public class ExcelXlsxExportExecutor implements ExportExecutor { @Autowired private StyleManager styleManager; @Override public ExportResult export(ExportContext context) throws ExportException { try (SXSSFWorkbook workbook = new SXSSFWorkbook(100)) { // 1. 创建Sheet,设置名称 Sheet sheet = workbook.createSheet(context.getConfig().getSheetName()); // 2. 创建表头行,应用StyleManager中的标题样式 Row headerRow = sheet.createRow(0); List<ColumnConfig> columns = context.getConfig().getColumns(); for (int i = 0; i < columns.size(); i++) { Cell cell = headerRow.createCell(i); cell.setCellValue(columns.get(i).getHeader()); cell.setCellStyle(styleManager.getHeaderStyle()); // 预设列宽 sheet.setColumnWidth(i, columns.get(i).getWidth() * 256); } // 3. 通过DataAdapter获取数据 DataAdapter dataAdapter = dataAdapterFactory.getAdapter(context); List<Map<String, Object>> dataList = dataAdapter.fetchData(context); // 4. 填充数据 int rowNum = 1; for (Map<String, Object> rowData : dataList) { Row row = sheet.createRow(rowNum++); for (int i = 0; i < columns.size(); i++) { ColumnConfig col = columns.get(i); Object value = rowData.get(col.getField()); Cell cell = row.createCell(i); // 根据列配置处理值和样式 processCellValue(cell, value, col); } // 5. 分Sheet逻辑(略) } // 6. 将workbook写入ByteArrayOutputStream ByteArrayOutputStream bos = new ByteArrayOutputStream(); workbook.write(bos); return ExportResult.builder().fileName(context.getConfig().getFileName() + ".xlsx") .contentType(ExportType.EXCEL_XLSX.getContentType()) .data(bos.toByteArray()).build(); } catch (Exception e) { throw new ExportException("Excel导出失败", e); } } private void processCellValue(Cell cell, Object value, ColumnConfig col) { // 处理数据类型、格式化、样式等 // 例如处理长数字文本 if (col.getDataType() == DataType.STRING && value instanceof Number) { // 防止长数字被科学计数 cell.setCellStyle(styleManager.getTextStyle()); cell.setCellValue(String.valueOf(value)); } else if (value instanceof Date) { cell.setCellValue((Date) value); cell.setCellStyle(styleManager.getDateStyle()); } else { // ... 其他类型处理 } } }最后,在Controller中调用。
@RestController @RequestMapping("/api/export") public class ExportController { @Autowired private ExportService exportService; @PostMapping("/do") public void export(@RequestBody ExportRequest request, HttpServletResponse response) throws IOException { // 1. 构建ExportContext ExportContext context = ExportContext.builder() .exportType(request.getExportType()) .templateCode(request.getTemplateCode()) .queryParams(request.getQueryParams()) .config(buildConfig(request)) // 根据请求构建基础配置 .operator(getCurrentOperator()) .build(); // 2. 调用导出服务 ExportResult result = exportService.doExport(context); // 3. 设置响应头,输出文件流 response.setContentType(result.getContentType()); response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(result.getFileName(), "UTF-8")); response.getOutputStream().write(result.getData()); response.flushBuffer(); } }这个示例省略了异常处理、异步、模板加载等细节,但展示了核心的流程。在实际项目中,你需要根据业务复杂度,不断完善各个组件,例如增加缓存、更精细的权限控制、导出量统计等功能。