Last weekend I created the smallest possible Docker image for my JHipster application. The result was a 180Mb Docker image which starts on avg. in 56 seconds on Google Cloud.

On this rainy Sunday here in Belgium I decided to create a JHipster application which has the fastest possible startup.

Executive summary

MacBook Pro ('18) 2,9 Ghz Core i9 0,056s startup

iMac Pro ('17) 3 Ghz Intel Xeon...... 0,043s startup 😱

Footprint is 59Mb

Quarkus to the rescue!

Ever since Red Hat announced Quarkus I wanted to play with this new project and today was that day.

My ambition is basically to replace an existing Spring Boot app (generated by JHipster) and replace it with a Quarkus native version. Let's see how far we can get.

I wanted to mimic the package structure which JHipster uses, which is very logical. Under the service package you'll also find the DTO's and Mappers.

Domain: Hibernate with Panache

Let's start with first creating a (Conference) Event domain object which has a name and description fields.

package com.devoxx.hipster.domain; import io.quarkus.hibernate.orm.panache. PanacheEntity ; import javax.persistence.*; import javax.validation.constraints. NotNull ; import javax.validation.constraints. Size ; (name = "hipster_event" ) public class Event extends PanacheEntity { (min = 3 , max = 100 ) (nullable = false ) public String name; public String description; }

Hibernate Panache reminds me of Lombok (no getters and setters needed) and in addition you can also use bean validation. With a simple @Cacheable annotation you cam activate Infinispan caching.

EventRepository

package com.devoxx.hipster.repository; import com.devoxx.hipster.domain.Event; import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; import javax.enterprise.context.ApplicationScoped; public class EventRepository implements PanacheRepositoryBase < Event , Integer > { public Event findByName (String name) { return find( "name" , name).firstResult(); } }

The finder methods are created in the EventRepository. For my simple CRUD web application this is currently only an example method. I'm hoping to add paging and sorting functionality here in later versions.

Basic domain objects could be given directly to Angular but when introducing more complex and confidential fields (like emails or OAuth secrets) you want to have a DTO mapper between the domain and web REST package.

MapStruct

MapStruct is a code generator that greatly simplifies the implementation of mappings between Java bean types based on a convention over configuration approach.

JHipster heavily depends on MapStruct so I needed to investigate if it was possible with Quarkus. The actual Quarkus website doesn't mention it but Google did return some recent effort to make MapStruct part of the Quarkus eco-system, great!

package com.devoxx.hipster.service.mapper; import com.devoxx.hipster.domain.Event; import com.devoxx.hipster.service.dto.EventDTO; import org.mapstruct.Mapper; @ Mapper ( config = QuarkusMappingConfig.class) public interface EventMapper { EventDTO toDto(Event event); Event toEntity(EventDTO eventDTO); }

The EventMapper needs a reference to a QuarkusMappingConfig interface which tells Quarkus it's using CDI for dependency injection. There is Spring DI support for Quarkus but not sure if MapStruct already supports it?

package com.devoxx.hipster.service.mapper; import org.mapstruct.MapperConfig; @ MapperConfig ( componentModel = "cdi" ) interface QuarkusMappingConfig { }

Domain model, DTO and Mappers DONE 👍🏼

Service Layer

The EventService is very lightweight and I was very tempted to move this logic into the EventResource class but having a clean separation between these vertical layers will eventually be a good thing. So here we go...

@ApplicationScoped public class EventService { @Inject EventMapper eventMapper; public List<EventDTO> getAll () { Stream<Event> events = Event.streamAll(); return events.map( event -> eventMapper.toDto( event ) ) .collect(Collectors.toList()); } }

The final Service also includes code for getting one specific event (by id) and saving a DTO.

EventResource

( "api/events" ) ( "application/json" ) ( "application/json" ) public class EventResource { EventService eventService; List<EventDTO> getEvents () { List<EventDTO> allEvents = eventService.getAll(); if (allEvents == null ) { throw new WebApplicationException( "No events available" , HttpURLConnection.HTTP_NOT_FOUND); } return allEvents; } }

The EventResource has again no real surprises. Quarkus uses RestEasy and that's a dip switch I need to change in my neck coming from Spring REST, but I'll survive and learn.

Functional Testing

Writing some functional tests which consume the REST endpoints is again a fun experience and looks as follows.

Talking about fun, Quarkus also supports Kotlin. Should give that a try next.

@QuarkusTest class EventEndpointTest { @ Test void testGetOneEvent () { given() . when (). get ( "/api/events/1" ) .then() .statusCode(HttpURLConnection.HTTP_OK) .assertThat() .body(containsString( "Devoxx" ), containsString( "for developers" )); } @ Test void testGetAllEvents () { given() . when (). get ( "/api/events" ) .then() .statusCode(HttpURLConnection.HTTP_OK) .assertThat() .body( "size()" , is ( 2 )); } }

Let's FlyWay

JHipster uses Liquibase but Quarkus (for now) only supports FlyWay (Axel thanks for the Octoberfest invite but I have already a headache just looking at beer 🤪).

Next to adding the FlyWay maven dependency you need to activate it by adding the following line in the application.properties file.

quarkus.flyway.migrate-at-start= true

In the resources/db/migration directory you then add the SQL statements. Don't forget to add a PostgreSQL sequence generator otherwise the new domain objects will not get any ids.

CREATE SEQUENCE hibernate_sequence START 10 ; CREATE TABLE hipster_event ( id INT , name VARCHAR ( 100 ), description VARCHAR ( 255 ) ); INSERT INTO hipster_event( id , name , description) VALUES ( 1 , 'Devoxx Belgium 2019' , 'The developers conference from developers for developers' ), ( 2 , 'Devoxx UK 2019' , 'The developers conference in London' );

Now that we have all the logic in place lets see how we can run this baby.

GraalVM

You need to download GraalVM 1.0 rc16 and Apache Maven 3.5.3+.

Note: GraalVM v19.0 is not yet supported by Quarkus but looks like the Red Hat team is on it @ https://github.com/quarkusio/quarkus/issues/2412

After the project has been compiled and packaged by maven you can now start the Quarkus application:

$ mvn quarkus: dev

What's really cool is that Quarkus supports hot-reload of the project. Whenever a HTTP request hits the application, it reloads the app because it only takes a few milliseconds. Finally having hot-reload without setting up ZeroTurnaround's JRebel is a very nice bonus.

The most important output is listed below...

INFO [io.qua.dep.QuarkusAugmentor] (main) Beginning quarkus augmentation INFO [io.qua.fly.FlywayProcessor] (build- 6 ) Adding application migrations in path: file:/Users/stephan/java/projects/quarkushipster/backend/target/classes/db/migration/ INFO [io.qua.fly.FlywayProcessor] (build- 6 ) Adding application migrations in path: file:/Users/stephan/java/projects/quarkushipster/backend/target/classes/db/migration INFO [io.qua.dep.QuarkusAugmentor] (main) Quarkus augmentation completed in 703 ms INFO [org.fly.cor. int .lic.VersionPrinter] (main) Flyway Community Edition 5.2 .4 by Boxfuse INFO [org.fly.cor. int .dat.DatabaseFactory] (main) Database: jdbc:postgresql:quarkus_hipster (PostgreSQL 10.5 ) INFO [org.fly.cor. int .com.DbValidate] (main) Successfully validated 1 migration (execution time 0 0: 00.013 s) INFO [org.fly.cor. int .sch.JdbcTableSchemaHistory] (main) Creating Schema History table: "public" . "flyway_schema_history" INFO [org.fly.cor. int .com.DbMigrate] (main) Current version of schema "public" : << Empty Schema >> INFO [org.fly.cor. int .com.DbMigrate] (main) Migrating schema "public" to version 1.0 .0 - HIPSTER INFO [org.fly.cor. int .com.DbMigrate] (main) Successfully applied 1 migration to schema "public" (execution time 0 0: 00.050 s) INFO [io.quarkus] (main) Quarkus 0.15 .0 started in 1.781 s. Listening on: http: INFO [io.quarkus] (main) Installed features: [agroal, cdi, flyway, hibernate-orm, jdbc-postgresql, narayana-jta, resteasy, resteasy-jsonb]

OOOOOooooh Quarkus started my simple CRUD Java application in 1.781 seconds!

Quarkus 0.15.0 started in 1.781s.

And it's not even running in native mode yet 😱

Going Native

Before you can build a native package you need to install the GraalVM native-image tool. Change the shell directory to the GraalVM bin directory and type the following:

$ gu install native -image

Now you can create a native image of the Java application which will take a few tweets and one coffee (around 3 minutes depending on your computer).

$ mvn package -Pnative

The maven command will create a {project}{version}-runner application in the target directory. You can just start the application by executing it in a shell.

The native app first started "only" at around 5,056 seconds but it seemed I had to update my /etc/hosts and add my hostname to the localhost.

127.0.0.1 localhost Stephans-MacBook-Pro.local ::1 localhost Stephans-MacBook-Pro.local

Once that was added the native app started in 0,056s as shown below and the application is only 56Mb small 😱

And now the FrontEnd

Had to relax first a bit after the speed shock, but now let's generate the Angular 7 app using JHipster.

JHipster Client

We only need to create the Angular side of the project, which you can do as follows:

-- -

Once created we can now import the JDL (JHipster Domain Language) file which will create all the related Angular CRUD pages and logic. The current JDL only has the Event domain model in it with two fields: name & description.

$ jhipster import -jdl jhipster.jdl

This is too easy, the previous command produces state-of-the-art Angular TypeScript code in less than 5 minutes. An average developer would need a day (or more) to make this and probably bill the customer one week!

You run the Angular JHipster web app using npm start and then open your browser and point it to http://localhost:9000

This is what you get:

To show the conference event data I had to implement a few mock REST endpoints in Quarkus for the user authentication and account details.

I also disabled the ROLE_USER authorities in the event.route.ts file because this is not yet configured.

Once those changes were made I could enjoy my CRUD logic.... euh, wait... what? Damn... the browser doesn't like accessing port 9000 and accessing the REST backend endpoints on port 8080. Cross-Origin Resource Sharing (CORS).

Hmm, how will Quarkus handle CORS?

Google pointed me to an example CorsFilter I had to add to the Quarkus project, and that did the trick, great!

Update: Quarkus 0.16.0 now supports "quarkus.http.cors=true"

We now have a (none secure) Angular web app created by JHipster and talking to a Quarkus backend with a startup time of only a few milliseconds & hot-reload of both the web and the java modules.

What's next?

Another rainy weekend should allow me to add JWT (which Quarkus supports) but in the mean time you can checkout the project from

GitLab @ https://gitlab.com/voxxed/quarkushipster

GitHub @ https://github.com/devoxx/quarkusHipster

I do accept Merge Requests 😎👍🏼

Thanks again to the GraalVM, Quarkus and JHipster teams for making this magic possible!

Addendum

After publishing the article I got some interesting feedback on Twitter, looks like the startup time is still way too slow 😎

I should be getting around 0.015s in native mode but for some unknown reason (DNS resolving?) my native app starts only after 5 seconds. According to Emmanuel it might be related to some slow/unavailable DNS resolution.

You can follow the related discussion on my Twitter timeline.

The above issue was resolved by adding my hostname to the localhost in /etc/hosts!!

127.0.0.1 localhost Stephans-MacBook-Pro.local ::1 localhost Stephans-MacBook-Pro.local