RESTful Webservices mit Quarkus

2 Kommentare

Im ersten Artikel zu Quarkus wurde beschrieben, wie man es nutzen kann und was die theoretischen Hintergründe sind. In diesem Artikel wird beleuchtet, wie mit Quarkus eine vollständige REST-Anwendung erstellt werden kann. In der Anwendung werden verschiedene Extensions vorgestellt, die für Quarkus zur Verfügung stehen.

Zielanwendung

Es soll eine Anwendung entwickelt werden, die als Backend für einen digitalen Einkaufszettel dient. Man soll Einkaufszettel anlegen können, die eine Liste von Artikeln enthalten. Die Artikel bestehen aus einer Bezeichnung und einer Menge bzw. Anzahl. Zusätzlich soll gespeichert werden, welche Artikel sich schon im Einkaufswagen befinden.
Die Anwendung soll REST-Schnittstellen für CRUD-Operationen auf den Daten bereitstellen. Die Daten des Einkaufszettels werden in einer PostgreSQL-Datenbank gespeichert. Das Schema für die Datenbank wird über Flyway definiert und der Datenbankzugriff aus der Anwendung soll über Hibernate mit Panache erfolgen. Die API-Definition soll außerdem über Swagger bereitgestellt werden.
Der Beispielcode für diese Anwendung steht auf GitHub zur Verfügung: Beispielprojekt

Aufsetzen des Projektes

Das initiale Projekt wird mithilfe des Quarkus-Maven-Plug-ins erstellt. Hierzu wird folgender Befehl im Terminal eingegeben:

mvn io.quarkus:quarkus-maven-plugin:0.15.0:create \
-DprojectGroupId=de.codecentric.quarkus \
-DprojectArtifactId=shoppinglist \
-DprojectVersion=0.0.1 \

Der Befehl erstellt ein neues Maven-Projekt mit den für die Arbeit mit Quarkus notwendigen initialen Dependencys und Konfigurationen. Über das mitgelieferte Dependency-Management wird sichergestellt, dass für die verwendete Quarkus-Version immer die passenden Extension-Versionen bereitgestellt werden. Die standardmäßig eingebundenen Dependencys sind RESTEasy, quarkus-junit5 und REST Assured.

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-bom</artifactId>
      <version>${quarkus.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>
<dependencies>
  <dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-resteasy</artifactId>
  </dependency>
  <dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-junit5</artifactId>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>io.rest-assured</groupId>
    <artifactId>rest-assured</artifactId>
    <scope>test</scope>
  </dependency>
</dependencies>

Für das Bauen der Anwendung werden zwei Profile bereitstellt. Mit einem Profil wird eine JAR-Datei gebaut, die in der Java-HotspotVM ausgeführt werden kann. Das zweite Profil erzeugt nativ ausführbare Dateien, die mithilfe der GraalVM gebaut werden.
Als nächstes werden die für die Anwendung benötigten Dependencys hinzugefügt. Dies kann auch über das Maven-Plug-in gemacht werden. Mithilfe des Befehls ./mvnw quarkus:list-extensions kann eine Liste der verfügbaren Extensions ausgegeben werden.
Liste von Extensions die für Quarkus verfügbar sind
Aus der Liste der verfügbaren Extensions benötigen wir folgende die Anwendung:

  • Flyway
  • Hibernate ORM with Panache
  • JDBC Driver – PostgreSQL
  • SmallRye OpenAPI
  • Swagger UI
  • RESTEasy – JSON-B

Für viele der Extensions stehen offizielle Anleitungen zur Verfügung, die zeigen, wie diese mit Quarkus eingesetzt werden können.
Um die Extensions zu installieren, wird folgender Befehl verwendet:

./mvnw quarkus:add-extension \
-Dextensions="quarkus-flyway, quarkus-hibernate-orm-panache, \
quarkus-jdbc-postgresql, quarkus-resteasy-jsonb, \
quarkus-smallrye-openapi, quarkus-swagger-ui"

Maven fügt die benötigten Dependencys für diese Extensions nun ins Projekts ein.

<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-resteasy-jsonb</artifactId>
</dependency>
<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-flyway</artifactId>
</dependency>
<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>
<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>
<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-swagger-ui</artifactId>
</dependency>

Datenbank einrichten

Die Daten der Anwendung sollen in einer ProstgreSQL-Datenbank gespeichert werden. Im ersten Schritt soll diese Datenbank an die Anwendung angebunden werden. Außerdem soll mithilfe von Flyway das Datenbankschema eingerichtet werden. Die Konfiguration der Datenbankverbindung geschieht mithilfe von Eclipse Microprofile und wird in der application.properties-Datei durchgeführt.

quarkus.datasource.url = 
    jdbc:postgresql://localhost:5432/quarkus-shoppinglist
quarkus.datasource.driver = org.postgresql.Driver
quarkus.datasource.username = postgres
quarkus.datasource.password = securePassword

Für die Anwendung muss auch eine PostgreSQL-Instanz mit den passenden Konfigurationen betrieben werden. Eine einfache Möglichkeit hierzu ist die Nutzung von Docker. Mit folgendem Befehl kann ein passender Container gestartet werden.

docker run -e POSTGRES_PASSWORD=securePassword  \
-e POSTGRES_USER=postgres  \
-e POSTGRES_DB=quarkus-shoppinglist  \
-d -p 5432:5432 postgres

Sobald die PostgreSQL-Instanz läuft und die Anwendung verbunden ist, kann das Datenbankschema angelegt werden. Hierzu wird Flyway verwendet. Quarkus bietet für Flyway eine Reihe von Konfigurationsmöglichkeiten. Der Parameter quarkus.flyway.migrate-at-start in den application.properties führt dazu, dass die SQL-Dateien von Flyway beim Start der Anwendung automatisch ausgeführt werden. Standardmäßig werden die Flyway-SQL-Skripte im Ordner src/main/resources/db/migration abgelegt und müssen mit dem Präfix V beginnen. Für das Datenbankschema wird eine Datei mit dem Namen V1.0__InitialTables.sql und folgendem Inhalt angelegt:

CREATE SEQUENCE hibernate_sequence START 1;

CREATE TABLE shoppinglist
(
  id   BIGINT,
  name VARCHAR(50)
);

CREATE TABLE shoppingitem
(
  id   BIGINT,
  amount FLOAT,
  incard BOOLEAN,
  name VARCHAR(50),
  shopping_list_id BIGINT
);

Beim Start der Anwendung wird jetzt geprüft, welche SQL-Skripte noch nicht ausgeführt wurden. Diese werden automatisch in der Reihenfolge ihrer Versionen eingespielt.

Domain Layer

Nachdem die Datenbank eingerichtet wurde, wird der Domain Layer der Anwendung definiert. Hierzu wird Hibernate in Verbindung mit Panache verwendet. Zur Abbildung der Datenbanktabellen werden zwei Entity-Klassen angelegt, die die Einkaufsliste und die einzelnen Artikel abbilden.

@Entity
public class ShoppingList extends PanacheEntity {
  String name;
  @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
  @JoinColumn(name = "shopping_list_id")
  List<ShoppingItem> items;
}

@Entity
public class ShoppingItem extends PanacheEntity {
  String name;
  long amount;
  boolean inCard;
}

Zwischen den zwei Klassen ist eine One-To-Many-Beziehung definiert, wobei die Referenz auf der Seite der Shopping Items gespeichert wird. Mit Panache kann auf zwei Arten mit den Entities gearbeitet werden. Die Klasse PanacheEntity, von der unsere Entities erben, bietet eine Reihe von Funktionen, mit denen Aktionen auf der Datenbank ausgeführt werden können. Alternativ kann auch ein Database Access Object angelegt werden, das für die Datenbankzugriffe verwendet wird.

Wir werden in diesem Beispiel zwei Repositorys als DAOs anlegen. Diese Repositorys können anschließend über Dependency Injection in unsere Services integriert und anschließend genutzt werden. Aufrufe der Repositorys können zum Beispiel folgende sein:

shoppingItemRepository.persist(shoppingItem);
shoppingItemRepository.findById(shoppingItemId)

Service Layer

Die Businesslogik wird in einem Service Layer abgebildet. Hierbei greifen die Services auf die Repositorys zu, um die Datenbankeinträge anzulegen und die Repositorys werden über Dependency Injection in die Services integriert.

private ShoppingItemRepository shoppingItemRepository;
private ShoppingListRepository shoppingListRepository;

@Inject
public ShoppingListService(ShoppingItemRepository shoppingItemRepository,
ShoppingListRepository shoppingListRepository) {
  this.shoppingItemRepository = shoppingItemRepository;
  this.shoppingListRepository = shoppingListRepository;
}

Für die einzelnen Aktionen, die der Nutzer durchführen kann, werden Methoden definiert. In diesem Beispiel werden Methoden zum Anlegen und Auslesen von Einkaufslisten und zum Anlegen und Bearbeiten der Items angelegt.

public ShoppingList getById(long id) {
  return shoppingListRepository.findById(id);
}

@Transactional
public ShoppingList createShoppingList(String name) {
  ShoppingList shoppingList = new ShoppingList();
  shoppingList.setName(name);
  shoppingListRepository.persist(shoppingList);
  return shoppingList;
}

@Transactional
public ShoppingList addItem(long shoppingListId, String name, float amount) {
  ShoppingItem shoppingItem = new ShoppingItem();
  shoppingItem.setName(name);
  shoppingItem.setAmount(amount);
  shoppingItemRepository.persist(shoppingItem);
  ShoppingList shoppingList = shoppingListRepository.findById(shoppingListId);
  shoppingList.getItems().add(shoppingItem);

return shoppingList;
}

@Transactional
public ShoppingItem putInCard(long itemId) {
  ShoppingItem shoppingItem = shoppingItemRepository.findById(itemId);
  shoppingItem.setInCard(true);
  return shoppingItem;
}

Controller Layer

Als letztes werden Controller definiert, sodass ein Nutzer über HTTP-Request auf die Anwendung zugreifen kann. Hierzu wird RESTEasy in Kombination mit RESTEasy-jsonb verwendet. Die Schnittstellen werden mit Data-Transfer-Objekten aufgerufen und nutzen die im Service bereitgestellten Funktionen. Die Services werden hierbei wieder über Dependency Injection eingebunden.

@Inject
ShoppingListService shoppingListService;

@POST
@Produces("application/json")
@Consumes("application/json")
public ShoppingList create(ShoppingListDTO shoppingList) {
  return shoppingListService.createShoppingList(shoppingList.getName());
}

@GET
@Path("/{listId}")
@Produces("application/json")
public ShoppingList get(@PathParam("listId") long listId) {
  return shoppingListService.getById(listId);
}

@POST
@Path("/{listId}/item")
@Produces("application/json")
@Consumes("application/json")
public ShoppingList addItem(@PathParam("listId") long listId, ShoppingItemDTO shoppingItem) {
  return shoppingListService.addItem(listId, shoppingItem.getName(), shoppingItem.getAmount());
}

@Inject
ShoppingItemService shoppingItemService;

@PUT
@Produces("application/json")
@Path("/{itemId}")
public ShoppingItem putInCard(@PathParam("itemId") long itemId) {
  return shoppingItemService.putInCard(itemId);
}

Durch die OpenApi- und Swagger-Extensions wird ohne zusätzliche Konfiguration eine Beschreibung der Schnittstellen unter http://localhost:8080/swagger-ui bereitgestellt.
Swagger Oberfläche, welche für das Projekt generiert wird

Deployment

Die Anwendung ist nun funktionsfähig. Der Nutzer kann über die Schnittstellen die Datenobjekte anlegen, bearbeiten und auslesen. Als letztes wird die Anwendung noch für die Verteilung vorbereitet. Hierzu wird ein nativer Build mit der GraalVM gestartet. Die gebaute Datei wird anschließend in einen Dockercontainer integriert.
Es gibt nun verschiedene Möglichkeiten, um ausführbare Dateien zu erzeugen. Um eine JAR-Datei zu erzeugen, kann einfach der Befehl ./mvnw package genutzt werden. Mit dem Befehl ./mvnw package -Pnative -Dnative-image.docker-build=true wird eine nativ ausführbare Datei aus dem Java-Code erzeugt. Diese ist für die Laufzeitumgebung des Geräts ausgelegt, auf dem sie gebaut wird. Man kann den Unterschied zwischen der Java-Version und der nativen Version schon sehr deutlich bei der Startzeit sehen. Die erste Abbildung zeigt die Ausgabe der Java-Version, die zum Starten ungefähr zwei Sekunden benötigt. Auf dem zweiten Bild kann man die native Version sehen, die nur ungefähr 0.05 Sekunden benötigt.
Ausgabe beim Starten der Java-Version
Ausgabe beim Starten der nativen Version
Für das Bauen einer Datei für die Ausführung in der Docker-Umgebung wird der Befehl ./mvnw package -Pnative -Dnative-image.docker-build=true genutzt. Dieser führt dazu, dass die Datei auf eine 64-Bit-Linux-Umgebung ausgelegt ist. Die erzeugte Datei kann jetzt in ein Docker Image eingebaut und anschließend mit Docker ausgeführt werden. Quarkus stellt hierzu ein Beispiel-Dockerfile zur Verfügung.

FROM registry.fedoraproject.org/fedora-minimal
WORKDIR /work/
COPY target/*-runner /work/application
RUN chmod 775 /work
EXPOSE 8080
CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]

Das Docker Image wird mit dem Befehl docker build -f src/main/docker/Dockerfile.native -t quarkus/shoppinglist . gebaut und kann anschließend über den Befehl docker run -i --rm --network="host" -p 8080:8080 quarkus/shoppinglist ausgeführt werden.
Wir haben nun eine native Anwendung, die im Docker-Container ausgeführt wird.

Fazit

Quarkus bietet mit seinen Extensions gute Möglichkeiten, um REST-Anwendungen zu schreiben. Es werden gängige Werkzeuge wie Flyway und Swagger unterstützt und man versucht, nicht das Rad neu zu erfinden. Quarkus setzt stark darauf, bestehende Standards zu nutzen und zu verbessern.
Außerdem liefert Quarkus durch die nativen Anwendungen, die am Ende erstellt werden können, einen großen Vorteil gegenüber herkömmlichem Java. Im Vergleich zu einer Spring-Anwendung mit gleicher Funktionalität sind die Startzeit und der Ressourcenbedarf wesentlich geringer.
Auch wenn Quarkus momentan noch in einer Beta-Phase ist und vom produktiven Einsatz abgeraten wird, macht das Arbeiten mit Quarkus bereits viel Spaß. Ich denke, dass es sich in Zukunft als eine gute Alternative zu bisher eingesetzten Frameworks entwickeln wird.

Beispielcode: Beispielprojekt

Weitere Informationen zu Quarkus: Quarkus

Enno Lohmann

Enno ist seit 2018 als Software Engineer bei der codecentric AG tätig. Sein Schwerpunkt liegt in der Fullstack Java Entwicklung mit Spring Boot und modernen Webframeworks. Außerdem interessiert er sich für Cloud- und Containertechnologien.

Kommentare

  • Ronny

    4. Juni 2019 von Ronny

    Frage zum Quellcode (nicht zum Framework): Warum wird im Controller bei dem get(listId) eine ShoppingList und keine ShoppingListDto zurückgegeben? Ist nicht das DTO die Schnittstelle für die Außenwelt?

    • Enno Lohmann

      4. Juni 2019 von Enno Lohmann

      Ja du hast recht, dass das DTO eigentlich die Schnittstelle zur Außenwelt ist. Um das ganze Sauber zu machen hätte ich wahrscheinlich 2 DTOs erstellen müssen. Eines zum anlegen neuer Objekte (ohne id) und eines welches von den Schnittstellen zurück gegeben wird (mit id). Die Id wird hierbei benötigt, damit man von außen die Elemente wieder referenzieren kann. In dem Beispielcode habe ich direkt die Entity heraus gegeben, da das DTO identisch gewesen wäre.

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.