A first microservice with Quarkus on GraalVM

How to set up a project with Quarkus and execute it on a GraalVM

In that article, I want to show you how to setup a Quarkus Application with Gradle. For this we are using a very simple business case of a stock-service that is supposed to give us the stock information.

Why Quarkus?


As s Spring Boot aficionado, I was not sure if I wanted to give Quarkus a chance. But there is one argument that makes the Framework really attractive: native support. For large applications with a lot of microservices in the cloud, it can become really expensive in term of memory consumption and startup time to run on a classic JVM. As a result, some organizations are currently trying alternatives like go or Node.js for their microservices. Quarkus was specifically designed to solve that issue by enabling applications tu run on a GraalVM with native compilation. This enables your application to start in milliseconds, use less RAM and less CPU. There is an alternative for Spring called Spring-Native but it’s still in a beta phase.

Project setup


Install GraalVM

For the installation of the JDK, I would strongly recommend to use sdkman. The tool will allow you to install multiple version of the JDK on your machine and to switch the version depending on the project your are working on. After the installation we will execute the following command to install the latest JDK:

$ sdk install java 21.1.0.r11-grl

Execute the following command after the installation to install the native-image component package

$ gu install native-image

Install Gradle

Gradle is a build automation tool for multi-language software development. We can install it the same way we install the SDK with sdkman. For more information about the gradle installation, follow the instruction from my previous Post.

Generate Quarkus Project

Quarkus libraries

To setup a quarkus project, the easiest way is to go to code.quarkus.io. Select the group and the artifact name of your project and select the following dependencies:

  • RESTEasy Jackson
  • Hibernate ORM with Panache
  • JDBC Driver - PostgreSQL
  • YAML Configuration

… and generate your project.

Lombok

Add the Lombok dependencies to your project to reduce the boilerplate code.

Add the following line in gradle.properties:

lombokVersion=1.18.20

Add the lombok dependency in build.gradle

compileOnly "org.projectlombok:lombok:${lombokVersion}"
annotationProcessor "org.projectlombok:lombok:${lombokVersion}"

Create the service


Database

Our stock-service will expose stock-data stored in a database. We will start to create a postgres database in a docker container. Create a file docker-compose.yml at the root of the project with the following content:

version: '3'
services:
  db:
    image: postgres
    restart: always
    ports:
      - "5432:5432"
    volumes:
      - database-data:/var/lib/postgresql/data/
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: quarkusdb
volumes:
  database-data:

Make sure the docker daemon is up and running on your maching and execute the following command to start the container:

$ docker compose up -d

Docker will download the latest postgres image from docker hub and create a container with a postgres database.

Create the JPA-Entity

Create a JPA Entity representing a stock table:

@Entity(name = "HISTORICAL_STOCK")
@Builder
@Getter
@Setter
@ToString(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
public class HistoricalStockEntity {
    @Id
    @GeneratedValue
    private Long id;
    private String symbol;
    private Currency currency;
    private OffsetDateTime stockDate;
    private BigDecimal open;
    private BigDecimal low;
    private BigDecimal high;
    private BigDecimal close;
}

Bootstrap some data

Create a file called import.sql in src/main/resources with the following content

INSERT INTO historical_stock (id, close, currency, high, low, open, stockdate, symbol) VALUES (1, 139.96, 'USD', 140.00, 137.75, 137.90, '2021-07-02 02:00:00.000000', 'AAPL');

Configure the database in Quarkus

Now that the database is created, we have to configure the connection in our project. This can be done in src/main/resources/application.yml

quarkus:
  datasource:
    db-kind: postgresql
    username: postgres
    password: postgres
    jdbc:
      url: jdbc:postgresql://localhost:5432/quarkusdb
  hibernate-orm:
    dialect: org.hibernate.dialect.PostgreSQLDialect
    database:
      generation: drop-and-create
    sql-load-script:
      import.sql

Create the Panache-Repository

Create a Repository extending PanacheRepository to get out of the box all the required CRUD methods.

@ApplicationScoped
public class StockRepository implements PanacheRepository<HistoricalStockEntity> {
}

Create the REST-service

Create a class named StockResource to expose the REST API. We’ll inject the StockRepository and retrieve all the stocks from the database.

@Path("/stock")
@AllArgsConstructor(onConstructor = @__(@Inject))
public class StockResource {

    private final StockRepository stockRepository;

    @GET
    @Path("all")
    @Produces(MediaType.APPLICATION_JSON)
    public List<HistoricalStockEntity> stocks() {
        return stockRepository.findAll().list();
    }
}

Run

We are now ready to call the application.

$ gradle quarkusDev

Open a browser and go to http://localhost:8080/stock/all to see all the stocks we saved in our database.

Testing

Quarkus comes with some interesting test features. In our example, we would like to bootstrap data in our database, call the Rest API and assert the result. For this, we will use an H2 Database for testing. Quarkus can automatically leverage an H2 Database for testing with some configuration.

Test configuration

To be able to test with an H2 Database in our project, we will have to first add some dependencies. I’ll use AssertJ for testing for the rich set of assertion it provides.

    testImplementation 'io.quarkus:quarkus-junit5'
    testImplementation 'io.quarkus:quarkus-test-h2'
    testImplementation "org.assertj:assertj-core:3.20.2"

Let’s create a file named application.yml in src/test/resources to configure the H2 database for our test:

quarkus:
  datasource:
    db-kind: h2
    jdbc:
      url: jdbc:h2:tcp://localhost/mem:test
      driver: org.h2.Driver
    hibernate-orm:
      dialect: org.hibernate.dialect.H2Dialect

Test class

Create a Test-class named StockResourcesTest to test the Rest API. The Annotation @QuarkusTest will automatically start the Quarkus application allowing us to call the Rest API. The use of @QuarkusTestResource(H2DatabaseTestResource.class) will automatically start an H2 Database for our test.

@QuarkusTest
@QuarkusTestResource(H2DatabaseTestResource.class)
public class StockResourcesTest {
    @Test
    public void testStockEndpoint() {
        final List<HistoricalStockEntity> stocks = given()
                .when().get("/stock/all")
                .then()
                .statusCode(200)
                .extract().as(new TypeRef<>() {});

        assertThat(stocks).extracting("id", "currency", "symbol", "open")
                .containsExactlyInAnyOrder(tuple(
                        1L,
                        Currency.getInstance(Locale.US),
                        "AAPL",
                        new BigDecimal("137.90")
                ));
    }
}

Native testing

Quarkus offers the possibility to test native executables. Create a test in src/native-test/java that will execute the same logic but on native code:

@NativeImageTest
public class NativeStockResourcesIT extends StockResourcesTest {

    // Execute the same tests but in native mode.
}

Those tests will be executed as pasrt of the build process when we execute the following command:

$ ./gradlew clean build -Dquarkus.package.type=native

Deployment on docker

Our application is now ready for development. As I said before, one of the main benefit of quarkus is the possibility to compiling the application to a native executable.

This could be achieved with the following command:

$ ./gradlew clean build -Dquarkus.package.type=native

One of the main drawbacks of this approach is that the executable will be build for the machine you are currently working on. To deploy our application in a docker container, we will have to compile it directly in the container. We will use a multi-stage docker build for that.

  • The first stage will build the native executable using Gradle
  • The second stage is a minimal image containing the native executable

Let’s create a dockerfile in src/main/docker called Dockerfile.multistage.

## Stage 1 : build with gradle builder image with native capabilities
FROM quay.io/quarkus/ubi-quarkus-native-image:21.1.0-java11 AS build
COPY gradlew /project/gradlew
COPY gradle /project/gradle
COPY build.gradle /project/
COPY settings.gradle /project/
COPY gradle.properties /project/
USER quarkus
WORKDIR /project
COPY src /project/src
RUN ./gradlew clean build -Dquarkus.package.type=native -b /project/build.gradle

## Stage 2 : create the docker final image
FROM registry.access.redhat.com/ubi8/ubi-minimal
WORKDIR /work/
RUN chown 1001 /work \
    && chmod "g+rwX" /work \
    && chown 1001:root /work
COPY --from=build --chown=1001:root /project/build/*-runner /work/application
USER 1001
CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]

Now let’s add the new service in our docker-compose.yml:

version: '3'
services:
  db:
    image: postgres
    restart: always
    ports:
      - "5432:5432"
    volumes:
      - database-data:/var/lib/postgresql/data/
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: quarkusdb
  stock-service:
    image: native/stock-service
    build:
      context: .
      dockerfile: src/main/docker/Dockerfile.multistage
    ports:
      - "8080:8080"
    depends_on:
      - db
    environment:
      QUARKUS_DATASOURCE_JDBC_URL: "jdbc:postgresql://db:5432/icoachconnect"
volumes:
  database-data:

Before building, make sure that you allocate at least 4 CPUs and at least 8Go RAM to Docker. The native compilation requires a large amount of resources and took approximately 5 minutes to build on my machine (MacBook Pro late 2013).

Let’s now start up our application by running:

$ docker-compose up -d

The log-file will produce the following output. We can there see that our application started in 0.118s.

__  ____  __  _____   ___  __ ____  ______ 
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/ 
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \   
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/   
2021-07-20 04:15:18,842 INFO  [io.quarkus] (main) code-with-quarkus 1.0.0-SNAPSHOT native (powered by Quarkus 2.0.2.Final) started in 0.118s. Listening on: http://0.0.0.0:8080
2021-07-20 04:15:18,842 INFO  [io.quarkus] (main) Profile prod activated. 
2021-07-20 04:15:18,843 INFO  [io.quarkus] (main) Installed features: [agroal, cdi, config-yaml, hibernate-orm, hibernate-orm-panache, hibernate-validator, jdbc-postgresql, narayana-jta, resteasy, resteasy-jackson, smallrye-context-propagation]

Summary


As a Spring fan, I have to admit that I was really amazed about Quarkus. The Framework has a lot to offer and is really easy to use. The native compilation is quite impressive and results in tiny container perfectly fit for the cloud.

Advantages

  • The startup time is really fast (3s in JVM mode, 0.118 seconds in native mode)
  • The Memory Footprint is lower than Spring, even in JVM mode.
  • The ecosystem is growing fast and you can easily integrate components like kafka, redis, …

Inconvenients

  • You have to be careful about integrating libraries as there is no guarantee that your application will compile “natively”.
  • The native compilation is only supported for Java 8 and 11. The GraalVM support for Java 16 is still experimental, and I was not able to generate a native executable on Java 16 for this example.
  • You have to compile your native executable on your target environment.

The source code of the project can be found in github.