如何让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
相关产品推荐
相关产品推荐

