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.
Table Of Contents
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.
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.
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
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
You can download the full source code of this article here: Introduction into GraalVM (Community Edition): Cloud as a Changing Force