This is a simple post about writing a stateless JSF application for
holding a dummy shopping cart using Hazelcast. The application uses PrimeFaces,
OmniFaces and it is deployed on Payara Server 4.1.1.162, which provide a very
nice support for Hazelcast.
Notice that this application is far away for being a good example to
follow in production, being more a proof of concept meant to show an example of
how we can develop JSF stateless (easy to scale out) application and keep user data
intact over multiple requests.
For those who don't know, JSF is by default stateful. This refers to
the fact that JSF works as expected only in presence of a view state (component
tree) which is restored at each postback request. Starting with version 2.2,
JSF provides support for stateless mode. This means that there is no view state
saved and we have to use only request scoped beans. The advantage of this
approach consist in the fact that we can easily scale out the application
without taking care of aspects as sessions replication or sticky sessions.
Some dependencies:
<dependencies>
<dependency>
<groupId>org.omnifaces</groupId>
<artifactId>omnifaces</artifactId>
<version>2.3</version>
</dependency>
<dependency>
<groupId>org.primefaces</groupId>
<artifactId>primefaces</artifactId>
<version>5.3</version>
</dependency>
<dependency>
<groupId>com.hazelcast</groupId>
<artifactId>hazelcast</artifactId>
<version>3.6.2</version>
</dependency>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-web-api</artifactId>
<version>7.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
Ok, let's see the steps for developing this proof of concept:
1. First, we need to activate stateless mode for JSF like below:
<f:view
transient="true">
...
</f:view>
2. Therefore, the index.xhtml
page that displays the shopping cart is pretty simple and you can easily understand
it by following the code line by line:
<f:view
transient="true">
<h:form id="cartId">
<p:dataList
value="#{shoppingCart.viewCart()}" var="t"
type="ordered">
<f:facet name="header">
Your
Shopping Cart
</f:facet>
#{t.name}, #{t.price}
<p:commandButton value="Remove" action="#{shoppingCart.removeItemFromCart(t)}"
update="@form"/>
</p:dataList>
<p:commandButton value="Add random item" action="#{shoppingCart.addItemToCart()}"
update="@form" />
</h:form>
</f:view>
3. The shopping cart content is stored in a Hazelcast IList, therefore we
need to activate Hazelcast. Commonly this is accomplished via something like
below:
Config cfg =
new Config();
HazelcastInstance
instance = Hazelcast.newHazelcastInstance(cfg);
But, in this case we don't need to do that because Payara already
provide Hazelcast support, and all we need to do is to activate it. For
example, we have created a cluster named TestCluster with two instances, test1 and test2. As you can see
in figure below, all we have to do for having a Hazelcast instance is to enable
Hazelcast for our cluster and click Save (by default Payara Server admin console is available at localhost:4848):
As a note not related to this app (we don't need sessions), is
good to know that Payara also provide session persistence via Hazelcast. Check
the below screen-shot:
4. Now, we have a Hazelcast instance available for our cluster. Payara expose
the Hazelcast instance via JNDI under the name payara/Hazelcast:
This means that we can easy obtain the Hazelcast instance in the
application via @Resource as below:
@Resource(name =
"payara/Hazelcast")
HazelcastInstance hazelcast;
5. In order to configure Hazelcast data structures we can use an XML
file. Payara recognize the hazelcast-config.xml
(it doesn't exist by default):
But, Hazelcast also supports programmatic configuration. For example,
let's suppose that each time we add a new instance in cluster, we want to add a
dummy item in the shopping cart (I know, this is a pretty useless use case, but
that not important here). This item is added only once or under certain
circumnstances, so ideally we will run that code only once or only under certain
conditions. There are many ways to accomplish this and different moments in
application flow where to do it, but let's do this
via a request scoped managed bean instantiated eagerly. For this we can assign
a
requestURI to
a managed bean annotated with the OmniFaces
@Eager.
The following bean will be instantiated whenever the URI
/faces/start.xhtml (relatively
to the application root) is requested:
@Eager(requestURI
= "/faces/start.xhtml")
@RequestScoped
public class
HazelcastInit {
@Resource(name = "payara/Hazelcast")
HazelcastInstance hazelcast;
private static final Logger LOG =
Logger.getLogger(HazelcastInit.class.getName());
@PostConstruct
public void init() {
LOG.info("Initialize list of products
started ...");
// since this is a default config we can skip the following 4 lines
Config config = new
XmlConfigBuilder().build();
ListConfig listConfig =
config.getListConfig("cart");
listConfig.setName("cart");
hazelcast.getConfig().addListConfig(listConfig);
IList<Item> cartList =
hazelcast.getList("cart");
cartList.add(new
Item("Dummy Product", 0));
LOG.info("Initialize list of products
successfully done ...");
}
}
In our case, we have set the start.xhtml as the start page of the application, so the
above code is executed at least once (and each time to navigate to the
specified URI):
<f:view
transient="true">
Hazelcast was successfully initialized ...
<p:link outcome="index">Go to
app ...</p:link>
</f:view>
Note that when Hazelcast "meets" a data structure (e.g. IList, IMap, etc) usage, it
is smart enough to create that data structure when it doesn't exist and to not
re-create it when exists, so if you don't need some specific tasks or
configuration then is no need to define that data structure in XML or
programmatic. At first use, Hazelcast will create it for you with the default
configurations.
6. Finally, we write the ShoppingCart
request managed bean a below:
@Named
@RequestScoped
public class
ShoppingCart implements Serializable {
@Resource(name = "payara/Hazelcast")
HazelcastInstance hazelcast;
private static final Logger LOG =
Logger.getLogger(ShoppingCart.class.getName());
// the available items
private static final List<Item>
AVAILABLE_PRODUCTS = new ArrayList<Item>() {
{
add(new Item("product_1", 23));
add(new Item("product_2", 53));
add(new Item("product_3", 13));
add(new Item("product_4", 58));
add(new
Item("product_5", 21));
}
};
public void addItemToCart() {
LOG.info("Adding a product to shopping
cart ...");
IList<Item> cartList =
hazelcast.getList("cart");
cartList.add(AVAILABLE_PRODUCTS.get(new
Random().nextInt(5)));
LOG.info("Product successfully added
...");
viewCart();
}
public void removeItemFromCart(Item item) {
LOG.info("Removing a product to shopping
cart ...");
IList<Item> cartList =
hazelcast.getList("cart");
cartList.remove(item);
LOG.info("Product successfully remove
...");
viewCart();
}
public List<Item> viewCart() {
List<Item> cart = new
ArrayList<>();
LOG.info("View cart ...");
IList<Item> cartList =
hazelcast.getList("cart");
for (int i = 0; i < cartList.size(); i++)
{
cart.add(cartList.get(i));
}
return cart;
}
}
7. Let's test the app. Ensure that you have the WAR of the application
and follow these steps:
7.1 Start the TestCluster:
7.2 Ensure that
the cluster was successfully started and that Hazelcast contains the members:
7.3 Deploy the WAR
application in cluster:
7.4 Launch the
application
7.5 Click the link
for test1 (notice
the log below):
7.6 Click the Go to app link (notice
the dummy item added at initialization):
7.7 Add few more
random items by clicking the button labeled Add random item:
7.8 Start test2 (notice the log
below):
7.9 Click the Go to app link (notice
the dummy item added at initialization and the rest of items added eariler):
Done! You try further to play by removing and adding items and notice
how the shopping cart is maintained by Hazelcast in a JSF stateless
application. Of course, if you want to take into account the login part, you
can easily go for JWT authentication mechanism.
Notice that we have accomplished many of our tasks (e.g. cluster
creation, deploy app, etc) via the visual admin console of Payara. But, you can
also accomplish all these steps from CLI. Payara comes with a very handy tool
named Asadmin Recorder which is capable to record the actions from visual
console and output it in a text file as commands:
Well, I have used this feature and here it is the commands:
copy-config
default-config TestCluster-config
create-cluster
--config=TestCluster-config TestCluster
create-instance
--cluster=TestCluster --node=localhost-domain1 test2
create-instance
--cluster=TestCluster --node=localhost-domain1 test1
set-hazelcast-configuration
--startPort=5900 --hazelcastConfigurationFile=hazelcast-config.xml
--clusterName=development --clusterPassword=D3v3l0pm3nt --dynamic=true
--multicastPort=54327 --enabled=true --multicastGroup=224.2.2.3
--jndiName=payara/Hazelcast --target=TestCluster
start-cluster
TestCluster
deploy
--keepState=false --precompilejsp=false --availabilityEnabled=false
--name=JSFStatelessSC-1.0 --verify=false --force=false
--contextroot=JSFStatelessSC-1.0 --enabled=true
--properties=implicitCdiEnabled=true:preserveAppScopedResources=false
--target=TestCluster C:\Users\Leonard\AppData\Local\Temp\JSFStatelessSC-13447695818088979490.0.war
The complete application is available
here.