Search code examples
primefaceshazelcastspring-sessionjsf-2.3joinfaces

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


I am working on introducing clustering to a JSF based Spring-Boot web application and as soon as we enabled session-replication with Hazelcast, we started noticing that several of our JSF pages that utilize ViewScoped beans were no longer functioning correctly. If we disable session replication and Hazelcast the odd behavior no longer occurs.

I first noticed the problem in one of our pages that utilizes a PrimeFaces Wizard component. Values entered on the first page of the wizard are lost when the second page is "submitted".

Then on a different page I noticed that a command button was no longer calling the actionListener method on the managed bean. I set a breakpoint in the method and the breakpoint is never hit, but the page "blinks" and refreshes back to its initial state. I did note that the PostConstruct method on the managed bean is NOT called again, so it is not generating a new instance of the ViewScoped bean.

None of these problems occur when I disable session replication and Hazelcast though. As far as I can tell, inspecting the session and its contents, it does look like sessions are being created and properly stored, as far as I can tell anyway.

The app is a Spring-Boot web application using the joinfaces starters to bring in JSF 2.3.7 (Mojarra), PrimeFaces 6.2, and Omnifaces 1.14.1. We initially developed the app without any session-replication in place and we had no problems with our ViewScoped beans.

The ViewScoped beans are using org.springframework.stereotype.Component annotation, just like you see in the joinfaces examples, and javax.faces.view.ViewScoped as the scope annotation. I have also tried bringing in Weld and using the @Named annotation as well as falling back on the old deprecated JSF @ManagedBean and @ViewScoped annotations, but the same behavior persists in all cases.

I have gone through and ensured that our ManagedBeans are all fully serializable as well as any attributes on the beans themselves.

To demonstrate what I am seeing I picked two very simple examples from a couple places on the web and created a simple Spring-Boot project that you can clone and run yourself.
https://github.com/illingtonFlex/ViewScopeDemo

This demo app contains two managed beans and two xhtml files.

The first example is one that a copied from an example on BalusC's website: http://balusc.omnifaces.org/2010/06/benefits-and-pitfalls-of-viewscoped.html

The xhtml file looks like this:

<!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>

and the ViewScoped bean backing this page looks like this:

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.
}

If you start the application and navigate to localhost:8080/index.xhtml, click edit on one of the entries. Then enter a new name in the text field and click save. The save method on the managed bean is never called, and the page "resets" to its initial state. If you disable Hazelcast and session-replication by commenting out the @EnableHazelcastHttpSession annotation along with the hazelcastInstance @Bean defined in ViewScopeDemoApplication, the above example steps work. The save method is called, and the name of the edited item is changed.

To demonstrate another example of odd ViewScoped behavior I copied the Wizard example code verbatim from the PrimeFaces showcase: https://www.primefaces.org/showcase/ui/panel/wizard.xhtml

With the app started, you can access this example via localhost:8080/wizard.xhtml

With Hazelcast and session-replication enabled, you can set a break point in the onFlowProcess method, which fires when navigating from one page of the wizard to the next. You can see that values entered on the first step of the wizard are lost (they become null) on subsequent wizard page changes. Disable Hazelcast and the values persist across the entire span of Wizard tabs.

I am not seeing any errors or exceptions of any kind in the logs when the problems occur. Nor do I see any problems in the browser debug console for that matter. However, its clear from these two examples that the ViewScoped beans are behaving differently depending on whether or not Hazelcast session-replication is enabled.

Thank you in advance for your help and consideration!


Solution

  • I seem to have stumbled onto a solution for my ViewScoped problems. I will admit that I do not fully understand exactly how this made a difference yet, but I thought I would post a solution for others who might come across this post in the future. Hopefully someone more wise than I can come along and help me understand why this worked, and possibly point out why its not a good idea if a better solution is available.

    The thing that did the trick was adding the following property to my application.properties file:

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

    The thing is though, setting ANY dispatcher type other than "request" seems to cause my ViewScoped beans to behave as I expect them to. If "request" is one of the dispatcher types that you specify, the odd ViewScope behavior seems to manifest.

    I will update the Github project mentioned in the original post so that others can play with it and see the difference.

    Hopefully this will at least serve as a clue for someone else having a similar problem!