Приложение Spring boot с постоянно обновляющимся экраном Vaadin UI

У меня есть загрузочное приложение, в которое я добавляю некоторые скриншоты. Я решил использовать Vaadin, и, похоже, он отлично работает, пока я не разверну его в многоузловой производственной среде. Попадая в рабочую среду, экраны постоянно обновляются без видимой причины. Например, на одном экране есть сетка, в которой при щелчке по строке появляется диалоговое окно с подробностями об элементе. но как только появляется диалоговое окно, страница обновляется несколько раз.

Эта ветка форума здесь https://vaadin.com/forum/thread/17586129/routerlayout-causing-page-refresh является примером того же макета, который я использую, и описывает очень похожую проблему.

У меня есть базовый абстрактный класс, который расширяет VerticalLayout, и все конкретные классы расширяют этот базовый абстрактный класс. Каждый конкретный класс определяет свой собственный маршрут и использует общий класс макета.

Я обратился к форуму gitter, vaadin и обнаружил ошибку в github, но никто из Vaadin не хочет ни на что отвечать, насколько я могу судить.

Вот версии всего, что я использую: Версия Vaadin Flow: 14 Версия Java: 12.0.1+12 Версия ОС: Mac 10.14.5 Версия браузера: Fire Fox 70.0.1, Chrome 78.0.3904.97

Фрагменты кода из моей реализации: основной вид

@Slf4j
@RoutePrefix("v1/crud")
@Theme(value = Material.class, variant = Material.DARK)
public class MainView extends Div implements RouterLayout {
private H1 h1 = new H1("Vaadin Crud UI");
private HorizontalLayout header = new HorizontalLayout(h1);
private Div content = new Div();
private ApplicationContext context;

@Inject
public MainView(ApplicationContext context) {
    this.context = context;
    setSizeFull();
    h1.setWidthFull();
    content.setWidthFull();
    header.setWidthFull();
    header.setAlignItems(FlexComponent.Alignment.CENTER);
    VerticalLayout navigationBar = new VerticalLayout();
    navigationBar.setWidth("25%");

    navigationBar.add(createNavigationButton("Home", VaadinIcon.HOME, ReportTab.class));
    navigationBar.add(createNavigationButton("Batch Search", VaadinIcon.SEARCH, BatchSearchTab.class));
    ... a bunch more buttons
    HorizontalLayout layout = new HorizontalLayout(navigationBar, content);
    layout.setWidthFull();
    VerticalLayout page = new VerticalLayout(header, layout);
    page.setWidthFull();
    add(page);
}

@Override
public void showRouterLayoutContent(HasElement hasElement) {
    if (hasElement != null) {
        Element newElement = hasElement.getElement();
        if (newElement != null) {
            content.removeAll();
            content.getElement().appendChild(newElement);
        }
    }
}

private Button createNavigationButton(String caption, VaadinIcon icon, Class<? extends BaseEditor> editor) {
    Button button = new Button(caption, icon.create());
    button.addClickListener(event -> UI.getCurrent().navigate(editor));
    button.addThemeVariants(ButtonVariant.MATERIAL_CONTAINED);
    button.getStyle().set("background-color", "#00819D");
    button.setWidthFull();
    return button;
}

}

Базовый компонент:

@Slf4j
@Data
@SpringComponent
@UIScope
public abstract class BaseEditor<P, B> extends VerticalLayout {

private final RememberMeService rememberMe;
private final P businessProcess;
protected Binder<B> binder;
protected Dialog editDialog = new Dialog();
protected Button save = new Button("Save", VaadinIcon.CHECK.create());
protected Button close = new Button("Close", VaadinIcon.EXIT.create());
protected Button delete = new Button("Delete", VaadinIcon.TRASH.create());
protected B bean;

private ChangeHandler changeHandler;
private boolean proceed = true;

public BaseEditor(P businessProcess, RememberMeService rememberMe) {
    this.rememberMe = rememberMe;
    this.businessProcess = businessProcess;
    save.addClickListener(e -> save());
    delete.addClickListener(e -> delete());
}

public abstract void delete();

public abstract void save();

protected abstract Component getContent();

protected void edit(B e) {
    bean = e;
    editDialog.open();
    getBinder().setBean(e);
}

protected void initEditorPanel(Component... components) {
    HorizontalLayout actions = new HorizontalLayout(save, close, delete);
    VerticalLayout data = new VerticalLayout(components);
    data.add(actions);
    editDialog.removeAll();
    editDialog.add(data);
    getBinder().bindInstanceFields(this);
    close.addClickListener(e -> editDialog.close());
}

public interface ChangeHandler {
    void onChange();
}

void setChangeHandler(ChangeHandler h) {
    changeHandler = h;
}

void errorDialog(String message) {
    final Button close = new Button("Close", VaadinIcon.CLOSE.create());
    H3 h3 = new H3(message);
    final Dialog errorDialog = new Dialog(h3, close);
    errorDialog.open();
    close.addClickListener(e -> errorDialog.close());
}

BaseEditor filter(Predicate<B> predicate) {
    Objects.requireNonNull(predicate);
    proceed = predicate.test(bean);
    return this;
}

void buttonConsumer(Consumer<B> consumer) {
    if (!proceed) {
        proceed = true;
        return;
    }
    try {
        consumer.accept(bean);
    } catch (Exception e) {
        errorDialog(e.getMessage());
    } finally {
        editDialog.close();
        getChangeHandler().onChange();
    }
}

void either(Consumer<B> whenTrue, Consumer<B> whenFalse) {
    try {
        if (proceed) {
            whenTrue.accept(bean);
        } else {
            whenFalse.accept(bean);
        }
    } catch (Exception e) {
        errorDialog(e.getMessage());
    } finally {
        proceed = true;
        editDialog.close();
        getChangeHandler().onChange();
    }
}
}

Бетонный компонент:

@Slf4j
@Route(value = "search/batch", layout = MainView.class)
public class BatchSearchTab extends BaseEditor<BatchService, Batch> {
private TextField searchField1;
private TextField searchField2;

public BatchSearchTab(BatchService businessProcess, RememberMeService rememberMe) {
    super(businessProcess, rememberMe);
    binder = new Binder<>(Batch.class);
    save.setIcon(VaadinIcon.REPLY.create());
    save.setText("Replay");
    delete.setIcon(VaadinIcon.CLOSE.create());
    delete.setText("Cancel");
    getContent();
}

@Override
public void delete() {
    buttonConsumer(b -> getBusinessProcess().cancelBatch(b.getBatchId(), b.getUserAgent()));
}

@Override
public void save() {
    filter(b -> b.isReplayable()).buttonConsumer(b -> getBusinessProcess().buildAndSendFile((getBean())));
}

@Override
public void edit(Batch batch) {
    HorizontalLayout actions = new HorizontalLayout();
    H2 h2 = new H2();
    if (batch.isReplayable()) {
        h2.setText("Would you like to replay the following.");
        actions.add(save, delete, close);
    } else {
        h2.setText("This record is not eligible for replay.");
        actions.add(close);
    }

    Label batchId = new Label("Correlation Id: " + batch.getBatchId());
    Label txnCount = new Label("Transaction Count: " + batch.getTotalTxns());
    Label txnAmount = new Label("Total: " + batch.getTotalBatchAmount());
    VerticalLayout data = new VerticalLayout(h2, batchId, txnCount, txnAmount, actions);
    data.add(actions);
    editDialog.removeAll();
    editDialog.add(data);
    close.addClickListener(e -> editDialog.close());
    editDialog.open();
    getBinder().setBean(batch);
}

@Override
protected Component getContent() {
    final H2 h2 = new H2("Locate Batches");
    searchField1 = new TextField("Batch Code");
    searchField2 = new TextField("User Agent");
    searchField2.addKeyPressListener(Key.ENTER, e -> keyPressListener());
    Button searchBtn = new Button("Search", VaadinIcon.SEARCH.create());
    HorizontalLayout search = new HorizontalLayout(searchField1, searchField2);
    searchBtn.addClickListener(e -> {
        search(searchField1.getValue(), searchField2.getValue());
    });
    add(h2, search, searchBtn);
    return this;
}

private void search(String code, String userAgent) {
    log.info("Searching {} and {}", code, userAgent);
    List<Batch> batches =
        getBusinessProcess().getBatchesForUserAgent(code, userAgent, 60);
    log.info("Found {} batches", batches.size());
    if (batches.size() > 0) {
        buildGrid(batches, "BatchId", "totalTxns", "totalBatchAmount", "status");
    } else {
        errorDialog("No Records found for criteria");
    }
}

private void keyPressListener() {
    String code = StringUtils.isNotBlank(searchField1.getValue()) ? searchField1.getValue() : null;
    if (StringUtils.isNotBlank(searchField2.getValue())) {
        search(code, searchField2.getValue());
    }
}

private void buildGrid(Collection<Batch> records, String... columns) {
    Component result;
    if (records.size() == 0) {
        result = new Label("NO REPORT DATA AVAILABLE.");
    } else {
        final Grid<Batch> grid = new Grid<>(Batch.class);
        grid.setHeightByRows(records.size() < 10);
        grid.setColumns(columns);
        grid.setItems(records);
        grid.setWidthFull();
        grid.asSingleSelect().addValueChangeListener(l -> Optional.ofNullable(l.getValue()).ifPresent(this::edit));
        result = grid;
    }
    if (getComponentCount() < 3) {
        add(result);
    } else {
        replace(getComponentAt(2), result);
    }
}

private void loadData(String code, String userAgent) {
    if (StringUtils.isNotBlank(code)) {
        search(null, userAgent);
    } else {
        search(code, userAgent);
    }
}
}

1 ответ

Отказ от ответственности: дальнейшее обнаружение фактов произошло через IRC

Ответ на вопрос связан с OP, запускающим несколько экземпляров приложения за балансировщиком циклической нагрузки. Клиенты попадают на случайные серверы, и поэтому сеанс на них не запускается.

Решением этой проблемы является наличие общего хранилища сеансов и, в идеале, диспетчеризация балансировщика нагрузки в существующем сеансе, так что "горячие" серверные серверы будут поражены.

Другие вопросы по тегам