Core Java

Introduction into GraalVM (Community Edition): Cloud as a Changing Force

1. Introduction

The shift towards cloud computing has had a massive impact on every single aspect of the software development process. In particular, the tools and frameworks the developers have had mastered for years suddenly became unfit, or to say it mildly, outdated. To reflect the reality, the whole new family of frameworks and libraries has emerged, collectively called cloud-native.

Cloud-native is an approach to building and running applications that exploits the advantages of the cloud computing delivery model.

https://tanzu.vmware.com/cloud-native

Admittedly, Java and the JVM in general were (and still are) a good choice if you want to run certain kind of applications and services in the cloud. But for others, like serverless for example, it was definitely feasible but from a cost perspective, it may not be always reasonable. The role of the GraalVM in pushing the JVM on the edge of cloud computing is hard to overestimate: from the startup time to memory footprint to packaging, everything has suddenly changed in favor of the JVM.

In this section of the tutorial we are going to talk about the new generation of JVM (primarily, Java) frameworks which are designed to build cloud-native applications and services while fully leveraging the capabilities provided by GraalVM. Although we will implement a completely functional demo service in each of them, any sorts of comparison between those frameworks are not on the table. Hence, in-depth optimizations and techniques (reducing native image size, debug symbols, …) are out of scope as well.

It is important to mention that while the tutorial was still in the work, a new version 21.0.0 of the GraalVM has been released. Nonetheless the examples were developed and packaged using older 2.3.x release line, they should work against the latest one smoothly.

2. Towards Cloud-Native

A bit of the history would help us to understand how the JVM ecosystem has evolved over the years. The Java EE has gained the reputation of bloated, complex and very costly platform only the very large enterprises could afford. When Spring Framework came out to offer the alternative to Java EE, it was an immediate success, and thanks to constant flow of innovations, it is relevant and very popular even today.

The rapid adoption of the cloud computing screamed for changes. In the environment where startup time and memory footprint matter, directly impacting the bills, both Java EE and Spring Framework had no choice but to adapt or to fade away. We are going to learn about their fates soon but the common themes we are about to see are radical shift from runtime instrumentation to build time generation and friendliness to GraalVM, in particular becoming native image ready from get-go.

To keep the tutorial practical, we are going to learn by building the sample Car Reservation service and package it as a native image. The service exposes its APIs over HTTP protocol and stores the reservations data in the relational database (for such purposes MySQL was picked).

2.1. Microprofile

Java EE was definitely not ready for such changes. The Eclipse Foundation took a lead here with the introduction of the MicroProfile specifications.

The MicroProfile project is aimed at optimizing Enterprise Java for the microservices architecture. […] The goal of the MicroProfile project is to iterate and innovate in short cycles to propose new common APIs and functionality, get community approval, release, and repeat.

https://projects.eclipse.org/projects/technology.microprofile

Another very important goal of the MicroProfile project was to leverage the skills and knowledge acquired by Java EE developers over the years and apply them to develop cloud-native applications and services (and surely, microservices). Some of the frameworks we are going to look at shortly do implement MicroProfile specifications. The landscape is changing very fast so it is better to always consult the up-to-date list of the implementation runtimes.

Last but not least, Java EE is dead now, superseded by Jakarta EE.

2.2. Micronaut

The Micronaut framework came out from the inventors of the Grails framework and its key maintainer and supporter is Object Computing, Inc.

A modern, JVM-based, full-stack framework for building modular, easily testable microservice and serverless applications.

https://micronaut.io/

If you happened to develop the applications and services using Grails and Spring, you may find a lot of recognizable concepts in Micronaut but implementation wise, they are likely to differ. The Micronaut is undergoing its second generation, with the 2.3.1 being the latest release to date.

Micronaut does not implement MicroProfile specification and relies purely on own set of APIs and annotations. As such, porting Micronaut applications to another framework, if the real need arises, could be quite challenging.

In Micronaut, the HTTP APIs should be annotated with @Controller annotation, optionally with validation enabled using @Validated annotation.

package com.javacodegeeks.micronaut.reservation;

import java.util.Collection;

import javax.inject.Inject;
import javax.validation.Valid;

import io.micronaut.http.HttpStatus;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Post;
import io.micronaut.http.annotation.Status;
import io.micronaut.validation.Validated;

@Validated
@Controller("/api/reservations")
public class ReservationController {
    private final ReservationService reservationService;
    
    @Inject
    public ReservationController(ReservationService reservationService) {
        this.reservationService = reservationService;
    }

    @Get
    public Collection<Reservation> list() {
        return reservationService.reservations();
    }
    
    @Post
    @Status(HttpStatus.CREATED)
    public Reservation reserve(@Body @Valid CreateReservation payload) {
        return reservationService.reserve(payload);
    }
}

On the service layer, we have ReservationService which essentially serves as the intermediary between the rest of the application and data access repositories.

@Named
@Singleton
public class ReservationService {
    private final ReservationRepository reservationRepository;
    
    @Inject 
    public ReservationService(ReservationRepository reservationRepository) {
        this.reservationRepository = reservationRepository;
    }
    
    public Collection<Reservation> reservations() {
        return StreamSupport
            .stream(reservationRepository.findAll().spliterator(), false)
            .map(this::convert)
            .collect(Collectors.toList());
    }

    public Reservation reserve(@Valid CreateReservation payload) {
        return convert(reservationRepository.save(convert(payload)));
    }
    
    private ReservationEntity convert(CreateReservation source) {
        final ReservationEntity entity = new ReservationEntity(UUID.randomUUID(), source.getVehicleId());
        entity.setFrom(source.getFrom());
        entity.setTo(source.getTo());
        entity.setStatus(Status.CREATED);
        return entity;
    }
    
    private Reservation convert(ReservationEntity source) {
        final Reservation reservation = new Reservation(source.getId());
        reservation.setVehicleId(source.getVehicleId());
        reservation.setFrom(source.getFrom());
        reservation.setTo(source.getTo());
        reservation.setStatus(source.getStatus().name());
        return reservation;
    }
}

So what is behind ReservationRepository then? Micronaut has a complementary project, Micronaut Data, a dedicated ahead-of-time (AOT) focused database access toolkit. Unsurprisingly, it takes a lot of inspiration from Spring Data project.

package com.javacodegeeks.micronaut.reservation;

import java.util.UUID;

import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.repository.CrudRepository;

@JdbcRepository(dialect = Dialect.MYSQL)
public interface ReservationRepository extends CrudRepository<ReservationEntity, UUID> { 
}

To run the application, just delegate that to the Micronaut class (a number of parallels could be drawn to widely popular Spring Boot approach).

package com.javacodegeeks.micronaut.reservation;

import io.micronaut.runtime.Micronaut;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Info;

@OpenAPIDefinition(
    info = @Info(
            title = "Reservations",
            version = "0.0.1-SNAPSHOT",
            description = "Exposes Car Reservations API"
    )
)
public class ReservationStarter {
    public static void main(String[] args) {
        Micronaut.run(ReservationStarter.class, args);
    }    
}

Please notice the out-of-the box support of the OpenAPI specification. The last piece to mention is the configuration, stored in application.yml, but plain property files are also supported (again, the familiar conventions ring a bell for Spring developers).

micronaut:
  application:
    name: reservations
  router:
    static-resources:
      swagger:
        enabled: true
        paths: classpath:META-INF/swagger
        mapping: /swagger/**
  server:
    netty:
      log-level: ERROR

swagger-ui:
  enabled: true

datasources:
  default:
    url: jdbc:mysql://localhost:3306/reservations_db
    driverClassName: com.mysql.cj.jdbc.Driver
    username: reservations
    password: passw0rd
    schema-generate: CREATE_DROP
    dialect: MYSQL
    
jackson:
  serialization:
    write-dates-as-timestamps: false

With that, we are ready to build the native image of our service. Since we are using Apache Maven as the build tool, we could take benefits of the Micronaut Maven Plugin and hint it that we would like to utilize native-image packaging instead of the default shaded JAR target (the Gradle builds are also well supported).

$ mvn clean package -Dpackaging=native-image
...
[micronaut-reservation:36248]     (clinit):     982.06 ms,  4.04 GB
[micronaut-reservation:36248]   (typeflow):  16,922.14 ms,  4.04 GB
[micronaut-reservation:36248]    (objects):  15,891.50 ms,  4.04 GB
[micronaut-reservation:36248]   (features):   2,089.40 ms,  4.04 GB
[micronaut-reservation:36248]     analysis:  37,850.74 ms,  4.04 GB
[micronaut-reservation:36248]     universe:   1,769.72 ms,  4.19 GB
[micronaut-reservation:36248]      (parse):   5,768.04 ms,  5.52 GB
[micronaut-reservation:36248]     (inline):   2,480.12 ms,  5.81 GB
[micronaut-reservation:36248]    (compile):  19,413.86 ms,  7.39 GB
[micronaut-reservation:36248]      compile:  30,310.64 ms,  7.39 GB
[micronaut-reservation:36248]        image:   5,218.65 ms,  7.39 GB
[micronaut-reservation:36248]        write:     480.16 ms,  7.39 GB
[micronaut-reservation:36248]      [total]:  80,011.26 ms,  7.39 GB 
[INFO] ----------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ----------------------------------------------------------------------
[INFO] Total time:  01:24 min
[INFO] Finished at: 2021-01-25T14:17:04-05:00
[INFO] ----------------------------------------------------------------------

The native image generation could take a while and consume quite large amount of memory, but at the end you end up with a self-sufficient executable. Without any optimizations, the size of the binary for the particular platform is around 76Mb. Let us run it.

$ ./target/micronaut-reservation
…
12:21:29.373 [main] DEBUG io.micronaut.context.DefaultBeanContext - Resolving beans for type:  io.micronaut.context.event.ApplicationEventListener 
12:21:29.373 [main] DEBUG io.micronaut.context.DefaultBeanContext - Qualifying bean [io.micronaut.context.event.ApplicationEventListener] from candidates [Definition: io.micronaut.runtime.context.scope.refresh.RefreshScope, Definition: io.micronaut.runtime.http.scope.RequestCustomScope] for qualifier: 
12:21:29.373 [main] DEBUG io.micronaut.context.DefaultBeanContext - Found no matching beans of type [io.micronaut.context.event.ApplicationEventListener] for qualifier: 
12:21:29.373 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 416ms. Server Running: http://localhost:8080

Please notice the startup time: 416ms. Issuing a HTTP request to create a new reservation confirms that the service is working as expected.

$ curl -X POST http://localhost:8080/api/reservations 
  -H "Content-Type: application/json" 
  -d '{
    "vehicleId": "07a6962a-723c-4fe2-8b64-5ac68f4f4b6f", 
    "from": "2025-01-20T00:00:00.000000000-05:00", 
    "to": "2025-01-25T23:59:59.000000000-05:00"
  }'

...
HTTP/1.1 201 Created
Date: Sat, 30 Jan 2021 17:22:42 GMT
Content-Type: application/json
...

{
    "id":"9f47de08-aaeb-47b4-b68b-078a435abe0f",
    "vehicleId":"07a6962a-723c-4fe2-8b64-5ac68f4f4b6f",
    "from":"2025-01-20T05:00:00Z",
    "to":"2025-01-26T04:59:59Z",
    "status":"CREATED"
}

Sending another request to list all available reservations proves our newly created one is properly persisted in the database.

$ curl http://localhost:8080/api/reservations

...
HTTP/1.1 200 OK
Content-Type: application/json
...

[
    {
        "id":"9f47de08-aaeb-47b4-b68b-078a435abe0f",
        "vehicleId":"07a6962a-723c-4fe2-8b64-5ac68f4f4b6f",
        "from":"2025-01-20T00:00:00-05:00",
        "to":"2025-01-25T23:59:59-05:00",
        "status":"CREATED"
    }
]

Overall, developing services with Micronaut turned out to be rather simple, and in case you are experienced Spring developer, very intuitive. However, there are a number of rough edges: not every integration supported by the Micronaut could be packaged inside native image.

2.3. Helidon

The next framework we are going to look at is Helidon project, backed by Oracle, that positions itself as a set of Java libraries for writing microservices.

Helidon provides an open-source, lightweight, fast, reactive, cloud native framework for developing Java microservices. Helidon implements and supports MicroProfile, a baseline platform definition that leverages Java EE and Jakarta EE technologies for microservices and delivers application portability across multiple runtimes.

https://helidon.io/docs/v2/#/about/02_introduction

Similarly to Micronaut, it goes over the second generation with 2.2.0 being the latest release as of the moment of this writing. One of the Helidon strength is support of the two programming models: Helidon MP (MicroProfile 3.3) and Helidon SE (a small, functional style API). For our Car Reservation service, the MicroProfile favor (Helidon MP) sounds like a great fit.

The MicroProfile brings in the JAX-RS, JSON-B, JPA and CDI along with a whole bunch of other Jakarta EE specifications to serve the needs of modern application and services. The ReservationResource below is an example of the minimalistic JAX-RS resource implementation.

package com.javacodegeeks.helidon.reservation;

import java.util.Collection;

import javax.inject.Inject;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;

@Path("/api/reservations")
public class ReservationResource {
    private final ReservationService reservationService;
    
    @Inject
    public ReservationResource(ReservationService reservationService) {
        this.reservationService = reservationService;
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Collection<Reservation> list() {
        return reservationService.reservations();
    }
    
    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response reserve(@Valid CreateReservation payload, @Context UriInfo uri) {
        final Reservation reservation = reservationService.reserve(payload);
        return Response
            .created(uri
                .getRequestUriBuilder()
                .path(reservation.getId().toString())
                .build())
            .entity(reservation).build();
    }
} 

The ReservationService has mostly no changes comparing to the Micronaut version, beside the presence of the @Transactional annotations.

package com.javacodegeeks.helidon.reservation;

import java.util.Collection;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.transaction.Transactional;
import javax.validation.Valid;

@ApplicationScoped
public class ReservationService {
    private final ReservationRepository reservationRepository;
    
    @Inject 
    public ReservationService(ReservationRepository reservationRepository) {
        this.reservationRepository = reservationRepository;
    }
    
    @Transactional
    public Collection<Reservation> reservations() {
        return StreamSupport
            .stream(reservationRepository.findAll().spliterator(), false)
            .map(this::convert)
            .collect(Collectors.toList());
    }

    @Transactional
    public Reservation reserve(@Valid CreateReservation payload) {
        return convert(reservationRepository.save(convert(payload)));
    }
    
    private ReservationEntity convert(CreateReservation source) {
        final ReservationEntity entity = new ReservationEntity(UUID.randomUUID(), source.getVehicleId());
        entity.setFrom(source.getFrom());
        entity.setTo(source.getTo());
        entity.setStatus(Status.CREATED);
        return entity;
    }
    
    private Reservation convert(ReservationEntity source) {
        final Reservation reservation = new Reservation(source.getId());
        reservation.setVehicleId(source.getVehicleId());
        reservation.setFrom(source.getFrom());
        reservation.setTo(source.getTo());
        reservation.setStatus(source.getStatus().name());
        return reservation;
    }
}

Probably the repository is the only part we have to write by hand. Luckily, the JPA programming model makes it quite straightforward.

package com.javacodegeeks.helidon.reservation;

import java.util.Collection;

import javax.enterprise.context.ApplicationScoped;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;

@ApplicationScoped
public class ReservationRepository { 
    @PersistenceContext(unitName = "reservations-db")
    private EntityManager entityManager;

    public ReservationEntity save(ReservationEntity reservation) {
        entityManager.persist(reservation);
        return reservation;
    }

    public Collection<ReservationEntity> findAll() {
        final CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        final CriteriaQuery<ReservationEntity> cq = cb.createQuery(ReservationEntity.class);
        final Root<ReservationEntity> root = cq.from(ReservationEntity.class);
        final CriteriaQuery<ReservationEntity> all = cq.select(root);
        return entityManager.createQuery(all).getResultList();
    }
}

However, if you really like the Micronaut Data or Spring Data repositories, there are good news: Helidon now supports the integration with Micronaut Data (so technically speaking, we could have just borrowed Micronaut data repository). And finally, here are our configuration properties, stored in the microprofile-config.properties file.

server.port=8080
server.host=0.0.0.0

metrics.rest-request.enabled=true

javax.sql.DataSource.test.dataSource.url=jdbc:mysql://localhost:3306/reservations_db
javax.sql.DataSource.test.dataSource.user=reservations
javax.sql.DataSource.test.dataSource.password=passw0rd
javax.sql.DataSource.test.dataSourceClassName=com.mysql.cj.jdbc.MysqlDataSource

The generation of the native image is a bit different from the Micronaut way. It is still backed by dedicated Helidon Maven Plugin but relies on the Apache Maven profiles instead (the Gradle support is documented as well).

$ mvn clean package -Pnative-image
...
[INFO] [helidon-reservation:13280]     (clinit):   2,067.56 ms,  6.41 GB
[INFO] [helidon-reservation:13280]   (typeflow):  43,302.65 ms,  6.41 GB
[INFO] [helidon-reservation:13280]    (objects):  56,963.05 ms,  6.41 GB
[INFO] [helidon-reservation:13280]   (features):   6,825.32 ms,  6.41 GB
[INFO] [helidon-reservation:13280]     analysis: 114,147.59 ms,  6.41 GB
[INFO] [helidon-reservation:13280]     universe:   4,030.03 ms,  6.43 GB
[INFO] [helidon-reservation:13280]      (parse):   9,677.05 ms,  7.51 GB
[INFO] [helidon-reservation:13280]     (inline):   8,340.03 ms,  7.16 GB
[INFO] [helidon-reservation:13280]    (compile):  26,844.31 ms,  8.63 GB
[INFO] [helidon-reservation:13280]      compile:  49,766.42 ms,  8.63 GB
[INFO] [helidon-reservation:13280]        image:  12,509.20 ms,  8.71 GB
[INFO] [helidon-reservation:13280]        write:     740.35 ms,  8.71 GB
[INFO] [helidon-reservation:13280]      [total]: 188,043.89 ms,  8.71 GB
[INFO] ----------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ----------------------------------------------------------------------
[INFO] Total time:  03:22 min
[INFO] Finished at: 2021-01-30T15:24:56-05:00
[INFO] ----------------------------------------------------------------------

In case of Helidon, the native image generation took much longer (comparing to Micronaut) and the resulting non-optimized executable size is twice as large: 188Mb. Nonetheless, it is ready for prime time.

$ ./target/helidon-reservation
...
2021.01.30 15:25:13 INFO io.helidon.microprofile.server.ServerCdiExtension Thread[main,5,main]: Registering JAX-RS Application: HelidonMP
2021.01.30 15:25:13 INFO io.helidon.webserver.NettyWebServer Thread[nioEventLoopGroup-2-1,10,main]: Channel '@default' started: [id: 0x01881e0a, L:/0:0:0:0:0:0:0:0:8080]
2021.01.30 15:25:13 INFO io.helidon.microprofile.server.ServerCdiExtension Thread[main,5,main]: Server started on http://localhost:8080 (and all other host addresses) in 172 milliseconds (since JVM startup).
2021.01.30 15:25:13 INFO io.helidon.common.HelidonFeatures Thread[features-thread,5,main]: Helidon MP 2.2.0 features: [CDI, Config, Fault Tolerance, Health, JAX-RS, JPA, JTA, Metrics, Open API, REST Client, Security, Server, Tracing]
...

This time around, the service startup took just 172ms, very impressive. To confirm that things do actually work, let us send a few HTTP requests.

$ curl -X POST http://localhost:8080/api/reservations 
     -H "Content-Type: application/json"
     -d '{
        "vehicleId": "07a6962a-723c-4fe2-8b64-5ac68f4f4b6f", 
        "from": "2025-01-20T00:00:00.000000000-05:00", 
        "to": "2025-01-25T23:59:59.000000000-05:00"
    }'

...
HTTP/1.1 201 Created
Content-Type: application/json
Location: http://[0:0:0:0:0:0:0:1]:8080/api/reservations/e30fbbc7-9919-4071-b2f3-ad8721743543
...

{
    "from":"2025-01-20T00:00:00-05:00",
    "id":"e30fbbc7-9919-4071-b2f3-ad8721743543",
    "status":"CREATED",
     "to":"2025-01-25T23:59:59-05:00",
    "vehicleId":"07a6962a-723c-4fe2-8b64-5ac68f4f4b6f"
}

Indeed, the reservation seems to be created, let us check if it is persisted as well.

$ curl http://localhost:8080/api/reservations

...
HTTP/1.1 200 OK
Content-Type: application/json
...
[
    {
        "from":"2025-01-20T00:00:00-05:00",
        "id":"e30fbbc7-9919-4071-b2f3-ad8721743543",
        "status":"CREATED","to":"2025-01-25T23:59:59-05:00",
        "vehicleId":"07a6962a-723c-4fe2-8b64-5ac68f4f4b6f"
    }
]

By all means, Helidon would be a no-brainer for experienced Java EE developers to start with. On the cautionary note, please do not expect that all Helidon features could be used along with native image packaging (although each release makes it more complete).

2.4. Quarkus

One of the pioneering cloud-native frameworks on the JVM is Quarkus. It was born at RedHat, the well-established name in the industry and open source community.

A Kubernetes Native Java stack tailored for OpenJDK HotSpot and GraalVM, crafted from the best of breed Java libraries and standards

https://quarkus.io/

The Quarkus mojo, “Supersonic Subatomic Java”, says it all. Internally, Quarkus  heavily depends on the Vert.x toolkit and the MicroProfile project. Its latest version as of this moment is 1.11.1 which we are going to use for implementing Car Reservation service. Being compliant with MicroProfile means we could reuse most of the implementation from the Helidon section without any changes.

With that being said, ReservationResource stays unchanged. We could have used the repository as-is as well but Quarkus has something different to offer: Panache ORM with the active record pattern.

package com.javacodegeeks.quarkus.reservation;

import java.util.Collection;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;

import javax.enterprise.context.ApplicationScoped;
import javax.transaction.Transactional;
import javax.validation.Valid;

@ApplicationScoped
public class ReservationService {
    @Transactional
    public Collection<Reservation> reservations() {
        List<ReservationEntity> list = ReservationEntity.listAll();
        return list
            .stream()
            .map(this::convert)
            .collect(Collectors.toList());
    }

    @Transactional
    public Reservation reserve(@Valid CreateReservation payload) {
        final ReservationEntity entity = convert(payload);
        ReservationEntity.persist(entity);
        return convert(entity);
    }
    
    private ReservationEntity convert(CreateReservation source) {
        final ReservationEntity entity = new ReservationEntity(UUID.randomUUID(), source.getVehicleId());
        entity.setFrom(source.getFrom());
        entity.setTo(source.getTo());
        entity.setStatus(Status.CREATED);
        return entity;
    }
    
    private Reservation convert(ReservationEntity source) {
        final Reservation reservation = new Reservation(source.getId());
        reservation.setVehicleId(source.getVehicleId());
        reservation.setFrom(source.getFrom());
        reservation.setTo(source.getTo());
        reservation.setStatus(source.getStatus().name());
        return reservation;
    }
}

For some developers the active record pattern may not look familiar. Essentially, instead of introducing dedicated data repositories, the domain entity itself carries data and behavior. Anyway, if you prefer the data repositories, Panache ORM supports that too.

The configuration for Quarkus is provided by application.properties file.

quarkus.datasource.db-kind=mysql
quarkus.datasource.username=reservations
quarkus.datasource.password=passw0rd
quarkus.datasource.jdbc.url=jdbc:mysql://localhost:3306/reservations_db

quarkus.hibernate-orm.database.generation=drop-and-create
quarkus.hibernate-orm.log.sql=true

Similarly to others, Quarkus has excellent support of Apache Maven build tooling with Quarkus Maven Plugin. However, Gradle support is still considered to be in preview. Interestingly enough, Quarkus introduces yet another style of building different packaging targets, including native images, using quarkus.package.type property.

$ mvn clean package -Dquarkus.package.type=native
...
[quarkus-reservation-0.0.1-SNAPSHOT-runner:26932]     (clinit):     989.80 ms,  4.89 GB
[quarkus-reservation-0.0.1-SNAPSHOT-runner:26932]   (typeflow):  13,471.86 ms,  4.89 GB
[quarkus-reservation-0.0.1-SNAPSHOT-runner:26932]    (objects):  13,998.29 ms,  4.89 GB
[quarkus-reservation-0.0.1-SNAPSHOT-runner:26932]   (features):     696.73 ms,  4.89 GB
[quarkus-reservation-0.0.1-SNAPSHOT-runner:26932]     analysis:  30,435.55 ms,  4.89 GB
[quarkus-reservation-0.0.1-SNAPSHOT-runner:26932]     universe:   1,717.63 ms,  4.89 GB
[quarkus-reservation-0.0.1-SNAPSHOT-runner:26932]      (parse):   5,480.96 ms,  6.32 GB
[quarkus-reservation-0.0.1-SNAPSHOT-runner:26932]     (inline):   4,920.91 ms,  7.27 GB
[quarkus-reservation-0.0.1-SNAPSHOT-runner:26932]    (compile):  15,508.72 ms,  7.41 GB
[quarkus-reservation-0.0.1-SNAPSHOT-runner:26932]      compile:  28,509.71 ms,  7.41 GB
[quarkus-reservation-0.0.1-SNAPSHOT-runner:26932]        image:   5,093.31 ms,  7.41 GB
[quarkus-reservation-0.0.1-SNAPSHOT-runner:26932]        write:     453.13 ms,  7.41 GB
[quarkus-reservation-0.0.1-SNAPSHOT-runner:26932]      [total]:  71,534.52 ms,  7.41 GB
[WARNING] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] objcopy executable not found in PATH. Debug symbols will not be separated from executable.
[WARNING] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] That will result in a larger native image with debug symbols embedded in it.
[INFO] [io.quarkus.deployment.QuarkusAugmentor] Quarkus augmentation completed in 78789ms
[INFO] ----------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ----------------------------------------------------------------------
[INFO] Total time:  01:22 min
[INFO] Finished at: 2021-02-02T15:41:15-05:00
[INFO] ----------------------------------------------------------------------

The image generation time is comparable to Micronaut, with the end result producing the binary of similar size, a bit less than 70Mb for this particular platform. Also please notice that by default Quarkus Maven Plugin adds –runner suffix to the final executables.

$ ./target/quarkus-reservation-0.0.1-SNAPSHOT-runner
...
__  ____  __  _____   ___  __ ____  ______
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2021-02-02 15:44:01,788 INFO  [io.quarkus] (main) quarkus-reservation 0.0.1-SNAPSHOT native (powered by Quarkus 1.11.1.Final) started in 0.233s. Listening on: http://0.0.0.0:8080
2021-02-02 15:44:01,788 INFO  [io.quarkus] (main) Profile prod activated.
2021-02-02 15:44:01,788 INFO  [io.quarkus] (main) Installed features: [agroal, cdi, hibernate-orm, hibernate-orm-panache, jdbc-mysql, mutiny, narayana-jta, resteasy, resteasy-jackson, smallrye-context-propagation]
...

In just about 200ms, our Car Reservation service powered by Quarkus is ready to process the requests, very thrilling. Since we never trust the output, a couple of HTTP requests should prove it is the case.

$ curl -X POST http://localhost:8080/api/reservations 
     -H "Content-Type: application/json"
     -d '{
        "vehicleId": "07a6962a-723c-4fe2-8b64-5ac68f4f4b6f", 
        "from": "2025-01-20T00:00:00.000000000-05:00", 
        "to": "2025-01-25T23:59:59.000000000-05:00"
    }'

...
HTTP/1.1 201 Created                                                                  
Content-Type: application/json                                                          
Location: http://localhost:8080/api/reservations/678c3fb4-4a6a-475d-b456-0ee29c836498
...

{
    "id":"678c3fb4-4a6a-475d-b456-0ee29c836498",
    "vehicleId":"07a6962a-723c-4fe2-8b64-5ac68f4f4b6f",
    "from":"2025-01-20T05:00:00Z",
    "to":"2025-01-26T04:59:59Z",
    "status":"CREATED"
}

The persistence layer is working according to the plan.

$ curl http://localhost:8080/api/reservations

...
HTTP/1.1 200 OK
Content-Type: application/json
...

[
    {
        "id":"678c3fb4-4a6a-475d-b456-0ee29c836498",
        "vehicleId":"07a6962a-723c-4fe2-8b64-5ac68f4f4b6f",
        "from":"2025-01-20T00:00:00-05:00",
        "to":"2025-01-25T23:59:59-05:00",
        "status":"CREATED"
    }
]

It is hard to sound unbiased, but out of those three frameworks we have talked about so far, Quarkus looks the most mature and complete (in terms of native images generation coverage) at this point. But the ecosystem is evolving so fast that it may not be true anymore.

One notable experimental feature of Quarkus is ongoing work to provide the support of the build packs, yet another way to package Quarkus applications and services.

2.5. Spring

Why would we talk about Spring whereas there are so many new and shiny frameworks to choose from? Indeed, Spring does not qualify for “new generation” category, it is rather an old one. But because of the relentless innovation and evolution, it is still one of the most widely used frameworks for developing production grade applications and services on the JVM.

Obviously, the support of GraalVM by Spring was not left off and the work in this direction has started quite a while ago. Due to a number of factors, it turned out that running Spring applications as native images is not that easy overall but we are getting there. The effort is still not merged into the mainstream (hopefully, it will be soon) and is hosted under new experimental spring-native project.

In conclusion, our last reimplementation of the Car Reservation service is going to be a typical Spring Boot application, power by Spring Data and Spring MVC.

package com.javacodegeeks.spring.reservation;

import java.util.Collection;

import javax.validation.Valid;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@RestController 
@RequestMapping("/api/reservations")
public class ReservationController {
    private final ReservationService reservationService;
    
    @Autowired
    public ReservationController(ReservationService reservationService) {
        this.reservationService = reservationService;
    }

    @GetMapping
    public Collection<Reservation> list() {
        return reservationService.reservations();
    }
    
    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Reservation reserve(@RequestBody @Valid CreateReservation payload) {
        return reservationService.reserve(payload);
    }
}

The ReservationService is thin layer which delegates all the work to the ReservationRepository.

package com.javacodegeeks.spring.reservation;

import java.util.Collection;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

import javax.validation.Valid;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class ReservationService {
    private final ReservationRepository reservationRepository;
    
    @Autowired
    public ReservationService(ReservationRepository reservationRepository) {
        this.reservationRepository = reservationRepository;
    }
    
    public Collection<Reservation> reservations() {
        return StreamSupport
            .stream(reservationRepository.findAll().spliterator(), false)
            .map(this::convert)
            .collect(Collectors.toList());
    }

    public Reservation reserve(@Valid CreateReservation payload) {
        return convert(reservationRepository.save(convert(payload)));
    }
    
    private ReservationEntity convert(CreateReservation source) {
        final ReservationEntity entity = new ReservationEntity(UUID.randomUUID(), source.getVehicleId());
        entity.setFrom(source.getFrom());
        entity.setTo(source.getTo());
        entity.setStatus(Status.CREATED);
        return entity;
    }
    
    private Reservation convert(ReservationEntity source) {
        final Reservation reservation = new Reservation(source.getId());
        reservation.setVehicleId(source.getVehicleId());
        reservation.setFrom(source.getFrom());
        reservation.setTo(source.getTo());
        reservation.setStatus(source.getStatus().name());
        return reservation;
    }
}

In turn, ReservationRepository is a straightforward Spring Data repository which uses JPA and Hibernate under the hood.

package com.javacodegeeks.spring.reservation;

import java.util.UUID;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface ReservationRepository extends JpaRepository<ReservationEntity, UUID> { 
}

In order to run the application, it is sufficient to hand it off to the SpringApplication class, following the idiomatic Spring Boot style.

package com.javacodegeeks.spring.reservation;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication(proxyBeanMethods = false)
public class ReservationStarter {
    public static void main(String[] args) {
        SpringApplication.run(ReservationStarter.class, args);
    }
}

The configuration for our Spring Boot application is crafted in the YAML format and is stored in application.yml file.

spring:
  application:
    name: reservations
  datasource:
    url: jdbc:mysql://localhost:3306/reservations_db?serverTimezone=UTC
    driverClassName: com.mysql.cj.jdbc.Driver
    username: reservations
    password: passw0rd
    platform: MYSQL
    schema: classpath*:db/mysql/schema.sql
    initialization-mode: always
  jpa:
    database-platform: org.hibernate.dialect.MySQLDialect
    properties:
      hibernate.temp.use_jdbc_metadata_defaults: false

In contrast to others, Spring offers two options with respect to building native images, equally available for Apache Maven and Gradle. The first one is to use Spring Boot Maven Plugin (or Spring Boot Gradle Plugin) along with build packs. It is dead simple and does not require anything else besides Docker. The second option, which we are going to employ, is to create a dedicated profile (for example, native-image) and delegate the work to GraalVM ‘s Native Image Maven Plugin (which we covered in the previous part of the tutorial). Whatever your preference is, the result would be the same.

$ mvn clean package -Pnative-image
...
[spring-reservation:7332]     (clinit):   2,584.02 ms,  6.29 GB
[spring-reservation:7332]   (typeflow):  38,407.84 ms,  6.29 GB
[spring-reservation:7332]    (objects):  54,864.08 ms,  6.29 GB
[spring-reservation:7332]   (features):  11,538.16 ms,  6.29 GB
[spring-reservation:7332]     analysis: 111,131.94 ms,  6.29 GB
[spring-reservation:7332]     universe:   3,843.78 ms,  6.29 GB
[spring-reservation:7332]      (parse):  10,252.26 ms,  7.21 GB
[spring-reservation:7332]     (inline):   7,389.53 ms,  8.72 GB
[spring-reservation:7332]    (compile):  28,096.23 ms,  9.18 GB
[spring-reservation:7332]      compile:  50,900.61 ms,  9.18 GB
[spring-reservation:7332]        image:  13,801.12 ms,  8.79 GB
[spring-reservation:7332]        write:     814.61 ms,  8.79 GB
[spring-reservation:7332]      [total]: 189,239.73 ms,  8.79 GB
[INFO]
[INFO] --- spring-boot-maven-plugin:2.4.2:repackage (repackage) @ spring-reservation ---
[INFO] Replacing main artifact with repackaged archive
----------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ----------------------------------------------------------------------
[INFO] Total time:  03:16 min
[INFO] Finished at: 2021-02-03T22:03:26-05:00
[INFO] ----------------------------------------------------------------------


In around 3 minutes, the natively executable Spring application is out of the oven, weighting in range of 185Mb for this particular platform.  Let us see how fast it starts up.

$ ./target/spring-reservation

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

...
2021-02-04 16:10:21.258  INFO 33356 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2021-02-04 16:10:21.259  INFO 33356 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 0 ms. Found 1 JPA repository interfaces.
2021-02-04 16:10:21.428  INFO 33356 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
Feb 04, 2021 4:10:21 PM org.apache.coyote.AbstractProtocol init
INFO: Initializing ProtocolHandler ["http-nio-8080"]
Feb 04, 2021 4:10:21 PM org.apache.catalina.core.StandardService startInternal
INFO: Starting service [Tomcat]
Feb 04, 2021 4:10:21 PM org.apache.catalina.core.StandardEngine startInternal
INFO: Starting Servlet engine: [Apache Tomcat/9.0.41]
Feb 04, 2021 4:10:21 PM org.apache.catalina.core.ApplicationContext log
INFO: Initializing Spring embedded WebApplicationContext
2021-02-04 16:10:21.431  INFO 33356 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 207 ms
2021-02-04 16:10:21.434  WARN 33356 --- [           main] i.m.c.i.binder.jvm.JvmGcMetrics          : GC notifications will not be available because MemoryPoolMXBeans are not provided by the JVM
2021-02-04 16:10:21.453  INFO 33356 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2021-02-04 16:10:21.481  INFO 33356 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2021-02-04 16:10:21.494  INFO 33356 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [name: default]
2021-02-04 16:10:21.495  INFO 33356 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate ORM core version 5.4.27.Final
2021-02-04 16:10:21.495  INFO 33356 --- [           main] org.hibernate.cfg.Environment            : HHH000205: Loaded properties from resource hibernate.properties: {hibernate.bytecode.use_reflection_optimizer=false, hibernate.bytecode.provider=none}
2021-02-04 16:10:21.495  INFO 33356 --- [           main] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {5.1.2.Final}
2021-02-04 16:10:21.496  INFO 33356 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.MySQLDialect
2021-02-04 16:10:21.499  INFO 33356 --- [           main] o.h.e.t.j.p.i.JtaPlatformInitiator       : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
2021-02-04 16:10:21.500  INFO 33356 --- [           main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2021-02-04 16:10:21.523  WARN 33356 --- [           main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2021-02-04 16:10:21.532  INFO 33356 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2021-02-04 16:10:21.560  INFO 33356 --- [           main] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 2 endpoint(s) beneath base path '/actuator'
Feb 04, 2021 4:10:21 PM org.apache.coyote.AbstractProtocol start
INFO: Starting ProtocolHandler ["http-nio-8080"]
2021-02-04 16:10:21.564  INFO 33356 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2021-02-04 16:10:21.565  INFO 33356 --- [           main] c.j.s.reservation.ReservationStarter     : Started ReservationStarter in 0.353 seconds (JVM running for 0.355)
...

The familiar banner appears in the console and in 353ms the application is fully started. For a seasoned Spring developers, it is truly extraordinary to see such timing. Let us validate that the service is indeed ready by sending a few requests from the command line.

$ curl -X POST http://localhost:8080/api/reservations 
     -H "Content-Type: application/json"
     -d '{
        "vehicleId": "07a6962a-723c-4fe2-8b64-5ac68f4f4b6f", 
        "from": "2025-01-20T00:00:00.000000000-05:00", 
        "to": "2025-01-25T23:59:59.000000000-05:00"
    }'

...
HTTP/1.1 201 Created                                                                  
Content-Type: application/json                                                          
...

{
    "id":"c190417e-672a-4889-98a9-7fe433e00dcb",
    "vehicleId":"07a6962a-723c-4fe2-8b64-5ac68f4f4b6f",
    "from":"2025-01-20T05:00:00Z",
    "to":"2025-01-26T04:59:59Z",
    "status":"CREATED"
}

Checking the available reservations confirms that persistence layer is doing its job.

$ curl http://localhost:8080/api/reservations

...
HTTP/1.1 200 OK
Content-Type: application/json
...

[
   {
       "id":"c190417e-672a-4889-98a9-7fe433e00dcb",
        "vehicleId":"07a6962a-723c-4fe2-8b64-5ac68f4f4b6f",
        "from":"2025-01-20T00:00:00-05:00",
        "to":"2025-01-25T23:59:59-05:00",
        "status":"CREATED"
    }
]

Spring is continuing to impress and stay relevant. To the millions of the JVM developers out there, this is great news. Hopefully, by the time Spring Boot 2.5 comes out, the spring-native would drop the experimental label and become ready for production, promoting native Spring applications and services to the new normal.

3. Serverless

The JVM’s startup time and memory footprint were two major blockers on its path to power serverless and function-as-a-service (FaaS) deployments. But, as we have seen, GraalVM and its native-image builder are the game changers. Today, it is absolutely feasible to develop quite sophisticated JVM applications and services, package them as native executables, deploy with a cloud provider of your choice and achieve startup times in just a fraction of a second. And all that consuming quite reasonable amount of memory.

4. What’s Next

In the next part of the tutorial, we are going to look beyond just JVM support and explorer the polyglot capabilities of the GraalVM.

5. Download the source code

Download
You can download the full source code of this article here: Introduction into GraalVM (Community Edition): Cloud as a Changing Force

Andrey Redko

Andriy is a well-grounded software developer with more then 12 years of practical experience using Java/EE, C#/.NET, C++, Groovy, Ruby, functional programming (Scala), databases (MySQL, PostgreSQL, Oracle) and NoSQL solutions (MongoDB, Redis).
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button