Vaadin 7/8中如何用Upload组件在后台线程上传大文件?
Vaadin 7/8 Upload组件后台线程上传实现方案
核心原理
Vaadin的Upload组件默认在请求处理线程中执行文件接收逻辑,该线程会持有VaadinSession锁,导致整个会话被阻塞(无法处理其他标签页的请求)。解决思路是:
- 在请求线程中快速接收文件到临时存储(内存/临时文件)
- 将实际的文件上传逻辑(如存储到服务器、第三方服务)移交到后台线程
- 立即释放请求线程的会话锁,让会话可以处理其他请求
- 后台线程完成后,通过
VaadinSession.access()安全更新UI状态
具体实现步骤
1. 实现Upload的Receiver,暂存文件到临时文件
Receiver负责接收上传的输入流,这里我们将流写入临时文件,避免直接在请求线程中处理耗时操作:
public class FileUploadReceiver implements Receiver { private File tempFile; @Override public OutputStream receiveUpload(String filename, String mimeType) { try { // 创建临时文件,保留原文件后缀 tempFile = File.createTempFile("vaadin-upload-", "." + FilenameUtils.getExtension(filename)); return new FileOutputStream(tempFile); } catch (IOException e) { throw new RuntimeException("创建上传临时文件失败", e); } } public File getTempFile() { return tempFile; } }
2. 初始化Upload组件,绑定监听器
设置Upload的接收器、开始/成功/失败监听器,并在文件接收完成后启动后台线程:
public class CsvFilesComponent extends VerticalLayout { private static final Logger LOG = LoggerFactory.getLogger(CsvFilesComponent.class); private final ProgressBar uploadProgress = new ProgressBar(); private final Label statusLabel = new Label("等待上传..."); public CsvFilesComponent() { FileUploadReceiver receiver = new FileUploadReceiver(); Upload upload = new Upload("选择CSV文件", receiver); // 配置上传组件 upload.setButtonCaption("开始上传"); upload.addStartedListener(this::onUploadStarted); upload.addSucceededListener(event -> onUploadSucceeded(receiver, event.getFilename())); upload.addFailedListener(this::onUploadFailed); // 添加UI组件到布局 addComponents(statusLabel, uploadProgress, upload); } private void onUploadStarted(StartedEvent event) { statusLabel.setValue("正在接收文件..."); uploadProgress.setValue(0f); LOG.info("Starting upload of file [{}], session locked? {}", event.getFilename(), VaadinSession.getCurrent().hasLock()); } private void onUploadSucceeded(FileUploadReceiver receiver, String filename) { File tempFile = receiver.getTempFile(); if (tempFile == null || !tempFile.exists()) { statusLabel.setValue("文件接收失败"); return; } // 启动后台线程处理实际上传逻辑,当前请求线程将尽快释放会话锁 new Thread(() -> handleBackgroundUpload(tempFile, filename)).start(); LOG.info("Succeeded in uploading of file [{}], session locked? {}", filename, VaadinSession.getCurrent().hasLock()); statusLabel.setValue("文件接收完成,后台上传中..."); } private void onUploadFailed(FailedEvent event) { statusLabel.setValue("上传失败: " + event.getReason().getMessage()); uploadProgress.setValue(0f); } }
3. 后台线程处理实际上传逻辑
在后台线程中执行文件上传(如存储到本地目录、云存储等),完成后安全更新UI:
private void handleBackgroundUpload(File tempFile, String filename) { VaadinSession session = VaadinSession.getCurrent(); try { // 后台线程未持有会话锁,此处hasLock应为false LOG.info("Starting background upload of file [{}], session locked? {}", filename, session.hasLock()); // 模拟实际上传操作:将临时文件复制到目标目录 File targetDir = new File("/path/to/your/upload/directory"); if (!targetDir.exists()) { targetDir.mkdirs(); } File targetFile = new File(targetDir, filename); // 带进度的文件复制,用于更新进度条 try (InputStream in = new FileInputStream(tempFile); OutputStream out = new FileOutputStream(targetFile)) { byte[] buffer = new byte[4096]; long totalBytes = tempFile.length(); long uploadedBytes = 0; int bytesRead; while ((bytesRead = in.read(buffer)) != -1) { out.write(buffer, 0, bytesRead); uploadedBytes += bytesRead; float progress = (float) uploadedBytes / totalBytes; // 安全更新UI:必须通过VaadinSession.access()执行UI操作 session.access(() -> uploadProgress.setValue(progress)); // 可选:避免过于频繁更新UI,增加短暂延迟 Thread.sleep(10); } } // 上传完成,更新UI状态 session.access(() -> { statusLabel.setValue("文件上传完成: " + filename); uploadProgress.setValue(1f); }); LOG.info("Finished background upload of file [{}]", filename); } catch (IOException | InterruptedException e) { LOG.error("Background upload failed for file [{}]", filename, e); // 异常时更新UI提示用户 session.access(() -> statusLabel.setValue("后台上传失败: " + e.getMessage())); } finally { // 清理临时文件 if (tempFile.exists()) { tempFile.delete(); } } }
关键注意事项
- 临时文件管理:后台线程完成后必须删除临时文件,也可以定时清理未处理的临时文件
- UI更新安全:后台线程不能直接操作UI组件,必须通过
VaadinSession.access(Runnable)执行UI更新逻辑 - 会话锁释放:
receiveUpload和onUploadSucceeded方法必须快速执行,不能包含阻塞操作,确保请求线程尽快释放会话锁 - 进度更新频率:后台更新进度时不要过于频繁,避免过多的UI推送请求影响性能
- 异常处理:后台线程的异常必须捕获并反馈到UI,避免用户无感知
日志解读
你之前的日志中hasLock始终为true,是因为这些日志都是在请求处理线程中输出的(uploadStarted、receiveUpload、uploadSucceeded等方法都由Vaadin的请求线程调用,该线程持有会话锁)。只有在handleBackgroundUpload方法中,后台线程没有持有会话锁,此时session.hasLock()会返回false。
内容的提问来源于stack exchange,提问作者Tony B
相关产品推荐
相关产品推荐

