Spring Boot on Quarkus: Magic or madness?

Spring Boot on Quarkus: Magic or madness?

Quarkus is a Java stack tailored for OpenJDK HotSpot (or OpenJ9 on zSeries) and GraalVM, crafted from optimized Java libraries and standards. It is a good choice for building highly-scalable applications while using lower amounts of CPU and memory resources than other Java frameworks. These applications can be traditional web applications, serverless applications, or even functions as a service.

There are many documented instances of organizations migrating their applications to Quarkus. In this article, let’s see one such migration path from Spring Boot to Quarkus that is part magic and part madness! The magic will be some hand waving and performing the migration without changing a single line of code. The madness will be trying to figure out how it was done.

The Application

The application is a simple “to-do” task management system. The user can enter to-do items and then check them off once done. These items are stored in a PostgreSQL database. All the application’s source code can be found here. There’s a version that uses Gradle instead of Maven as a build tool on the gradle branch.

Start the database

The application requires a PostgreSQL database, so the first thing we will do is use Docker or Podman to start an instance locally:

docker run --ulimit memlock=-1:-1 -it --rm=true --memory-swappiness=0 --name tododb -e POSTGRES_USER=todo -e POSTGRES_PASSWORD=todo -e POSTGRES_DB=tododb -p 5432:5432 postgres:11.5

A PostgreSQL 11.5 instance on port 5432 should now be running. The tododb schema accessible by the user todo with the password todo should be created.

Run the application

Run the application by issuing the command ./mvnw clean spring-boot:run. If you want to use Gradle instead of Maven, first switch to the gradle branch (git checkout gradle) and run the command ./gradlew clean bootRun.

You should see the standard Spring Boot banner:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.4.1)

INFO 68107 --- [  restartedMain] i.q.todospringquarkus.TodoApplication    : Started TodoApplication in 4.074 seconds (JVM running for 4.645)

Take note of the startup time. We’ll revisit this in a bit.

Once the application is running, navigate to http://localhost:8080 in your favorite browser.  You should see the main application screen as shown here in Figure 1.

The Spring Todos application with "My first todo" marked as complete.

Figure 1: Initial application screen.

Play around with the application a bit. Type a new todo into the text box and press Enter. That todo will show up in the list, as seen here in Figure 2.

Spring Todos with the new todo added to the list.

Figure 2: Add a new todo.

  1. Click the empty circle next to a todo to complete it, or uncheck it to mark it as incomplete.
  2. Click the X to remove a todo.
  3. The OpenAPI link at the bottom of the page will open the OpenAPI 3.0 specification for the application.
  4. The Swagger UI link opens the embedded Swagger UI, which can be used to execute some of the RESTful endpoints directly.
  5. The Prometheus Metrics link leads to the Prometheus metrics endpoint, which would be scraped intermittently by Prometheus.
  6. The Health Check link opens the built-in health check exposed by Spring Boot.

Go ahead and play around a bit to see it all in action. Don’t forget to come back here once you’re done! Use CTRL-C on your keyboard to stop the application once you’re done.

Examine the internals

The application is a full-featured Spring Boot application using the following capabilities:

Configuration

Open src/main/resources/application.properties to find the application configuration:

spring.jpa.hibernate.ddl-auto=create-drop
spring.datasource.url=jdbc:postgresql://localhost:5432/tododb
spring.datasource.username=todo
spring.datasource.password=todo

springdoc.api-docs.path=/openapi
springdoc.swagger-ui.path=/swagger-ui

management.endpoints.web.exposure.include=prometheus,health

Open src/main/resources/import.sql to find some SQL that will pre-populate the database table with an initial set of data:

INSERT INTO todo(id, title, completed) VALUES (0, 'My first todo', 'true');

That’s it Not a lot of code here for a fully-functional application that’s a whole lot more than “Hello World!”

The Magic

One hard requirement for this migration has been chosen: The application’s source code cannot be modified in any way.

Are you ready for the magic trick? Return to the command line and run the command ./.mvnw clean spring-boot:run. If you want to use Gradle instead of Maven, first switch to the gradle branch (git checkout gradle) and run the command ./.gradlew clean bootRun.

The first thing you’ll notice is the Quarkus banner and startup messaging instead of the Spring Boot one:

__  ____  __  _____   ___  __ ____  ______
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
INFO  [io.quarkus] (Quarkus Main Thread) todo-spring-quarkus 1.0.0-SNAPSHOT on JVM (powered by Quarkus 1.10.5.Final) started in 2.743s. Listening on: http://localhost:8080
INFO  [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated.
INFO  [io.quarkus] (Quarkus Main Thread) Installed features: [agroal, cdi, hibernate-orm, hibernate-orm-panache, jdbc-postgresql, kubernetes, micrometer, mutiny, narayana-jta, resteasy, resteasy-jackson, smallrye-context-propagation, smallrye-health, smallrye-openapi, spring-boot-properties, spring-data-jpa, spring-di, spring-web, swagger-ui]

Wait, what just happened? The application is now a Quarkus application and no longer a Spring Boot application? That’s madness!

Want proof? Go back to your browser window (http://localhost:8080 in case you closed it) and reload the page. The same user interface is there and is completely functional. Click on all the various links at the bottom of the page. They’re all completely functional as they were before.

Also, note the startup time. The Quarkus version starts up in almost half the amount of time given the exact same codebase (4.074s for Spring vs. 2.743s for Quarkus in the shown examples). That’s Supersonic, Subatomic, Java!

Nothing changed, so how did all this happen? A good magician doesn’t reveal his or her secrets!

The Madness

All good magicians use sleight-of-hand to distract his or her audience when performing a trick. There are a few things that weren’t shown yet in this post that are hidden from the naked eye. Did you notice anything suspicious? Let’s take a closer look at how the trick was done.

Execution

Look very closely at the commands you used to run the application:

Using Maven:

Spring Boot: ./mvnw clean spring-boot:run

Quarkus: ./.mvnw clean spring-boot:run

Using Gradle:

Spring Boot: ./gradlew clean bootRun

Quarkus: ./.gradlew clean bootRun

Notice any differences? A different executable (.mvnw/.gradlew) is used in the Quarkus version. If you open those files and examine them closely, you’ll notice some trickery going on:

.mvnw

#!/bin/sh
./mvnw clean quarkus:dev -Pquarkus

.gradlew

#!/bin/sh
./gradlew -Pprofile=quarkus $@

We used some sleight-of-hand to disguise the actual commands used to start the application. More on Gradle and Maven profiles later.

Configuration

Are you saying that Quarkus knows how to read and understand the Spring Boot configuration inside src/main/resources/application.properties? Well, no. Remember, this is a magic trick. A magician never tells the complete truth. What wasn’t shown in the example above is the Quarkus-specific configuration. It was hidden further down in the file.

If you re-open src/main/resources/application.properties and scroll all the way to the bottom (around line 58) you’ll see some additional configuration:

quarkus.datasource.db-kind=postgresql
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/tododb
quarkus.datasource.username=todo
quarkus.datasource.password=todo
quarkus.datasource.metrics.enabled=true
quarkus.hibernate-orm.database.generation=drop-and-create
quarkus.hibernate-orm.sql-load-script=import.sql

quarkus.micrometer.export.prometheus.path=/actuator/prometheus
quarkus.smallrye-health.root-path=/actuator/health

This configuration is similar to the Spring Boot configuration at the top of the file. One thing it does do, however, is to re-define the paths for the Prometheus and the health probe endpoints, so they match the paths of the Spring Boot actuator endpoints. This allows the Prometheus Metrics and Health Check links at the bottom of the screen to work without any changes to the user interface.

Pretty sneaky, huh?

Dependencies

As you can imagine, there are lots of dependencies that need to be changed, added, or updated. Luckily, Quarkus provides many Spring compatibility extensions:

Simply swapping some of the Spring Boot dependencies for the Quarkus ones will go a long way. There are a few other capabilities, such as Prometheus metrics, OpenAPI documentation, Swagger UI integration, and health checks that need other dependencies added. Lucky for us again, Quarkus has these capabilities as well:

Now you’re probably thinking to yourself, “But I didn’t make ANY changes whatsoever. All I did was run a Maven or Gradle command, and the entire application ran as a Quarkus application instead of a Spring Boot application. How can that be?”

Build setup

The build file is where all the magic happens. The build tool you are using determines how the dependency resolution magic actually happens, though.

Maven

On the main branch, open the pom.xml file. You’ll immediately notice the build file is broken into multiple Maven profiles, spring and quarkus, with the spring profile being the default profile. These profiles define which dependencies and build plugins are included when you run either ./mvnw clean spring-boot:run or ./mvnw -Pquarkus clean quarkus:dev (which is disguised using some redirection in the aforementioned .mvnw script).

Gradle

Gradle, on the other hand, does not have the concept of a profile. You’ll notice a handful of .gradle files in the gradle branch of the project:

  • build-common.gradle
    • Contains any common build logic applicable to both versions of the application, including setting the groupId/version of the produced artifact as well as artifact repository definitions.
  • build-spring.gradle
    • Contains all build logic, plugins, and dependencies specific to the Spring version of the application.
  • build-quarkus.gradle
  • settings.gradle
    • Where the magic actually happens! The settings.gradle file looks for a Gradle project property called profile:
      • If this property isn’t found, it defaults the value to spring.
      • It then sets the project’s build file, either build-spring.gradle or build-quarkus.gradle, as the main build file to use.

In general, this is a pretty good pattern to use if you need Gradle to simulate the capabilities of Maven profiles.

Application Main Class

It was mentioned at the beginning of this post that we wanted to perform the migration without changing a single line of code. Every Spring Boot application needs to have an “application” class that contains a main method and is annotated with @SpringBootApplication. In our project, src/main/java/io/quarkus/todospringquarkus/TodoApplication.java is that class.

Quarkus does not require such a class, nor do any of the Quarkus Spring compatibility extensions provide resolution for the @SpringBootApplication annotation nor the SpringApplication class referenced in this class.

So, what gives? We didn’t make any code changes whatsoever, yet those classes seem to resolve just fine in Quarkus.

You’ll notice a peculiar comment in both pom.xml (for Maven)/build-quarkus.gradle (for Gradle), right above the dependency declaration for the dependency org.springframework.boot:spring-boot-autoconfigure:

This dependency is a hack for TodoApplication.java, which isn't required for Quarkus. Point of demo is to NOT have any code changes.

This is the key to this part of the trick. This dependency allows both Spring Boot and Quarkus to resolve these classes at build time. The dependency is declared optional in Maven/compileOnly in Gradle, meaning it will never be included in the application binary the Quarkus build produces. It will be included in the binary the Spring Boot build produces because all of the other spring-boot-starter-* dependencies also depend on it, so it’s included transitively.

Wrap Up

You saw in this post how to take an existing Spring Boot application and run it on Quarkus without making a single change to the code. Was the method magic or madness? Maybe a little bit of both? It’s up to you to decide for yourself.

This post showed one way to migrate an application that is more than “Hello World” from Spring Boot to Quarkus with a hard requirement of not changing a single line of source code. It isn’t at all intended to represent the only potential migration path. Furthermore, there may be times when an application uses some library or API where there isn’t a Quarkus equivalent. In those cases, there may be code changes or some refactoring necessary.

A big thanks to Eric Murphy. He is the original author of the application and came up with the magic trick idea.

References

All source code for this post can be found here. The main branch contains the Maven version, and the gradle branch contains the Gradle version. The application source code is the same on both branches.

Share