Фоновые задачи и Push
Обычно сервер Vaadin меняет экран только в ответ на действие пользователя: нажали кнопку — пришёл ответ с изменениями. Но бывают долгие операции: отчёт считается десять секунд, и всё это время интерфейс не должен зависать. Решение — выполнить работу в фоновом потоке, а результат отправить в браузер самому сервером. Такая отправка без запроса от браузера называется push.
Включаем Push
Добавьте аннотацию на главный класс:
// src/main/java/com/example/blog/Application.java (фрагмент)
@SpringBootApplication
@StyleSheet(Lumo.STYLESHEET)
@StyleSheet(Lumo.UTILITY_STYLESHEET)
@StyleSheet("styles.css")
@Push
public class Application implements AppShellConfigurator {Импорт — com.vaadin.flow.component.page.Push. Теперь браузер держит с сервером постоянное соединение (WebSocket), и сервер может присылать изменения в любой момент.
Долгая операция в сервисе
Для примера посчитаем слова во всех статьях с искусственной задержкой в 2 секунды:
// src/main/java/com/example/blog/PostService.java (новые методы)
public long countWords() {
sleep(2000);
return repository.findAll().stream()
.map(Post::getContent)
.filter(text -> text != null && !text.isBlank())
.mapToLong(text -> text.trim().split("\\s+").length)
.sum();
}
private static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}Экран статистики
// src/main/java/com/example/blog/views/StatsView.java
package com.example.blog.views;
import com.example.blog.PostService;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import jakarta.annotation.security.PermitAll;
import java.util.concurrent.CompletableFuture;
@Route("stats")
@PageTitle("Статистика")
@PermitAll
public class StatsView extends VerticalLayout {
private final Span result = new Span("Нажмите кнопку, чтобы посчитать слова");
public StatsView(PostService service) {
Button count = new Button("Посчитать слова");
count.addClickListener(event -> {
UI ui = UI.getCurrent();
count.setEnabled(false);
result.setText("Считаем...");
CompletableFuture.supplyAsync(service::countWords)
.thenAccept(words -> ui.access(() -> {
result.setText("Слов во всех статьях: " + words);
count.setEnabled(true);
}));
});
add(count, result);
}
}Добавьте пункт «Статистика» в меню MainLayout и откройте /stats. После нажатия кнопка сразу становится неактивной, появляется «Считаем...», а через 2 секунды текст сам меняется на «Слов во всех статьях: 7» — без новых нажатий.
Что здесь важно
| Шаг | Зачем |
|---|---|
UI ui = UI.getCurrent() | Запоминаем вкладку, пока мы в потоке обработчика: в фоне UI.getCurrent() вернёт null |
CompletableFuture.supplyAsync(...) | Долгая работа уходит в другой поток, обработчик сразу завершается |
ui.access(() -> ...) | Блокирует сессию пользователя, меняет компоненты и отправляет изменения через push |
Пока идёт подсчёт, пользователь может пользоваться меню и другими экранами. Если он уйдёт со страницы, изменения просто никуда не отправятся.
Частая ошибка
Компонент меняют прямо в фоновом потоке, без ui.access:
// src/main/java/com/example/blog/views/StatsView.java (неправильно)
CompletableFuture.supplyAsync(service::countWords)
.thenAccept(words -> result.setText("Слов: " + words));Если в JVM включены проверки assert (так запускаются тесты), сразу видно:
java.lang.AssertionError: Cannot access state in VaadinSession or UI without locking the session.Без проверок ошибки может не быть, но изменение не дойдёт до браузера или сломает состояние сессии. Любые изменения компонентов из чужого потока делайте только внутри ui.access(...). А без @Push даже правильный код покажет результат лишь после следующего действия пользователя.