提问者:小点点

在Hazelcast中使用Spring-Session集群时,ViewScoped bean的意外行为


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

我第一次注意到这个问题是在我们的一个使用PrimeFaces向导组件的页面中。当第二页“提交”时,在向导第一页输入的值会丢失。

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

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

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

ViewScoped bean使用的是org. springframe.的原型.Component注释,就像你在连接面示例中看到的那样,以及javax.faces.view.ViewScoped作为作用域注释。我还尝试引入Weld并使用@命名注释以及回退到旧的已弃用的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上的保存方法永远不会被调用,页面将“重置”到其初始状态。如果您通过注释掉@EnableHazelcastHttpSession注释以及ViewScopeDemoApplication中定义的hazelcastInstance@Bean来禁用Hazelcast和会话复制,则上述示例步骤有效。调用保存方法,并更改编辑项的名称。

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

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

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

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

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


共1个答案

匿名用户

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

这样做的诀窍是添加以下属性到我的application.properties文件:

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

问题是,设置除“请求”之外的任何调度程序类型似乎会导致我的ViewScoped bean按照我期望的方式运行。如果“请求”是您指定的调度程序类型之一,那么奇怪的ViewScope行为似乎会显现出来。

我将更新原始帖子中提到的Github项目,以便其他人可以玩它并看到差异。

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