You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让Vaadin CallbackDataProvider实现异步数据获取避免UI冻结

Vaadin 14 CallbackDataProvider 异步加载优化方案

问题根因

Vaadin 14 默认的CallbackDataProvider回调是持有UI会话锁的同步线程中执行的,当你在回调中执行耗时的后端查询时,会长时间占用会话锁,导致同会话下的所有UI交互都被阻塞,就会出现整个界面冻结的情况。

可行实现方案

前置准备:开启Push推送

首先要为应用启用服务器推送能力,才能让异步线程加载完数据后主动更新UI,在你的路由视图类上添加@Push注解即可:

@Push
@Route("your-route")
public class GridWithSortableColumnsView extends VerticalLayout {
    // 原有逻辑
}

自定义异步DataProvider实现

不需要完全废弃CallbackDataProvider的分页逻辑,我们可以调整执行顺序:将耗时查询放到独立的异步线程执行,不占用UI锁,加载完成后再主动刷新数据。同时可以给Grid添加加载状态提示,避免用户以为界面无响应。
完整修改后的示例代码如下:

import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.data.provider.DataProvider;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.DetachEvent;
import com.vaadin.flow.shared.ui.Push;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Stream;

@Push
@Route("grid-view")
public class GridWithSortableColumnsView extends VerticalLayout {
    // 自定义线程池处理异步查询,根据实际业务调整线程数
    private static final ExecutorService QUERY_EXECUTOR = Executors.newFixedThreadPool(4);
    // 缓存当前查询结果,避免重复请求,WeakHashMap自动回收无用缓存
    private final Map<Query<GridRow, Void>, List<GridRow>> queryCache = new WeakHashMap<>();
    private final List<GridRow> gridRows = new LinkedList<>();
    private Grid<GridRow> grid;

    public GridWithSortableColumnsView() {
        addClassName("dialogwithgrid-view");
        setSizeFull();

        // 准备模拟数据
        for (int i = 0; i < 10000; i++) {
            gridRows.add(new GridRow(i, RandomStringUtils.randomAlphanumeric(10)));
        }

        // 自定义异步DataProvider
        DataProvider<GridRow, Void> asyncDataProvider = DataProvider.fromFilteringCallbacks(
                query -> {
                    // 优先返回缓存数据
                    if (queryCache.containsKey(query)) {
                        return queryCache.remove(query).stream();
                    }
                    // 触发异步查询
                    UI currentUi = UI.getCurrent();
                    grid.setLoading(true);
                    QUERY_EXECUTOR.submit(() -> {
                        try {
                            // 耗时操作完全在异步线程执行,不持有UI锁
                            Thread.sleep(2000);
                            List<GridRow> result = gridRows.stream()
                                    .sorted((o1, o2) -> StringUtils.compare(o1.getCol05(), o2.getCol05()))
                                    .skip(query.getOffset())
                                    .limit(query.getLimit())
                                    .toList();
                            // 缓存查询结果
                            queryCache.put(query, result);
                            // 切回UI线程刷新数据
                            currentUi.access(() -> {
                                grid.getDataProvider().refreshAll();
                                grid.setLoading(false);
                            });
                        } catch (InterruptedException e) {
                            currentUi.access(() -> {
                                Notification.show("数据加载失败");
                                grid.setLoading(false);
                            });
                        }
                    });
                    // 首次返回空流,等异步加载完成后刷新
                    return Stream.empty();
                },
                // 总数查询如果也耗时,可同样做异步缓存处理
                query -> gridRows.size()
        );

        grid = new Grid<>();
        grid.setPageSize(10);
        grid.setSizeFull();
        grid.addColumn(GridRow::getId).setHeader("Id");
        grid.addColumn(GridRow::getCol05).setHeader("col05");
        grid.setDataProvider(asyncDataProvider);

        add(grid);
    }

    // 组件销毁时关闭线程池,避免内存泄漏
    @Override
    protected void onDetach(DetachEvent detachEvent) {
        super.onDetach(detachEvent);
        QUERY_EXECUTOR.shutdownNow();
    }

    @EqualsAndHashCode(onlyExplicitlyIncluded = true)
    @AllArgsConstructor
    @Getter
    @Setter
    public static class GridRow {
        @EqualsAndHashCode.Include
        private int id;
        private String col05;
    }
}

优化点说明

  • 解决首次空数据问题:通过grid.setLoading(true)显示加载状态,用户可以明确感知数据正在加载,不会认为是异常
  • 会话锁释放:耗时查询完全在独立线程执行,期间UI锁是释放状态,其他组件的交互完全不受影响
  • 数据新鲜度:可以给queryCache添加过期时间,比如缓存10秒,超过时间就重新拉取数据,适配更新频繁的业务场景
  • 10万条数据适配:原有的分页逻辑完全保留,每次只请求当前页的少量数据,不会有内存压力

注意事项

  • 线程池大小根据业务并发量调整,不要设置过大避免后端服务压力过高
  • 如果总数查询也很慢,可以同样做异步缓存处理,不用每次请求都查总数
  • 后端分页查询要添加对应索引,避免查询本身耗时过长

内容的提问来源于stack exchange,提问作者lanmaster

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.26 23:36:02