DEV Community

loading...
Cover image for The JHipster Quarkus demo app

The JHipster Quarkus demo app

Stephan
Devoxx founder, conference organiser and passionate coder
・Updated on ・8 min read

Last weekend I wrote an article on creating 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

59Mb footprint and 0.056s startup time 😱πŸ’ͺ🏻

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.

Package structure

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;

@Cacheable
@Entity(name = "hipster_event")
public class Event extends PanacheEntity {

    @NotNull@Size(min = 3, max = 100)
    @Column(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;

@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;

    /**
     * Get all events.
     *
     * @return list of event DTOs.
     */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

@Path("api/events")
@ApplicationScoped
@Produces("application/json")
@Consumes("application/json")
public class EventResource {

    @Inject
    EventService eventService;

//...

    @GETpublic 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() {
        //List all, the database has initially 2 events
        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
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.

# Flyway minimal config properties
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 703ms
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 00:00.013s)
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 00:00.050s)
INFO  [io.quarkus] (main) Quarkus 0.15.0 started in 1.781s. Listening on: http://[::]:8080
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 -Dnative

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:

$ jhipster --skip-server

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:

JHipster welcome page

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!

Angular CRUD page

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

What's next?

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

Quarkus JHipster demo project

This is a basic JHipster Angular CRUD application using Quarkus as the backend service.

Checkout also my related LinkedIn article.

The backend code is very straight forward and uses the following Quarkus (extensions) :

  • RESTEasy to expose the REST endpoints
  • Hibernate ORM with Panache to perform the CRUD operations on the database
  • MapStruct for DTO mapping
  • FlyWay version control for the database tables
  • ArC, the CDI inspired dependency injection tool with zero overhead
  • The high performance Agroal connection pool
  • Infinispan based caching
  • All safely coordinated by the Narayana Transaction Manager
  • A PostgreSQL database; see below to run one via Docker

This demo application is based on the Quarkus example project 'hibernate-orm-panache-resteasy' provided by the RedHat team @ https://github.com/quarkusio/quarkus-quickstarts

Thanks to the Quarkus (Red Hat), JHipster, GraalVM teams for their amazing work!

Requirements

To compile and run this demo you will need:

  • GraalVM 1.0 rc16
  • Apache…

Or on GitLab @ https://gitlab.com/voxxed/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.

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

You can follow the related discussion on my Twitter timeline.

Discussion (0)