Creating and deploying ZK Apps with Quarkus
Matthieu Duchemin, Engineer, Potix Corporation
May. 2, 2023
ZK 9.6.3 / ZK 10FL
Introduction: why ZK in Quarkus?
Quarkus is a Java development platform geared toward fast dev and cloud-native applications. The goal of running a Quarkus application instead of a traditional project to war file to server deployment is to take advantage of Quarkus quick-to-run project setup and live-coding capabilities.
In this article, we will explore how to run a ZK application in a Quarkus context. We will also discuss how ZK 10 cloud-native development brings ZK components to the cloud.
We will review the technical steps from installing Quarkus to deploying a Quarkus application, either serverless or as part of a scalable deployment on Docker or Kubernetes.
Technical steps
Installing the Quarkus development environment
A Quarkus project is run either from an IDE or through a command line. For this article, we will use the command line approach. We assume that the Quarkus command is already installed on the development machine. You can consult the getting started page on the Quarkus website for more information.
Creating a new Quarkus project
We will start by taking advantage of the Quarkus command to generate our project:
quarkus create zkquarkus
cd .\zkquarkus\
Adding the required Quarkus extensions
To run ZK, we need Servlet support. For this purpose, we need to add the Quarkus Undertow extension to our project.
quarkus ext add io.quarkus:quarkus-undertow
Adding ZK dependencies
Now that we have our project ready, we can start adding ZK dependencies. The project contains a pom.xml file, which we can use to manage dependencies through Maven.
We first add the ZK repository. You can find more information about how to use Maven to resolve ZK dependencies in the ZK documentation.
The fast way: bom
ZK provides a “Bill of material”, which can be used to simplify the dependency declaration.
<dependency>
<groupId>org.zkoss.zk</groupId>
<artifactId>zk-bom</artifactId>
<version>9.6.3-Eval</version>
<type>pom</type>
<scope>import</scope>
</dependency>
Declaring a BOM for a given ZK version will automatically set the versions of relevant ZK dependencies. This way, we can simply declare the relevant dependencies we want to resolve, and the BOM will provide version information, ensuring that all dependencies are on the same functional level for both the main ZK dependencies, and for any extra plugin or misc package we may need.
<dependency>
<groupId>org.zkoss.zk</groupId>
<artifactId>zkmax</artifactId>
</dependency>
<dependency>
<groupId>org.zkoss.zk</groupId>
<artifactId>zkplus</artifactId>
</dependency>
<dependency>
<groupId>org.zkoss.zk</groupId>
<artifactId>zhtml</artifactId>
</dependency>
The granular way: maven dependencies
If we want to configure our project with a set of dependencies outside of the scope of a BOM, we can also use standard Maven dependencies declaration:
<dependency>
<groupId>org.zkoss.zk</groupId>
<artifactId>zkmax</artifactId>
<version>${zk.version}</version>
</dependency>
<dependency>
<groupId>org.zkoss.zk</groupId>
<artifactId>zkplus</artifactId>
<version>${zk.version}</version>
</dependency>
<dependency>
<groupId>org.zkoss.zk</groupId>
<artifactId>zhtml</artifactId>
<version>${zk.version}</version>
</dependency>
Configuring ZK servlets in undertow
ZK uses servlets as endpoints for its operations. We can use the following options to wire up these servlets.
With web.xml
As with classic servlet containers, we can declare a web.xml file to configure ZK servlets. In Quarkus/Undertow, the web.xml
file can be created under the [root]/src/main/resources/META-INF/web.xml
path.
In this web.xml, we will add the standard ZK configuration. You can find more information about ZK web.xml config in the ZK documentation.
With web fragment
For standard use, we can also simply load the zk web fragment dependency, which will automatically wire up ZK servlets.
<dependency>
<groupId>org.zkoss.zk</groupId>
<artifactId>zkwebfragment</artifactId>
<version>${zk.version}</version>
</dependency>
Note on web-fragment and web.xml
If you want to manually configure ZK web.xml
, you will need to exclude web-fragment from your pom.xml
to avoid conflicts in servlets initialization.
<exclusions>
<exclusion>
<groupId>org.zkoss.zk</groupId>
<artifactId>zkwebfragment</artifactId>
</exclusion>
</exclusions>
Starting the project
Now that our project is complete, we can start developing. First, we can launch the Quarkus dev mode, which supports live development by watching files and automatically recompiling/serving the latest updates.
quarkus dev
ZK page in Quarkus
Creating the zul file
To serve a zul file from the default ZK endpoint, we can create it under the resource directory:
[root]/src/main/resources/META-INF/resources/index.zul
For now, let’s just create a simple hello world page.
<zk>
<window title="My First ZK Application" border="normal">
Hello World!
</window>
</zk>
Creating the Java Composer (or ViewModel)
We can create the Java classes used by our application in the standard Java source directory:
org.zkoss.classic.HelloWorldComposer
Applying the composer to the zul file
Same here as in a normal ZK application.
<zk>
<window apply="org.zkoss.classic.HelloWorldComposer" title="My First ZK Application" border="normal">
<div>Hello World!</div>
<button id="btn" label="Click me!"/>
</window>
</zk>
In short
At this point, we have done the following:
- Created a Quarkus application.
- Deployed the Undertow extension to support Servlets.
- Deployed ZK dependencies and configuration files.
- Started the Quarkus application from the command line.
- Created ZK zul and Java content.
From here, we can continue to develop, using the live code updates provided by Quarkus for Java classes.
We can access our test application on the default local url: http://localhost:8080/
Going further with ZK 10 preview: Native cloud development with ZK 10 Stateless Components
Comparing classic ZK and Cloud Native ZK workflows
In this section, we will discuss the differences between the classic ZK workflow and the upcoming ZK 10 stateless workflow.
Classic ZK architecture keeps the state of ZK pages as collections of Java objects on the server side. These objects are instantiated when the page is created. Then, when an event makes modifications to the server-side state, the server will emit updates to the client to synchronize these changes.
In this structure, the server-side state is authoritative, and the client-state reflects the server state.
In contrast, in ZK 10 cloud mode, there is no server-side state. In this mode, the server doesn’t keep track of the individual components of a page. Instead, when the client sends an action to the server, the client will send all necessary state information to the server in the request payload. The server will then use this information to craft a differential update response.
As a result, in ZK 10 cloud mode, if multiple server nodes are running the same application, any of them can process actions and provide differential updates to a given client. None of the nodes hold state, and the response content is generated based on the data passed with actions. This allows a client to receive an initial page response from node1, a first update from node2, etc.
Cloud-native applications
The intent behind ZK 10 stateless component is to bring ZK UI elements to the growing cloud-native / micro-services environment. The micro-service philosophy is to create smaller executables which run independently from each other. ZK 10 stateless components / cloud-mode is more suitable for this type of architecture, while classic ZK is a better fit for traditional web server deployments.
Technical steps
ZK 10 Immutable Components and ZK 10 cloud mode: Coming soon
ZK stateless components are a new option to build and interact with a ZK UI. These components produce the same in-browser elements and user experience as classic ZK components.
The main difference is that immutable components are discarded after rendering. Instead of updating the same object, updates to the UI are processed by a component updater, which will create a copy of the original component with updated values, then send the differential update to the client.
These elements are usable in a classic “stateful” environment and can be used to reduce the memory footprint of a page at server-side. This said, they can be used to create cloud native ZK applications as well.
Declaring the stateless service filter
To process stateless richlets, we first need to enable the DispatcherRichletFilter in web.xml
.
<filter>
<filter-name>DispatcherRichletFilter</filter-name>
<filter-class>org.zkoss.stateless.ui.http.DispatcherRichletFilter</filter-class>
<init-param>
<param-name>basePackages</param-name>
<param-value>org.zkoss.stateless</param-value>
</init-param>
<init-param>
<param-name>cloudMode</param-name>
<param-value>true</param-value>
</init-param>
</filter>
The filter requires the basePackages
parameter. We use this parameter to specify which Java packages should be inspected by filter for ZK stateless annotations.
Classes with ZK stateless annotations will be automatically bound to their respective URLs by the filter.
We also specify cloudMode
as a parameter of the filter.
In cloud-mode, features that would require a server-side state are automatically disabled. For example, server-side event queues, model controller, and websocket channels are features that require a server-side state and are not usable under this architecture.
After creating the filter, we also need to map it to a given url in our application:
<filter-mapping>
<filter-name>DispatcherRichletFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
This will allow the filter to intercept requests targeting richlets annotated with this path.
Creating the Richlet
We can now create the Stateless richlet by extending org.zkoss.stateless.ui.StatelessRichlet:
public class DemoRichlet implements StatelessRichlet {
In addition, we add the org.zkoss.stateless.annotation.RichletMapping
annotation to our class:
@RichletMapping("/demo")
public class DemoRichlet implements StatelessRichlet {
Next, we create the mapping for the initial page response:
@RichletMapping("")
public List<IComponent> index() {
return Immutables.createComponents("stateless-page.zul", null);
}
When associated to a method, the RichletMapping annotation indicates that the user can request a page rendering from this method.
The Page URL is
http://server:port/context/[class richlet mapping]/[method richlet mapping]
Since we used an empty string, this is the equivalent of declaring an index page, which will be available directly from
http://server:port/context/[class richlet mapping]
For the method content, we use the Immutables
utility class to generate immutable components from a zul file.
Building the page in zul
<zk>
<vlayout>
<hlayout>
<intbox width="300px" id="firstMember" value="2" />
<label value="first member"/>
</hlayout>
<hlayout>
<listbox id="operation" width="300px" mold="select">
<listitem value="add" label="add" selected="true"></listitem>
<listitem value="multiply" label="multiply"></listitem>
</listbox>
<label value="operation"/>
</hlayout>
<hlayout>
<intbox width="300px" id="secondMember" value="3"/>
<label value="second member"/>
</hlayout>
<button width="300px" id="calculate" label="calculate"/>
<label id="result" value="Result: "/>
</vlayout>
</zk>
The stateless zul file is fairly similar to a classic zul page. We can pass arguments to the zul file if needed, which can be accessed from the ${args}
implicit variable.
Importantly, we define IDs on components which we want to look up from the richlet. For example, the input components and the button all receive IDs, which makes them easier to locate during updates.
Mapping the update actions
There are several options to map a server action to a user’s event on the page. For more information, please consult the ZK 10 stateless articles and demo (links available at the end of this article).
In this case, since we are creating the immutable components from a zul file, the easiest option to map actions is to use the org.zkoss.stateless.annotation.Action
annotation on a richlet method.
@Action(from = "#calculate", type = Events.ON_CLICK)
The action annotation has an option “from” field, which accepts a selector like jQuery selectors. In this case, we select the button by ID, and we declare that the action should trigger on click.
In method signature, we need to declare the values which are to be retrieved from the client state when the action triggers:
public void calculate(@ActionVariable(targetId = "firstMember", field = "value") int firstMemberValue,
@ActionVariable(targetId = "secondMember", field = "value") int secondMemberValue,
@ActionVariable(targetId = "operation", field = "selectedIndex") int operation)
These variables are annotated with org.zkoss.stateless.annotation.ActionVariable
, which indicates the target (the component) and the field (the client-side getter) holding the value in question.
In this structure, ZK doesn’t keep track of the components values at server-side, and will use these annotations to retrieve the value from the client’s current state, then pass them to the richlet method on invocation.
At the end of this action, we can update the client using an immutable component updater. IComponent classes provide updaters which allow for differential updates from the server to the client:
UiAgent.getCurrent().smartUpdate(Locator.ofId("result"), new ILabel.Updater().value("result: "+result));
In this case, we use the label updater to send the “value” field back to the client, in order to update a specific component.
In short
At this point, we have created a Quarkus application running ZK 10 stateless in cloud-native mode.
From here, we can bundle the application using the default deployment information from Quarkus presets.
./mvnw clean package
Using the default preset, this will create a native image, which we can start directly from the command line:
java -jar .\target\quarkus-app\quarkus-run.jar
Quarkus also generates automatically docker files, which we can use to run our application into a docker container:
docker build -f src/main/docker/Dockerfile.jvm -t zkquarkus/my-service .
docker run -i --rm -p 8080:8080 zkquarkus/my-service
(Note: require a local Docker installation)
For more information about deployment options, please visit the Quarkus documentation.
Afterword
In this article, we explored how ZK can be integrated into a Quarkus application for quick development and for container-centric deployments.
We have also taken a sneak peek into ZK cloud-native mode coming soon in ZK 10 and how it enables ZK components to be used for micro-services applications in cloud-native environments. ZK 10 Freshly is currently available for a sneak peek into the upcoming ZK 10 stateless features. You can consult the linked github project or the previous articles above to find out how to run a ZK 10 stateless application locally.
Further reading: