使用 Spring-Hazelcast 会话集群时 ViewScoped bean 的意外行为

Unexpected behavior with ViewScoped beans when using Spring-Session clustering with Hazelcast

我正在努力将集群引入基于 JSF 的 Spring-Boot Web 应用程序,一旦我们使用 Hazelcast 启用会话复制,我们就开始注意到我们的几个使用 ViewScoped bean 的 JSF 页面是不再正常工作。如果我们禁用会话复制和 Hazelcast,奇怪的行为将不再发生。

我首先在我们的一个使用 PrimeFaces 向导组件的页面中注意到了这个问题。当第二页为 "submitted" 时,向导第一页上输入的值将丢失。

然后在另一个页面上,我注意到命令按钮不再调用托管 bean 上的 actionListener 方法。我在方法中设置了一个断点,断点从未命中,但页面 "blinks" 并刷新回其初始状态。我确实注意到托管 bean 上的 PostConstruct 方法不会再次调用,因此它不会生成 ViewScoped bean 的新实例。

None 这些问题是在我禁用会话复制和 Hazelcast 时发生的。据我所知,检查会话及其内容,看起来确实正在创建会话并正确存储,据我所知。

该应用程序是一个 Spring-Boot Web 应用程序,使用 joinfaces 启动器引入 JSF 2.3.7 (Mojarra)、PrimeFaces 6.2 和 Omnifaces 1.14.1。我们最初开发应用程序时没有任何会话复制,我们的 ViewScoped bean 没有问题。

ViewScoped bean 使用 org.springframework.stereotype.Component 注释,就像您在连接面示例中看到的那样,并且 javax.faces.view.ViewScoped 作为范围注释。我还尝试引入 Weld 并使用 @Named 注释以及回退到旧的已弃用的 JSF @ManagedBean 和 @ViewScoped 注释,但在所有情况下都存在相同的行为。

我已经完成并确保我们的 ManagedBean 以及 bean 本身的任何属性都是完全可序列化的。

为了演示我所看到的,我从网上的几个地方挑选了两个非常简单的示例,并创建了一个简单的 Spring-Boot 项目,您可以自己克隆和 运行。
https://github.com/illingtonFlex/ViewScopeDemo

此演示应用程序包含两个托管 bean 和两个 xhtml 文件。

第一个示例是从 BalusC 网站上的示例复制而来的: http://balusc.omnifaces.org/2010/06/benefits-and-pitfalls-of-viewscoped.html

xhtml 文件如下所示:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:h="http://java.sun.com/jsf/html">
<h:head>
    <title>Really simple CRUD</title>
</h:head>
<h:body>
    <h3>List items</h3>
    <h:form rendered="#{not empty viewScopedController.list}">
        <h:dataTable value="#{viewScopedController.list}" var="item">
            <h:column><f:facet name="header">ID</f:facet>#{item.id}</h:column>
            <h:column><f:facet name="header">Value</f:facet>#{item.value}</h:column>
            <h:column><h:commandButton value="edit" action="#{viewScopedController.doEdit(item)}" /></h:column>
            <h:column><h:commandButton value="delete" action="#{viewScopedController.delete(item)}" /></h:column>
        </h:dataTable>
    </h:form>
    <h:panelGroup rendered="#{empty viewScopedController.list}">
        <p>Table is empty! Please add new items.</p>
    </h:panelGroup>
    <h:panelGroup rendered="#{!viewScopedController.edit}">
        <h3>Add item</h3>
        <h:form>
            <p>Value: <h:inputText value="#{viewScopedController.item.value}" /></p>
            <p><h:commandButton value="add" action="#{viewScopedController.add}" /></p>
        </h:form>
    </h:panelGroup>
    <h:panelGroup rendered="#{viewScopedController.edit}">
        <h3>Edit item #{viewScopedController.item.id}</h3>
        <h:form>
            <p>Value: <h:inputText value="#{viewScopedController.item.value}" /></p>
            <p><h:commandButton value="save" action="#{viewScopedController.save}" /></p>
        </h:form>
    </h:panelGroup>
</h:body>
</html>

支持此页面的 ViewScoped bean 如下所示:

package help.me.understand.jsf.ViewScopeDemo.controller;

import help.me.understand.jsf.ViewScopeDemo.model.Item;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.faces.view.ViewScoped;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

@Component
@ViewScoped
@Data
@EqualsAndHashCode(callSuper=false)
@ToString
public class ViewScopedController implements Serializable {
    private static final Logger log = LoggerFactory.getLogger(ViewScopedController.class);
    private List<Item> list;
    private Item item = new Item();
    private boolean edit;

    @PostConstruct
    public void init() {
        // list = dao.list();
        // Actually, you should retrieve the list from DAO. This is just for demo.
        list = new ArrayList<Item>();
        list.add(new Item(1L, "item1"));
        list.add(new Item(2L, "item2"));
        list.add(new Item(3L, "item3"));
    }

    public void add() {
        // dao.create(item);
        // Actually, the DAO should already have set the ID from DB. This is just for demo.
        item.setId(list.isEmpty() ? 1 : list.get(list.size() - 1).getId() + 1);
        list.add(item);
        item = new Item(); // Reset placeholder.
    }

    public void doEdit(Item item) {
        this.item = item;
        edit = true;
    }

    public void save() {
        // dao.update(item);
        item = new Item(); // Reset placeholder.
        edit = false;
    }

    public void delete(Item item) {
        // dao.delete(item);
        list.remove(item);
    }

    public List<Item> getList() {
        return list;
    }

    public Item getItem() {
        return item;
    }

    public boolean isEdit() {
        return edit;
    }

    // Other getters/setters are actually unnecessary. Feel free to add them though.
}

如果您启动应用程序并导航到 localhost:8080/index.xhtml,请单击其中一个条目的编辑。然后在文本字段中输入新名称并单击保存。永远不会调用托管 bean 上的 save 方法,并且页面 "resets" 回到其初始状态。如果您通过注释掉 @EnableHazelcastHttpSession 注释以及 ViewScopeDemoApplication 中定义的 hazelcastInstance @Bean 来禁用 Hazelcast 和会话复制,则上述示例步骤有效。调用保存方法,修改编辑项名称

为了演示奇怪的 ViewScoped 行为的另一个示例,我从 PrimeFaces 展示中逐字复制了向导示例代码: https://www.primefaces.org/showcase/ui/panel/wizard.xhtml

应用程序启动后,您可以通过以下方式访问此示例 localhost:8080/wizard.xhtml

启用 Hazelcast 和会话复制后,您可以在 onFlowProcess 方法中设置一个断点,该方法在从向导的一页导航到下一页时触发。您可以看到在向导的第一步中输入的值在随后的向导页面更改中丢失(它们变为空)。禁用 Hazelcast,这些值会在整个向导选项卡范围内保持不变。

当问题发生时,我在日志中没有看到任何错误或任何类型的异常。我也没有在浏览器调试控制台中看到任何问题。但是,从这两个示例中可以清楚地看出,ViewScoped bean 的行为有所不同,具体取决于是否启用了 Hazelcast 会话复制。

提前感谢您的帮助和考虑!

我似乎偶然发现了解决我的 ViewScoped 问题的方法。我承认我还没有完全理解这到底是如何产生影响的,但我想我会 post 为将来可能遇到此 post 的其他人提供解决方案。希望比我更聪明的人能来帮助我理解为什么这样做有效,并可能指出为什么如果有更好的解决方案可用,这不是一个好主意。

成功的方法是将以下 属性 添加到我的 application.properties 文件中:

spring.session.servlet.filter-dispatcher-types=async, error, forward, include

尽管如此,设置 "request" 以外的任何调度程序类型似乎都会导致我的 ViewScoped bean 像我期望的那样运行。如果 "request" 是您指定的调度程序类型之一,则奇怪的 ViewScope 行为似乎会出现。

我会更新原post中提到的Github项目,让大家玩玩看看有什么不同。

希望这至少能为遇到类似问题的其他人提供线索!