21 August 2020

Summary

The Java enterprise ecosystem is changing rapidly. One of the shooting stars is the Quarkus framework. It describes itself as "Supersonic Subatomic Java" and "Kubernetes Native Java stack". This Blog post describes the first experience I made with Quarkus.

 

There are many reasons to take a look at the Quarkus framework. But beside its design for developer friendliness, Quarkus focuses consequently onto cloud platforms, it’s perfectly suited for particular container orchestrators like Kubernetes or OpenShift. Compared to classical JEE platforms the benefits of Quarkus are:

  • fast application startup by moving annotation processing from run-time to build-time

  • simplified packaging with just a single JAR file

  • small application and container image size

  • native images possible by GraalVM support

The Quarkus framework consists of a small core, to which arbitrary APIs can be added by extensions. Beside proprietary APIs, the most JEE and MicroProfife APIs are available within the Quarkus ecosystem. That makes Quarkus a perfect candidate for moving JakartaEE applications to the cloud.

As a sample project I wanted to implement a simplified chat application. Users visiting the chat room can submit messages to the server that get distributed to all other users of the chat-room.

Because JakartaEE Server-Sent Events (SSE) API are a perfect fit to distribute small pieces of information from a server to one or many clients, the Proof-of-Concept (PoC) project should demonstrate that SSE is eligible to implement a chat application. For the distribution of a message to many receivers the broadcast feature of SSE will be used.

Getting Started

Similar to Spring initializr or Thorntail Project Generator Quarkus offers a Start Coding page that let you pick the desired dependencies/APIs and generates a starter project that is downloaded as ZIP archive on demand. Alternatively, the starter project can be generated by instantiating a Maven archetype.

For my PoC project I just picked RESTEasy JAX-RS as extension, which automatically includes CDI. The generated starter project contained:

  • Sample JAX/RS resource

  • Sample static content

  • Maven POM and Maven wrapper script

  • Dockerfiles for different types of packages (OpenJDK, native GraalVM, etc.)

After extracting the generated project setup, the project can already be executed by the Quarkus' development server:

$ ./mvnw quarkus:dev
[INFO] Scanning for projects...
[INFO]
[INFO] --------------------< net.gunther:quarkus-chat-sse >--------------------
[INFO] Building quarkus-chat-sse 0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- quarkus-maven-plugin:1.6.1.Final:dev (default-cli) @ quarkus-chat-sse ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 4 resources
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 3 source files to /home/gunther/_work/repos/GuntherRotsch.github.com/guntherrotsch.github.io/code/quarkus-chat-sse/target/classes
Listening for transport dt_socket at address: 5005
__  ____  __  _____   ___  __ ____  ______
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/contai /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2020-08-16 18:08:01,293 INFO  [io.quarkus] (Quarkus Main Thread) quarkus-chat-sse 0-SNAPSHOT on JVM (powered by Quarkus 1.6.1.Final) started in 1.509s. Listening on: http://0.0.0.0:8080
2020-08-16 18:08:01,309 INFO  [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated.
2020-08-16 18:08:01,309 INFO  [io.quarkus] (Quarkus Main Thread) Installed features: [cdi, qute, resteasy, resteasy-jsonb, resteasy-qute]

Browsers can directly navigate to the Quarkus Development server. Hence, the project setup is complete and empowers developers to start immediately implementing their application.

One feature of the development server I’d like to highlight: Modified project code is immediately deployed. Even when dependencies defined in the POM change the re-built application gets hot-deployed - what JRebel used to do in the past, can Quarkus already out-of-the-box, really cool.

Message Distribution by SSE

The client of the chat application is implemented using HTML and JavaScript. Thanks to Quarkus' built-in feature to serve static content, the HTML files just need to be added to the src/main/resources/META-INF/resources/ folder. An index.html file hosted in this folder can be fetched by a Browser from http://localhost:8080/ or http://localhost:8080/index.html.

The core logic of the client consists of:

  • registering an EventSource to receive chat messages as events

  • transmitting messages users typed in to the server, which distributes it to the other clients as Server-Sent Events

Here are the most important pieces of the client’s code:

<div id="history"></div> (1)

<textarea id="inputbox" name="inputbox"></textarea> (2)
<button id="submitButton">Submit</button>

<script>
  const evtSource = new EventSource("/chat?name={name}"); (3)
evtSource.onmessage = function(event) {
  console.log("Received event: ", event);
    const newElement = document.createElement("li");
    const historyList = document.getElementById("history");

  newElement.innerHTML = "message: " + event.data;
    historyList.appendChild(newElement);
}
evtSource.onerror = function(event) {
  console.error("EventSource error: ", event);
}

document.getElementById("submitButton").onclick = function() { (4)
  const message = document.getElementById("inputbox").value;

  var xhttp = new XMLHttpRequest();
  xhttp.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 200) {
      alert(this.responseText);
    }
  };
  xhttp.open("POST", "/chat", true);
  xhttp.setRequestHeader("Content-type", "text/plain");
  xhttp.send(message);
}
</script>
1 The list of user messages received from as SSE. Whenever a new event occurs the message is appended to the list (see <3>).
2 Text area users inputs messages.
3 The registration of the EventSource on the server. The EventSource is a standard HTML5/JavaScript API implemented by all Browsers nowadays. Given proper registration on the server-side, SSEs are received by the EventSource#onmessage function.
4 The function to submit messages to be send to the other users of the chat-room.

The JavaScript code to register is automatically executed on load of the page.

The server also implements 2 parts of logic:

  • registration of clients which enters the chat-room

  • distribution of messages sent by users to all registered clients as SSE

The Java code is also stripped down to the crucial parts here:

@ApplicationScoped
@Path("/chat")
public class ChatResource {

	private static final Logger LOGGER = LoggerFactory.getLogger(ChatResource.class);

	private SseBroadcaster sseBroadcaster; (1)
	private Sse sse;

	@Context
	public synchronized void setSse(Sse sse) { (2)
		if (this.sse != null) {
			return;
		}
		this.sse = sse;
		this.sseBroadcaster = sse.newBroadcaster();
		this.sseBroadcaster.onClose(eventSink -> LOGGER.info("On close EventSink: {}", eventSink));
		this.sseBroadcaster.onError(
				(eventSink, throwable) -> LOGGER.info("On Error EventSink: {}, Throwable: {}", eventSink, throwable));
	}

	@GET (3)
	@Produces(MediaType.SERVER_SENT_EVENTS)
	public void register(@Context SseEventSink eventSink, @QueryParam("name") String name) {
		LOGGER.info("Registering user: {}", name);
		sseBroadcaster.register(eventSink);
		eventSink.send(sse.newEvent(String.format("Welcome, %s!", name)));
		broadcast(String.format("%s entered the chat room...", name));
	}

	@POST (4)
	public void broadcast(String message) {
		OutboundSseEvent event = sse.newEventBuilder().data(message).reconnectDelay(10000).build();
		sseBroadcaster.broadcast(event);
	}
}
1 The Sse and SseBroadcaster objects are standard JAX-RS types. Because these objects are thread-safe, they can after initialization (see ChatResource#setSse method) be used without synchronization.
2 Although the JAX-RS resource is @ApplicationScoped, the Context will be injected on every call. That makes sense because the context can be different on every call, even if the JAX-RS resource is a singleton. However, the initialization of instance variables Sse and SseBroadcast has to be performed only once. Otherwise, the registrations of clients get due to the re-initialization lost on every request. Hence, the setSse injection method is synchronized and keeps the already existing initialization.
3 When users enter the chat-room, the client sends a registration to this method, which @Produces messages of server-sent event type. Newly registered users are announced to the other visitors of the chat-room by a broadcast message.
4 The messages typed in by users and submitted to the servers get broadcasted to all registered users by this method.

Because users should give their name on registration, the code for entering the chartroom is implemented as template, that injects the user’s name. For templating the Qute extension has been added to the project, which turned out to be simple and straightforward. The entire code of the project is hosted in the code branch of the Blog repo.

Containerization

So far, we looked only at the development server. The following command will create an executable single JAR:

$ ./mvnw clean install
$ ll target
...
-rw-rw-r--  1 gunther gunther 285458 Aug 17 19:16 quarkus-chat-sse-0-SNAPSHOT-runner.jar
...

$ java -jar target/quarkus-chat-sse-0-SNAPSHOT-runner.jar
__  ____  __  _____   ___  __ ____  ______
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2020-08-17 17:17:23,707 INFO  [io.quarkus] (main) quarkus-chat-sse 0-SNAPSHOT on JVM (powered by Quarkus 1.6.1.Final) started in 1.261s. Listening on: http://0.0.0.0:8080
2020-08-17 17:17:23,757 INFO  [io.quarkus] (main) Profile prod activated.
2020-08-17 17:17:23,758 INFO  [io.quarkus] (main) Installed features: [cdi, qute, resteasy, resteasy-jsonb, resteasy-qute]
2020-08-17 17:17:41,886 INFO  [io.quarkus] (main) quarkus-chat-sse stopped in 0.041s
...

Compared to classical application server deployments it’s already noticeable that

  • memory foot-print is small: 200 KB + JDK runtime, but no application server installation required

  • application starts up very fast: about 1 second on my a bit out-dated laptop; simple JAX-RS applications take due to classpath-scanning on Wildfly 19 about 10 seconds

This features already emphasize that Quarkus is suitable for cloud deployments.

In the following, we take an look at how Quarkus supports creating application containers, but leave apart the ability to create native GraalVM images, which might be an extra Blog post in the future.

Installing jib extension

Quarkus supports 3 different ways to build container image:

  • Docker (requires a local Docker installation)

  • jib

  • S2I by fabric8

The extension for jib support can be added to the project by submitting the following command:

gunther@gunther-K501UQ:~/_work/repos/GuntherRotsch.github.com/guntherrotsch.github.io/code/quarkus-chat-sse$ ./mvnw quarkus:add-extension -Dextensions="container-image-jib"
[INFO] Scanning for projects...
[INFO]
[INFO] --------------------< net.gunther:quarkus-chat-sse >--------------------
[INFO] Building quarkus-chat-sse 0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- quarkus-maven-plugin:1.6.1.Final:add-extension (default-cli) @ quarkus-chat-sse ---
Downloading from central: https://repo.maven.apache.org/maven2/org/jboss/shrinkwrap/shrinkwrap-depchain/1.2.6/shrinkwrap-depchain-1.2.6.json
Downloading from central: https://repo.maven.apache.org/maven2/org/jboss/shrinkwrap/shrinkwrap-depchain-descriptor-json/1.2.6/shrinkwrap-depchain-descriptor-json-1.2.6.json
Downloading from central: https://repo.maven.apache.org/maven2/io/vertx/vertx-rx/3.9.1/vertx-rx-3.9.1.json
...

Building the image

After installing the jib extension, the following command builds the image:

gunther@gunther-K501UQ:~/_work/repos/GuntherRotsch.github.com/guntherrotsch.github.io/code/quarkus-chat-sse$ ./mvnw clean package -Dquarkus.container-image.build=true
[INFO] Scanning for projects...
[INFO]
[INFO] --------------------< net.gunther:quarkus-chat-sse >--------------------
[INFO] Building quarkus-chat-sse 0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
...

The built image is based on the lean Alpine Linux. However, distroless base images might be an alternative.

The image can be started as usual by:

gunther@gunther-K501UQ:~/_work/repos/GuntherRotsch.github.com/guntherrotsch.github.io/code/quarkus-chat-sse$ docker run -it --rm gunther/quarkus-chat-sse:0-SNAPSHOT
__  ____  __  _____   ___  __ ____  ______
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2020-08-17 17:17:23,707 INFO  [io.quarkus] (main) quarkus-chat-sse 0-SNAPSHOT on JVM (powered by Quarkus 1.6.1.Final) started in 1.261s. Listening on: http://0.0.0.0:8080
2020-08-17 17:17:23,757 INFO  [io.quarkus] (main) Profile prod activated.
2020-08-17 17:17:23,758 INFO  [io.quarkus] (main) Installed features: [cdi, qute, resteasy, resteasy-jsonb, resteasy-qute]
^C2020-08-17 17:17:41,886 INFO  [io.quarkus] (main) quarkus-chat-sse stopped in 0.041s


gunther@gunther-K501UQ:~/_work/repos/GuntherRotsch.github.com/guntherrotsch.github.io/code/quarkus-chat-sse$ docker images
REPOSITORY                                                         TAG                       IMAGE ID            CREATED              SIZE
gunther/quarkus-chat-sse                                           0-SNAPSHOT                265891dea59b        About a minute ago   200MB
...

The startup time is still about 1 second and the image size is about 200 MB for our sample application utilizing JAX-RS, CDI, and Templating. That’s really impressive, if you compare it with application server based setups, which result to my experience in images sizes of roughly:

  • JAX-RS service application & Wildlfly & OpenJDK: 750 MB

  • JAX-RS service application & Thorntail & OpenJDK: 400 MB

The native GraalVM image of the Quarkus application would even be considerably smaller.

Summary

The developer experience of Quarkus is incredible: Easy project setup and short development cycles due to hot-deployment with development server. In addition, Quarkus is well documented and actively supported by the community. The APIs offered by extensions do not miss anything. No wonder that the popularity of Quarkus arose to almost the level of Spring Boot within just 2 years.

In addition, the cloud-related properties are impressive, in regard to both, the image size as well as the startup time. Now even for Server-less deployment targets Java applications can considered without bad conscience.

Furthermore, the availability of Quarkus extensions of JEE/Jakarta EE APIs makes it a perfect candidate for migration of standard Java EE applications into the cloud.

Tags: cloud-native java jakarta-ee microservices quarkus