Welcome
Let’s start from the beginning... Quarkus. What’s Quarkus? That’s a pretty good question, and probably a good start. If you go on the Quarkus web site, Quarkus is "A Kubernetes Native Java stack tailored for OpenJDK HotSpot & GraalVM, crafted from the best of breed Java libraries and standards". This description is rather unclear, but does a very good job at using bankable keywords, right? It’s also written: "Supersonic Subatomic Java". Still very foggy. In practice, Quarkus is an Open Source stack to write Java applications, specifically backend applications. In this lab, we are going to explain what Quarkus is, and, because the best way to understand Quarkus is to use it, build a set of microservices with it. Don’t be mistaken, Quarkus is not limited to microservices, and we are going to learn about this in the workshop.
This lab offers attendees an intro-level, hands-on session with Quarkus, from the first line of code to making services, to consuming them, and finally to assembling everything in a consistent system. But, what are we going to build? Well, it’s going to be a set of microservices using Quarkus.
What you are going to learn:
-
What is Quarkus and how you can use it
-
How to build an HTTP endpoint (REST API) with Quarkus
-
How to access a database
-
How you can use Swagger and OpenAPI
-
How you test your microservice
-
How you improve the resilience of your service
-
And many more…
Ready? Here we go!
Presenting the Workshop
What Is This Workshop About?
This workshop should give you a practical introduction to Quarkus. You will first install all the needed tools to then develop an entire microservice architecture, mixing classical HTTP microservices.
The idea is that you leave this workshop with a first experience using Quarkus, what it is not, and how it can help you in your projects. Then, you’ll be prepared to investigate a bit more and, hopefully, contribute.
What Will You Be Developing?
In this workshop you will develop an application that allows super-heroes to fight against villains. Being a workshop about microservices, you will be developing several microservices communicating either synchronously via REST :
-
Super Hero UI: an Angular application allowing you to pick up a random super-hero, a random villain and makes them fight. The Super Hero UI is exposed via Quarkus and invokes the Fight REST API
-
Hero REST API: Allows CRUD operations on Heroes which are stored in a Postgres database
-
Villain REST API: Allows CRUD operations on Villains which are stored in a Postgres database
-
Fight REST API: This REST API invokes the Hero and Villain APIs to get a random super-hero and a random villain. Each fight is stored in a Postgres database
The main UI allows you to pick up one random Hero and Villain by clicking on "New Fighters". Then it’s just a matter of clicking on "Fight!" to get them to fight. The table at the bottom shows the list of the previous fights.
How Does This Workshop Work?
You have this material in your hands (either electronically or printed) and you can now follow it step by step. The structure of this workshop is as follow :
-
Installing all the needed tools: in this section you will install all the tools and code to be able to develop, compile and execute our application
-
Developing with Quarkus: in this section you will develop a microservice architecture by creating several Maven projects, write some Java code, add JPA entities, JAX-RS REST endpoints, write some tests, use an Angular web application, and all that on Quarkus
If you already have the tools installed, skip the Installing all the needed tools section and jump to the sections Developing with Quarkus, and start hacking some code and addons. This "à la carte" mode allows you to make the most of this 6 hours long hands on lab.
What Do You Have to Do?
This workshop should be as self explanatory as possible. So your job is to follow the instructions by yourself, do what you are supposed to do, and do not hesitate to ask for any clarification or assistance, that’s why the team is here. Oh, and be ready to have some fun!
Software Requirements
First of all, make sure you have Web browser installed on your laptop and internet connectivity. You will also need a GitHub account.
Your environment is remote and can be accessed via CodeReady Workspaces (CRW) through your local browser, you just need to sign up and configure some elements. Your environment includes also Red Hat’s OpenShift Container Platform (OCP).
The next section focuses on how to install and setup the needed software.
Installing Software
CodeReady Workspace
CodeReady Workspaces is a collaborative Kubernetes-native development solution that delivers OpenShift workspaces and in-browser IDE for rapid cloud application development. You are going to create your own environment.
CRW creation
-
Go to the Etherpad site and choose an user. This user will be used to access the CRW and the OpenShift Web Console and for naming some components that you are going to create during the workshop.
-
Launch the CRW creation by clicking the link mentioned in the Etherpad site.
-
Once the CRW creation done, access to your CRW and sign up with your own user (selected previously in Etherpad) and full fill the form:
user: USERNAME pwd: openshift email: USERNAME@ocp.com first name: Yago last name: Sanchez
If everything goes well, you should have a CodeReady Workspace with a quarkus-workshop
folder ready to start to code:
-
Finally, open a terminal from the Terminal menu → Open Terminal in specific container → maven.
Command Line Utilities
Just make sure the following commands work on your CRW terminal
$ java -version
$ mvn -version
$ curl --version
OpenShift Container Platform
Your lab environment includes Red Hat’s OpenShift Container Platform (OCP).
Access to your OCP resources can be gained via both the oc
CLI utility and the OCP web console.
OpenShift Namespaces
The project we are going to develop will contain 3 microservices accessing to a PostgreSQL database. We will make the databases to run in a OpenShift dedicated namespace.
-
In the terminal of your CRW, authenticate into OpenShift as a non cluster admin user (USERNAME) using the
oc
utility.
You can get the command for authenticating from the OpenShift Web Console. |
$ oc login
There are 2 namespaces (OpenShift projects) in your OpenShift cluster:
The namespace for hosting your CRW environment is USERNAME-codeready where USERNAME
correspond to your specific username.
The namespace for hosting databases and microservices is USERNAME-heroes.
change the USERNAME with your own. |
Operators
Your lab environment comes pre-installed with an PostgreSQL operator.
PostgreSQL operator
The PostgreSQL operator allows you to package, install, configure and manage a PostgreSQL database within an OpenShift cluster.
Congratulations! Your lab environment is now ready to use.
Preparing for the Workshop
Workshop scaffolding
Call to action
The workshop folder containing the code is already in your CRW, at the root of the filesystem.
In this workshop you will be developing an application dealing with Super Heroes (and Super Villains 🦹). The code will be in the super-heroes directory:
Super Heroes Application
Under the super-heroes
directory you will find the entire Super Hero application spread throughout a set of subdirectories, each one containing a microservice or some tooling.
The final structure will be the following:
Most of theses subdirectories are Maven projects and follow the Maven directory structure:
For next steps, you need to be authenticated on OpenShift with your own user
$ oc login ....
$ oc project USERNAME-heroes
change USERNAME with your own. |
Databases
Any microservice system is going to rely on a set of technical services.
In our context, we are going to use PostgreSQL as the database.
The infrastructure folder contains the OpenShift manifests to create the OpenShift resources in order to have this technical service available.
We need to create a database for each microservice: heroes, villains and fights.
The OpenShift manifests describing them can be found under $CHE_PROJECTS_ROOT/quarkus-workshop-super-heroes/super-heroes/infrastructure/
directory.
We can deploy the DBs easily using oc
utility and the manifests from the CRW terminal.
Make sure that you are authenticated with your USERNAME and you are using the USERNAME-heroes
namespace, then apply
the databases yaml file:
oc apply -f $CHE_PROJECTS_ROOT/quarkus-workshop-super-heroes/super-heroes/infrastructure/databases.yaml
You can verify the availability of databases with the following command:
curl http://heroes-database.USERNAME-heroes:5432
You should have the following response:
curl: (52) Empty reply from server
Warming up Maven
Now that you have the initial structure in place, we just need configure the correct url for the database.
On your CRW, edit the $CHE_PROJECTS_ROOT/quarkus-workshop-super-heroes/super-heroes/rest-villain/src/main/resources/application.properties
file and change database configuration. You need to change DB’s URL:
quarkus.datasource.jdbc.url=jdbc:postgresql://villains-database.USERNAME-heroes:5432/villains_database
Change USERNAME with your own. |
Now, in the Terminal, you can navigate to the root directory quarkus-workshop-super-heroes/
and run:
Call to action
mvn clean install
By running this command, it downloads all the required dependencies.
Ready?
Prerequisites has been installed, the different components have been warmed up, it’s now time to write some code!
Creating a REST/HTTP Microservice
At the heart of the Super Hero application comes Heroes. We need to expose a REST API allowing CRUD operations on Super Heroes. This microservice is, let’s say, a classical microservice. It uses HTTP to expose a REST API and internally store data into a database. This service will be used by the fight microservice.
In the following sections, you learn:
-
how to create a new Quarkus application
-
how to implement REST API using JAX-RS
-
how to compose your application using CDI beans
-
how to access your database using Hibernate with Panache
-
how to use transactions
-
how to enable OpenAPI and Swagger-UI
But first, let’s describe our service. The Super Heroes microservice stores super-heroes, with their names, powers, and so on. The REST API allows adding, removing, listing, and picking a random hero from the stored set. Nothing outstanding but a good first step to discover Quarkus.
Hero Microservice
First thing first, we need a project. That’s what your are going to see in this section.
Bootstrapping the Hero REST Endpoint
The easiest way to create a new Quarkus project is to use the Quarkus Maven plugin.
We have created the project structure earlier, so we will move to the super-heroes
directory and run the project creation command.
Open a terminal and run the following command:
Call to action
cd $CHE_PROJECTS_ROOT/quarkus-workshop-super-heroes/super-heroes
mvn io.quarkus:quarkus-maven-plugin:2.2.2.Final:create \
-DprojectGroupId=io.quarkus.workshop.super-heroes \
-DprojectArtifactId=rest-hero \
-DclassName="io.quarkus.workshop.superheroes.hero.HeroResource" \
-Dpath="api/heroes"
Directory Structure
Once you bootstrap the project, you get the following directory structure with a few Java classes and other artifacts :
The Maven archetype generates the following rest-hero
sub-directory:
-
the Maven structure with a
pom.xml
-
an
io.quarkus.workshop.superheroes.hero.HeroResource
resource exposed on/api/heroes
-
an associated unit test
HeroResourceTest
-
the landing page
index.html
that is accessible after starting the application -
example
Dockerfile
files for both native and jvm modes insrc/main/docker
-
the
application.properties
configuration file
Once generated, look at the pom.xml
.
You will find the import of the Quarkus BOM, allowing you to omit the version on the different Quarkus dependencies.
In addition, you can see the quarkus-maven-plugin
responsible of the packaging of the application and also providing the development mode.
<properties>
<compiler-plugin.version>3.8.1</compiler-plugin.version>
<maven.compiler.parameters>true</maven.compiler.parameters>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
<quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
<quarkus.platform.version>2.2.2.Final</quarkus.platform.version>
<surefire-plugin.version>3.0.0-M5</surefire-plugin.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>${quarkus.platform.group-id}</groupId>
<artifactId>${quarkus.platform.artifact-id}</artifactId>
<version>${quarkus.platform.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>${quarkus.platform.group-id}</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<version>${quarkus.platform.version}</version>
<extensions>true</extensions>
<executions>
<execution>
<goals>
<goal>build</goal>
<goal>generate-code</goal>
<goal>generate-code-tests</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>${compiler-plugin.version}</version>
<configuration>
<parameters>${maven.compiler.parameters}</parameters>
</configuration>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire-plugin.version}</version>
<configuration>
<systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
If we focus on the dependencies section, you can see the extension allowing the development of REST applications:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy</artifactId>
</dependency>
RESTEasy
You may not be familiar with RESTEasy.[1] It’s an implementation of JAX-RS and it uses to implement RestFul services in Quarkus. |
What’s this Quarkus bom thingy?
The Quarkus Universe includes Quarkus as well as third-party extensions, like Apache Camel. |
The JAX-RS Resource
During the project creation, the HeroResource.java
file has been created with the following content:
package io.quarkus.workshop.superheroes.hero;
@Path("/api/heroes")
public class HeroResource {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "hello";
}
}
It’s a very simple REST endpoint, returning "hello" to requests on /api/heroes
.
Running the Application
Now we are ready to run our application.
Call to action
Use: ./mvnw quarkus:dev
:
IMPORTANT copy the url mentioned in the CodeReady Workspace pop-up. The url should contain the port 8080 as we can see in the previous log. This url corresponds to the rest-hero microservice. |
$ ./mvnw quarkus:dev
[INFO] Scanning for projects...
[INFO]
[INFO] -------------< io.quarkus.workshop.super-heroes:rest-hero >-------------
[INFO] Building rest-hero 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- quarkus-maven-plugin:2.2.2.Final:dev (default-cli) @ rest-hero ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 2 resources
[INFO] Nothing to compile - all classes are up to date
Listening for transport dt_socket at address: 5005
__ ____ __ _____ ___ __ ____ ______
--/ __ \/ / / / _ | / _ \/ //_/ / / / __/
-/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2020-11-16 10:01:51,331 INFO [io.quarkus] (Quarkus Main Thread) rest-hero 1.0-SNAPSHOT on JVM (powered by Quarkus 2.2.2.Final) started in 3.797s. Listening on: http://0.0.0.0:8080
2020-11-16 10:01:51,343 INFO [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated.
2020-11-16 10:01:51,343 INFO [io.quarkus] (Quarkus Main Thread) Installed features: [cdi, resteasy]
Then check that the endpoint returns hello
as expected:
[source,shell]Now we are ready to run our application
$ curl $URL/api/heroes
hello RESTEasy
Alternatively, you can open $URL/api/heroes in your browser.
Change the url by the one you should have got from CRW |
Development Mode
quarkus:dev
runs Quarkus in development mode.
This enables hot deployment with background compilation, which means that when you modify your Java files and/or your resource files and invoke a REST endpoint (i.e. cUrl command or refresh your browser), these changes will automatically take effect.
This works too for resource files like the configuration property and HTML files.
Refreshing the browser triggers a scan of the workspace, and if any changes are detected, the Java files are recompiled and the application is redeployed;
your request is then serviced by the redeployed application.
If there are any issues with compilation or deployment an error page will let you know.
The development mode also allows debugging and listens for a debugger on port 5005.
If you want to wait for the debugger to attach before running you can pass -Dsuspend=true
on the command line.
If you don’t want the debugger at all you can use -Ddebug=false
.
Alright, time to change some code.
Open your favorite IDE and import the project.
To check that the hot reload is working, update the method HeroResource.hello()
by returning the String "hello hero".
Now, execute the cUrl command again, the output has changed without you to having to stop and restart Quarkus:
Call to action
$ curl https://$(oc get route -n USERNAME-codeready | grep 8080 | awk '{ print $2 }')/api/heroes
hello hero
Testing the Application
All right, so far so good, but wouldn’t it be better with a few tests, just in case.
In the generated pom.xml
file, you can see 2 test dependencies:
<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>
Quarkus supports Junit 4 and Junit 5 tests. In the generated project, we use Junit 5. Because of this, the version of the Surefire Maven Plugin must be set, as the default version does not support Junit 5:
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire-plugin.version}</version>
<configuration>
<systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
</configuration>
</plugin>
We also set the java.util.logging
system property to make sure tests will use the correct log manager.
The generated project contains a simple test in HeroResourceTest.java
.
package io.quarkus.workshop.superheroes.hero;
@QuarkusTest
public class HeroResourceTest {
@Test
public void testHelloEndpoint() {
given()
.when().get("/api/heroes")
.then()
.statusCode(200)
.body(is("hello"));
}
}
By using the QuarkusTest
runner, the HeroResourceTest
class instructs JUnit to start the application before the tests.
Then, the testHelloEndpoint
method checks the HTTP response status code and content.
Notice that these tests use RestAssured, but feel free to use your favorite library.[2]
Call to action
Execute it with ./mvnw test
or from your IDE.
It fails! It’s expected, you changed the output of HeroResource.hello()
earlier.
Adjust the test body condition accordingly.
Packaging and Running the Application
Call to action
The application is packaged using ./mvnw package
.
It produces the quarkus-run.jar
file in the target/quarkus-app/
directory.
Be aware that it’s not an über-jar as the dependencies are copied into the target/quarkus-app/lib/
directory.
You can run the application using: java -jar target/quarkus-app/quarkus-run.jar
.
Before running the application, don’t forget to stop the hot reload mode (hit CTRL+C), or you will have a port conflict. |
Troubleshooting
You might come across the following error while developing:
WARN [io.qu.ne.ru.NettyRecorder] (Thread-48) Localhost lookup took more than one second, you need to add a /etc/hosts entry to improve Quarkus startup time. See https://thoeni.io/post/macos-sierra-java/ for details.
If this is the case, it’s just a matter to add the node name of your machine to the /etc/hosts. For that, first get the name of your node with the following command:
$ uname -n
my-node.local
Then sudo vi /etc/hosts
so you have the rights to edit the file and add the following entry
127.0.0.1 localhost my-node.local
Transactions and ORM
The Hero API’s role is to allow CRUD operations on Super Heroes. In this module we will create a Hero entity and persist/update/delete/retrieve it from a Postgres database in a transactional way.
Directory Structure
In this module we will add extra classes to the Hero API project. You will end-up with the following directory structure:
Installing the PostgreSQL Dependency, Hibernate with Panache and Hibernate Validator
This microservice:
-
interacts with a PostGreSQL database - so it needs a driver
-
uses Hibernate with Panache - so need the dependency on it
-
validates payloads and entities - so need a validator
-
consumes and produces JSON - so we need a mapper
Hibernate ORM is the de-facto JPA implementation and offers you the full breadth of an Object Relational Mapper. It makes complex mappings possible, but it does not make simple and common mappings trivial. Hibernate ORM with Panache focuses on making your entities trivial and fun to write in Quarkus.[3]
Because JPA and Bean Validation work well together, we will use Bean Validation to constrain our business model.
To add the required dependencies, just run the following command:
Call to action
$ ./mvnw quarkus:add-extension -Dextensions="jdbc-postgresql,hibernate-orm-panache,hibernate-validator,resteasy-jsonb"
This will add the following dependencies in the pom.xml
file:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-validator</artifactId>
</dependency>
From now on, you can choose to either edit your pom directly or use the quarkus:add-extension
command.
Hero Entity
Call to action
To define a Panache entity, simply extend PanacheEntity
, annotate it with @Entity
and add your columns as public fields (no need to have getters and setters).
The Hero
entity should look like this:
package io.quarkus.workshop.superheroes.hero;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.Random;
@Entity
public class Hero extends PanacheEntity {
@NotNull
@Size(min = 3, max = 50)
public String name;
public String otherName;
@NotNull
@Min(1)
public int level;
public String picture;
@Column(columnDefinition = "TEXT")
public String powers;
@Override
public String toString() {
return "Hero{" +
"id=" + id +
", name='" + name + '\'' +
", otherName='" + otherName + '\'' +
", level=" + level +
", picture='" + picture + '\'' +
", powers='" + powers + '\'' +
'}';
}
}
Notice that you can put all your JPA column annotations and Bean Validation constraint annotations on the public fields.
Adding Operations
Thanks to Panache, once you have written the Hero
entity, here are the most common operations you will be able to do:
// creating a hero
Hero hero = new Hero();
hero.name = "Superman";
hero.level = 9;
// persist it
hero.persist();
// getting a list of all Hero entities
List<Hero> heroes = Hero.listAll();
// finding a specific hero by ID
hero = Hero.findById(id);
// counting all heroes
long countAll = Hero.count();
But we are missing a business method:
we need to return a random hero.
For that it’s just a matter to add the following method to our Hero.java
entity:
public static Hero findRandom() {
long countHeroes = Hero.count();
Random random = new Random();
int randomHero = random.nextInt((int) countHeroes);
return Hero.findAll().page(randomHero, 1).firstResult();
}
You would need to add the following import statement if not done automatically by your IDE |
Configuring Hibernate
Quarkus development mode is really useful for applications that mix front end or services and database access.
We use quarkus.hibernate-orm.database.generation=drop-and-create
in conjunction with import.sql
so every change to your app and in particular to your entities, the database schema will be properly recreated and your data (stored in import.sql
) will be used to repopulate it from scratch.
This is best to perfectly control your environment and works magic with Quarkus live reload mode:
your entity changes or any change to your import.sql
is immediately picked up and the schema updated without restarting the application!
Call to action
For that, make sure to have the following configuration in your application.properties
(located in src/main/resources
):
quarkus.hibernate-orm.database.generation=drop-and-create
quarkus.hibernate-orm.log.sql=true
HeroService Transactional Service
To manipulate the Hero
entity we will develop a transactional HeroService
class.
The idea is to wrap methods modifying the database (e.g. entity.persist()
) within a transaction.
Marking a CDI bean method @Transactional
will do that for you and make that method a transaction boundary.
@Transactional
can be used to control transaction boundaries on any CDI bean at the method level or at the class level to ensure every method is transactional.
You can control whether and how the transaction is started with parameters on @Transactional
:
-
@Transactional(REQUIRED)
(default): starts a transaction if none was started, stays with the existing one otherwise. -
@Transactional(REQUIRES_NEW)
: starts a transaction if none was started ; if an existing one was started, suspends it and starts a new one for the boundary of that method. -
@Transactional(MANDATORY)
: fails if no transaction was started ; works within the existing transaction otherwise. -
@Transactional(SUPPORTS)
: if a transaction was started, joins it ; otherwise works with no transaction. -
@Transactional(NOT_SUPPORTED)
: if a transaction was started, suspends it and works with no transaction for the boundary of the method ; otherwise works with no transaction. -
@Transactional(NEVER)
: if a transaction was started, raises an exception ; otherwise works with no transaction.
Call to action
Creates a new HeroService.java
file in the same package with the following content:
package io.quarkus.workshop.superheroes.hero;
import javax.enterprise.context.ApplicationScoped;
import javax.transaction.Transactional;
import javax.validation.Valid;
import java.util.List;
import static javax.transaction.Transactional.TxType.REQUIRED;
import static javax.transaction.Transactional.TxType.SUPPORTS;
@ApplicationScoped
@Transactional(REQUIRED)
public class HeroService {
@Transactional(SUPPORTS)
public List<Hero> findAllHeroes() {
return Hero.listAll();
}
@Transactional(SUPPORTS)
public Hero findHeroById(Long id) {
return Hero.findById(id);
}
@Transactional(SUPPORTS)
public Hero findRandomHero() {
Hero randomHero = null;
while (randomHero == null) {
randomHero = Hero.findRandom();
}
return randomHero;
}
public Hero persistHero(@Valid Hero hero) {
Hero.persist(hero);
return hero;
}
public Hero updateHero(@Valid Hero hero) {
Hero entity = Hero.findById(hero.id);
entity.name = hero.name;
entity.otherName = hero.otherName;
entity.level = hero.level;
entity.picture = hero.picture;
entity.powers = hero.powers;
return entity;
}
public void deleteHero(Long id) {
Hero hero = Hero.findById(id);
hero.delete();
}
}
Notice that both methods that persist and update a hero, pass a Hero
object as a parameter.
Thanks to the Bean Validation’s @Valid
annotation, the Hero
object will be checked to see if it’s valid or not.
It it’s not, the transaction will be rollback-ed.
Configuring the Datasource
Our project now requires a connection to a PostgreSQL database. The main way of obtaining connections to a database is to use a datasource. In Quarkus, the out of the box datasource and connection pooling implementation is Agroal.[4]
This is done in the src/main/resources/application.properties
file.
Call to action
Just add the following datasource configuration:
quarkus.datasource.jdbc.url=jdbc:postgresql://heroes-database.USERNAME-heroes:5432/heroes_database
quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=superman
quarkus.datasource.password=superman
quarkus.datasource.max-size=8
quarkus.datasource.min-size=2
HeroResource Endpoint
The HeroResource
Endpoint was bootstrapped with only one method hello()
.
We need to add extra methods that will allow CRUD operations on heroes.
Call to action
Here are the new methods to add to the HeroResource
class:
package io.quarkus.workshop.superheroes.hero;
import org.jboss.logging.Logger;
import javax.inject.Inject;
import javax.validation.Valid;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
import java.net.URI;
import java.util.List;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static javax.ws.rs.core.MediaType.TEXT_PLAIN;
@Path("/api/heroes")
@Produces(APPLICATION_JSON)
public class HeroResource {
private static final Logger LOGGER = Logger.getLogger(HeroResource.class);
@Inject
HeroService service;
@GET
@Path("/random")
public Response getRandomHero() {
Hero hero = service.findRandomHero();
LOGGER.debug("Found random hero " + hero);
return Response.ok(hero).build();
}
@GET
public Response getAllHeroes() {
List<Hero> heroes = service.findAllHeroes();
LOGGER.debug("Total number of heroes " + heroes);
return Response.ok(heroes).build();
}
@GET
@Path("/{id}")
public Response getHero(
@PathParam("id") Long id) {
Hero hero = service.findHeroById(id);
if (hero != null) {
LOGGER.debug("Found hero " + hero);
return Response.ok(hero).build();
} else {
LOGGER.debug("No hero found with id " + id);
return Response.noContent().build();
}
}
@POST
public Response createHero(
@Valid Hero hero, @Context UriInfo uriInfo) {
hero = service.persistHero(hero);
UriBuilder builder = uriInfo.getAbsolutePathBuilder().path(Long.toString(hero.id));
LOGGER.debug("New hero created with URI " + builder.build().toString());
return Response.created(builder.build()).build();
}
@PUT
public Response updateHero(
@Valid Hero hero) {
hero = service.updateHero(hero);
LOGGER.debug("Hero updated with new valued " + hero);
return Response.ok(hero).build();
}
@DELETE
@Path("/{id}")
public Response deleteHero(
@PathParam("id") Long id) {
service.deleteHero(id);
LOGGER.debug("Hero deleted with " + id);
return Response.noContent().build();
}
@GET
@Produces(MediaType.TEXT_PLAIN)
@Path("/hello")
public String hello() {
return "hello RESTEasy";
}
}
Dependency Injection
Dependency injection in Quarkus is based on ArC which is a CDI-based dependency injection solution tailored for Quarkus' architecture.[5] You can learn more about it in the Contexts and Dependency Injection guide.[6]
ArC comes as a dependency of quarkus-resteasy
so you already have it handy.
That’s why you were able to use @Inject
in the HeroResource
to inject a reference to HeroService
.
Adding Data
Call to action
To load some SQL statements when Hibernate ORM starts, add the following import.sql
in the root of the resources
directory.
It contains SQL statements terminated by a semicolon.
This is useful to have a data set ready for the tests or demos.
INSERT INTO hero(id, name, otherName, picture, powers, level)
VALUES (nextval('hibernate_sequence'), 'Chewbacca', '', 'https://www.superherodb.com/pictures2/portraits/10/050/10466.jpg', 'Agility, Longevity, Marksmanship, Natural Weapons, Stealth, Super Strength, Weapons Master', 5);
INSERT INTO hero(id, name, otherName, picture, powers, level)
VALUES (nextval('hibernate_sequence'), 'Angel Salvadore', 'Angel Salvadore Bohusk', 'https://www.superherodb.com/pictures2/portraits/10/050/1406.jpg', 'Animal Attributes, Animal Oriented Powers, Flight, Regeneration, Toxin and Disease Control', 4);
INSERT INTO hero(id, name, otherName, picture, powers, level)
VALUES (nextval('hibernate_sequence'), 'Bill Harken', '', 'https://www.superherodb.com/pictures2/portraits/10/050/1527.jpg', 'Super Speed, Super Strength, Toxin and Disease Resistance', 6);
Ok, but that’s just a few entries.
Download the SQL file import.sql and copy it under src/main/resources
.
Now, you have around 500 heroes that will be loaded in the database.
If you didn’t yet, start the application in dev mode:
$./mvnw quarkus:dev
Consider copying the url mentioned by CRW |
Then, open your browser to $URL/api/heroes. You should see lots of heroes…
CRUD Tests in HeroResourceTest
To test the HeroResource
endpoint, we will be using a QuarkusTestResource
that will fire a Postgres database and then test CRUD operations.
The QuarkusTestResource
is a test extension that can configure the environment before running the application but in our context, because of CRW, we will be using the database configured and running on OpenShift.
Call to action
We need to install in our pom.xml
an extra dependency for data-binding functionality:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<scope>test</scope>
</dependency>
Call to action
Then, in io.quarkus.workshop.superheroes.hero.HeroResourceTest
, you will add the following test methods to the HeroResourceTest
class:
-
shouldNotGetUnknownHero
: giving a random Hero identifier, theHeroResource
endpoint should return a 204 (No content) -
shouldGetRandomHero
: checks that theHeroResource
endpoint returns a random hero -
shouldNotAddInvalidItem
: passing an invalidHero
should fail when creating it (thanks to the@Valid
annotation) -
shouldGetInitialItems
: checks that theHeroResource
endpoint returns the list of heroes -
shouldAddAnItem
: checks that theHeroResource
endpoint creates a validHero
-
shouldUpdateAnItem
: checks that theHeroResource
endpoint updates a newly createdHero
-
shouldRemoveAnItem
: checks that theHeroResource
endpoint deletes a hero from the database
The code is as follow:
package io.quarkus.workshop.superheroes.hero;
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.common.mapper.TypeRef;
import org.hamcrest.core.Is;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import java.util.List;
import java.util.Random;
import static io.restassured.RestAssured.get;
import static io.restassured.RestAssured.given;
import static javax.ws.rs.core.HttpHeaders.ACCEPT;
import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static javax.ws.rs.core.Response.Status.*;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.jupiter.api.Assertions.*;
@QuarkusTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class HeroResourceTest {
private static final String DEFAULT_NAME = "Super Baguette";
private static final String UPDATED_NAME = "Super Baguette (updated)";
private static final String DEFAULT_OTHER_NAME = "Super Baguette Tradition";
private static final String UPDATED_OTHER_NAME = "Super Baguette Tradition (updated)";
private static final String DEFAULT_PICTURE = "super_baguette.png";
private static final String UPDATED_PICTURE = "super_baguette_updated.png";
private static final String DEFAULT_POWERS = "eats baguette really quickly";
private static final String UPDATED_POWERS = "eats baguette really quickly (updated)";
private static final int DEFAULT_LEVEL = 42;
private static final int UPDATED_LEVEL = 43;
private static final int NB_HEROES = 951;
private static String heroId;
@Test
public void testHelloEndpoint() {
given()
.when().get("/api/heroes/hello")
.then()
.statusCode(200)
.body(is("hello RESTEasy"));
}
@Test
void shouldNotGetUnknownHero() {
Long randomId = new Random().nextLong();
given()
.pathParam("id", randomId)
.when().get("/api/heroes/{id}")
.then()
.statusCode(NO_CONTENT.getStatusCode());
}
@Test
void shouldGetRandomHero() {
given()
.when().get("/api/heroes/random")
.then()
.statusCode(OK.getStatusCode())
.header(CONTENT_TYPE, APPLICATION_JSON);
}
@Test
void shouldNotAddInvalidItem() {
Hero hero = new Hero();
hero.name = null;
hero.otherName = DEFAULT_OTHER_NAME;
hero.picture = DEFAULT_PICTURE;
hero.powers = DEFAULT_POWERS;
hero.level = 0;
given()
.body(hero)
.header(CONTENT_TYPE, APPLICATION_JSON)
.header(ACCEPT, APPLICATION_JSON)
.when()
.post("/api/heroes")
.then()
.statusCode(BAD_REQUEST.getStatusCode());
}
@Test
@Order(1)
void shouldGetInitialItems() {
List<Hero> heroes = get("/api/heroes").then()
.statusCode(OK.getStatusCode())
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
.extract().body().as(getHeroTypeRef());
assertEquals(NB_HEROES, heroes.size());
}
@Test
@Order(2)
void shouldAddAnItem() {
Hero hero = new Hero();
hero.name = DEFAULT_NAME;
hero.otherName = DEFAULT_OTHER_NAME;
hero.picture = DEFAULT_PICTURE;
hero.powers = DEFAULT_POWERS;
hero.level = DEFAULT_LEVEL;
String location = given()
.body(hero)
.header(CONTENT_TYPE, APPLICATION_JSON)
.header(ACCEPT, APPLICATION_JSON)
.when()
.post("/api/heroes")
.then()
.statusCode(CREATED.getStatusCode())
.extract().header("Location");
assertTrue(location.contains("/api/heroes"));
// Stores the id
String[] segments = location.split("/");
heroId = segments[segments.length - 1];
assertNotNull(heroId);
given()
.pathParam("id", heroId)
.when().get("/api/heroes/{id}")
.then()
.statusCode(OK.getStatusCode())
.header(CONTENT_TYPE, APPLICATION_JSON)
.body("name", Is.is(DEFAULT_NAME))
.body("otherName", Is.is(DEFAULT_OTHER_NAME))
.body("level", Is.is(DEFAULT_LEVEL))
.body("picture", Is.is(DEFAULT_PICTURE))
.body("powers", Is.is(DEFAULT_POWERS));
List<Hero> heroes = get("/api/heroes").then()
.statusCode(OK.getStatusCode())
.header(CONTENT_TYPE, APPLICATION_JSON)
.extract().body().as(getHeroTypeRef());
assertEquals(NB_HEROES + 1, heroes.size());
}
@Test
@Order(3)
void shouldUpdateAnItem() {
Hero hero = new Hero();
hero.id = Long.valueOf(heroId);
hero.name = UPDATED_NAME;
hero.otherName = UPDATED_OTHER_NAME;
hero.picture = UPDATED_PICTURE;
hero.powers = UPDATED_POWERS;
hero.level = UPDATED_LEVEL;
given()
.body(hero)
.header(CONTENT_TYPE, APPLICATION_JSON)
.header(ACCEPT, APPLICATION_JSON)
.when()
.put("/api/heroes")
.then()
.statusCode(OK.getStatusCode())
.header(CONTENT_TYPE, APPLICATION_JSON)
.body("name", Is.is(UPDATED_NAME))
.body("otherName", Is.is(UPDATED_OTHER_NAME))
.body("level", Is.is(UPDATED_LEVEL))
.body("picture", Is.is(UPDATED_PICTURE))
.body("powers", Is.is(UPDATED_POWERS));
List<Hero> heroes = get("/api/heroes").then()
.statusCode(OK.getStatusCode())
.header(CONTENT_TYPE, APPLICATION_JSON)
.extract().body().as(getHeroTypeRef());
assertEquals(NB_HEROES + 1, heroes.size());
}
@Test
@Order(4)
void shouldRemoveAnItem() {
given()
.pathParam("id", heroId)
.when().delete("/api/heroes/{id}")
.then()
.statusCode(NO_CONTENT.getStatusCode());
List<Hero> heroes = get("/api/heroes").then()
.statusCode(OK.getStatusCode())
.header(CONTENT_TYPE, APPLICATION_JSON)
.extract().body().as(getHeroTypeRef());
assertEquals(NB_HEROES, heroes.size());
}
private TypeRef<List<Hero>> getHeroTypeRef() {
return new TypeRef<List<Hero>>() {
// Kept empty on purpose
};
}
}
Let’s have a look to the 2 annotations used on the HeroResourceTest
class.
@QuarkusTest
indicates that this test class is checking the behavior of a Quarkus application.
The test framework starts the application before the test class and stops it once all the tests have been executed.
The tests and the application runs in the same JVM, meaning that the test can be injected with application beans.
This feature is very useful to test specific parts of the application.
However in our case, we just execute HTTP requests to check the result.
Call to action
With this code written, execute the test using ./mvnw test
.
The test should pass.
Configuring the Hero Microservice
Hardcoded values in our code are a no go (even if we all did it at some point ;-)). In this guide, we learn how to configure our Hero API as well as some parts of Quarkus.
Configuring Logging
Run time configuration of logging is done through the normal application.properties
file.
quarkus.log.console.enable=true
quarkus.log.console.format=%d{HH:mm:ss} %-5p [%c{2.}] (%t) %s%e%n
quarkus.log.console.level=DEBUG
quarkus.log.console.color=true
Configuring Quarkus Listening Port
Because we will end-up running several microservices, let’s configure Quarkus so it listens to a different port than 8080:
This is quite easy as we just need to add one property in the application.properties
file:
Call to action
quarkus.http.port=8083
quarkus.http.host=0.0.0.0
As we are running our microservice inside a maven container in the Code Ready Workspace environement, we need to make it listening in the 0.0.0.0
IP. For more details check https://quarkus.io/guides/all-config#quarkus-vertx-http_quarkus.http.host
Changing the port is one of the rare configuration that cannot be done while the application is running.
Call to action
You would need to restart the application to change the port.
Hit CTRL+C
to stop the application and restart it with: ./mvnw quarkus:dev
Injecting Configuration Value
When we persist a new hero we want to multiply its level by a value that can be configured.
For this, Quarkus uses MicroProfile Config to inject the configuration in the application.[7]
The injection uses the @ConfigProperty
annotation.
When injecting a configured value, you can use |
Call to action
Edit the HeroService
, and introduce the following configuration properties:
@ConfigProperty(name = "level.multiplier", defaultValue="1")
int levelMultiplier;
You may need to add the following import statement if your IDE does not do it automatically: |
-
If you do not provide a value for this property, the application startup fails with
javax.enterprise.inject.spi.DeploymentException: No config value of type [int] exists for: level.multiplier
-
A default value (property
defaultValue
) is injected if the configuration does not provide a value forlevel.multiplier
Call to action
Now, modify the HeroService.persistHero()
method to use the injected properties:
public Hero persistHero(@Valid Hero hero) {
hero.level = hero.level * levelMultiplier;
Hero.persist(hero);
return hero;
}
Create the Configuration
By default, Quarkus reads application.properties
.
Call to action
Edit the src/main/resources/application.properties
with the following content:
# Business configuration
level.multiplier = 3
Running and Testing the Application
Call to action
If you didn’t already, start the application with ./mvnw quarkus:dev
.
Once started, create a new hero with the following cUrl command:
$ curl -X POST -d '{"level":5, "name":"Chewbacca", "powers":"Agility, Longevity"}' -H "Content-Type: application/json" https://$(oc get route -n USERNAME-codeready | grep 8083 | awk '{ print $2 }')/api/heroes -v
< HTTP/1.1 201 Created
As you can see, we’ve passed a level of 5 to create this new hero. The cUrl command returns the location of the newly created hero. Take this URL and do an HTTP GET on it. You will see that the level has been increased.
$ curl $(oc get route -n USERNAME-codeready | grep 8083 | awk '{ print $2 }')/api/heroes/952 | jq
{
"id": 957,
"level": 15,
"name": "Chewbacca",
"powers": "Agility, Longevity"
}
Hey! Wait a minute! Tests are failing now! Indeed, they don’t know the multiplier.
Call to action
In the application.properties
file, add: %test.level.multiplier=1
which set the multiplier to 1 when running the tests.
We will cover the %test
syntax soon.
Open API
By default, a Quarkus application exposes its API description through an OpenAPI specification. Quarkus also lets you test it via a user-friendly UI named Swagger UI.
Directory Structure
In this module we will add extra class (HeroApplication
) to the Hero API project.
You will end-up with the following directory structure:
Installing the OpenAPI Dependency
Quarkus proposes a smallrye-openapi
extension compliant with the Eclipse MicroProfile OpenAPI specification in order to generate your API OpenAPI v3 specification.[8]
Call to action
To install the OpenAPI dependency, just run the following command:
$ ./mvnw quarkus:add-extension -Dextensions="smallrye-openapi"
This will add the following dependency in the pom.xml
file:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>
Open API
Now, you curl the hero endpoint:
$ curl $(oc get route -n USERNAME-codeready | grep 8083 | awk '{ print $2 }')/q/openapi
---
openapi: 3.0.3
info:
title: rest-hero API
version: 1.0.0-SNAPSHOT
paths:
/api/heroes:
get:
responses:
"200":
description: OK
put:
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Hero'
responses:
"200":
description: OK
post:
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Hero'
responses:
"200":
description: OK
/api/heroes/hello:
get:
responses:
"200":
description: OK
content:
text/plain:
schema:
type: string
/api/heroes/random:
get:
responses:
"200":
description: OK
/api/heroes/{id}:
get:
parameters:
- name: id
in: path
required: true
schema:
format: int64
type: integer
responses:
"200":
description: OK
delete:
parameters:
- name: id
in: path
required: true
schema:
format: int64
type: integer
responses:
"200":
description: OK
components:
schemas:
Hero:
required:
- level
- name
type: object
properties:
id:
format: int64
type: integer
level:
format: int32
minimum: 1
type: integer
nullable: false
name:
maxLength: 50
minLength: 3
type: string
nullable: false
otherName:
type: string
picture:
type: string
powers:
type: string
This contract lacks of documentation. The Eclipse MicroProfile OpenAPI allows you to customize the methods of your REST endpoint as well as the application.
Customizing Methods
The MicroProfile OpenAPI has a set of annotations to customize each REST endpoint method so the OpenAPI contract is richer and clearer for consumers:
-
@Operation
: Describes a single API operation on a path. -
@APIResponse
: Corresponds to the OpenAPI Response model object which describes a single response from an API Operation -
@Parameter
: The name of the parameter. -
@RequestBody
: A brief description of the request body.
This is what the HeroResource
endpoint looks like once annotated
package io.quarkus.workshop.superheroes.hero;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.jboss.logging.Logger;
import javax.inject.Inject;
import javax.validation.Valid;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
import java.net.URI;
import java.util.List;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static javax.ws.rs.core.MediaType.TEXT_PLAIN;
@Path("/api/heroes")
@Produces(APPLICATION_JSON)
public class HeroResource {
private static final Logger LOGGER = Logger.getLogger(HeroResource.class);
@Inject
HeroService service;
@Operation(summary = "Returns a random hero")
@APIResponse(responseCode = "200", content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = Hero.class, required = true)))
@GET
@Path("/random")
public Response getRandomHero() {
Hero hero = service.findRandomHero();
LOGGER.debug("Found random hero " + hero);
return Response.ok(hero).build();
}
@Operation(summary = "Returns all the heroes from the database")
@APIResponse(responseCode = "200", content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = Hero.class, type = SchemaType.ARRAY)))
@APIResponse(responseCode = "204", description = "No heroes")
@GET
public Response getAllHeroes() {
List<Hero> heroes = service.findAllHeroes();
LOGGER.debug("Total number of heroes " + heroes);
return Response.ok(heroes).build();
}
@Operation(summary = "Returns a hero for a given identifier")
@APIResponse(responseCode = "200", content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = Hero.class)))
@APIResponse(responseCode = "204", description = "The hero is not found for a given identifier")
@GET
@Path("/{id}")
public Response getHero(
@Parameter(description = "Hero identifier", required = true)
@PathParam("id") Long id) {
Hero hero = service.findHeroById(id);
if (hero != null) {
LOGGER.debug("Found hero " + hero);
return Response.ok(hero).build();
} else {
LOGGER.debug("No hero found with id " + id);
return Response.noContent().build();
}
}
@Operation(summary = "Creates a valid hero")
@APIResponse(responseCode = "201", description = "The URI of the created hero", content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = URI.class)))
@POST
public Response createHero(
@RequestBody(required = true, content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = Hero.class)))
@Valid Hero hero, @Context UriInfo uriInfo) {
hero = service.persistHero(hero);
UriBuilder builder = uriInfo.getAbsolutePathBuilder().path(Long.toString(hero.id));
LOGGER.debug("New hero created with URI " + builder.build().toString());
return Response.created(builder.build()).build();
}
@Operation(summary = "Updates an exiting hero")
@APIResponse(responseCode = "200", description = "The updated hero", content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = Hero.class)))
@PUT
public Response updateHero(
@RequestBody(required = true, content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = Hero.class)))
@Valid Hero hero) {
hero = service.updateHero(hero);
LOGGER.debug("Hero updated with new valued " + hero);
return Response.ok(hero).build();
}
@Operation(summary = "Deletes an exiting hero")
@APIResponse(responseCode = "204")
@DELETE
@Path("/{id}")
public Response deleteHero(
@Parameter(description = "Hero identifier", required = true)
@PathParam("id") Long id) {
service.deleteHero(id);
LOGGER.debug("Hero deleted with " + id);
return Response.noContent().build();
}
@GET
@Produces(MediaType.TEXT_PLAIN)
@Path("/hello")
public String hello() {
return "hello RESTEasy";
}
}
Customizing the Application
The previous annotations allow you to customize the contract for a given REST Endpoint. But it’s also important to customize the entire application. The Microprofile OpenAPI also has a set of annotation to do so. The difference is that these annotations cannot be used on the Endpoint itself, but instead on another Java class configuring the entire application.
Call to action
For this, you need to create the src/main/java/io/quarkus/workshop/superheroes/hero/HeroApplication
class with the following content:
package io.quarkus.workshop.superheroes.hero;
import org.eclipse.microprofile.openapi.annotations.ExternalDocumentation;
import org.eclipse.microprofile.openapi.annotations.OpenAPIDefinition;
import org.eclipse.microprofile.openapi.annotations.info.Contact;
import org.eclipse.microprofile.openapi.annotations.info.Info;
import org.eclipse.microprofile.openapi.annotations.servers.Server;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
@ApplicationPath("/")
@OpenAPIDefinition(
info = @Info(title = "Hero API",
description = "This API allows CRUD operations on a hero",
version = "1.0",
contact = @Contact(name = "Quarkus", url = "https://github.com/quarkusio")),
externalDocs = @ExternalDocumentation(url = "https://github.com/quarkusio/quarkus-workshops", description = "All the Quarkus workshops"),
tags = {
@Tag(name = "api", description = "Public that can be used by anybody"),
@Tag(name = "heroes", description = "Anybody interested in heroes")
}
)
public class HeroApplication extends Application {
}
Customized Contract
If you go back to the $URL/q/openapi
endpoint you will see the following OpenAPI contract:
---
openapi: 3.0.3
info:
title: rest-hero API
version: 1.0.0-SNAPSHOT
paths:
/api/heroes:
get:
summary: Returns all the heroes from the database
responses:
"200":
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Hero'
"204":
description: No heroes
put:
summary: Updates an exiting hero
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Hero'
required: true
responses:
"200":
description: The updated hero
content:
application/json:
schema:
$ref: '#/components/schemas/Hero'
post:
summary: Creates a valid hero
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Hero'
required: true
responses:
"201":
description: The URI of the created hero
content:
application/json:
schema:
format: uri
type: string
/api/heroes/hello:
get:
responses:
"200":
description: OK
content:
text/plain:
schema:
type: string
/api/heroes/random:
get:
summary: Returns a random hero
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Hero'
/api/heroes/{id}:
get:
summary: Returns a hero for a given identifier
parameters:
- name: id
in: path
description: Hero identifier
required: true
schema:
format: int64
type: integer
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Hero'
"204":
description: The hero is not found for a given identifier
delete:
summary: Deletes an exiting hero
parameters:
- name: id
in: path
description: Hero identifier
required: true
schema:
format: int64
type: integer
responses:
"204": {}
components:
schemas:
Hero:
required:
- level
- name
type: object
properties:
id:
format: int64
type: integer
level:
format: int32
minimum: 1
type: integer
nullable: false
name:
maxLength: 50
minLength: 3
type: string
nullable: false
otherName:
type: string
picture:
type: string
powers:
type: string
Swagger UI
When building APIs, developers want to test them quickly.
Swagger UI is a great tool permitting to visualize and interact with your APIs.[9]
The UI is automatically generated from your OpenAPI specification.
The Quarkus smallrye-openapi
extension comes with a swagger-ui extension embedding a properly configured Swagger UI page.
By default, Swagger UI is accessible at /q/swagger-ui
.
So, once your application is started, you can go to $URL/q/swagger-ui
and play with your API.
Call to action
You can visualize your API’s operations and schemas.
Notice that in the Swagger UI you have your specific URL exposing the rest-hero service |
For example, you can try the /api/heroes/random
endpoint to retrieve a random hero.
OpenAPI Tests in HeroResourceTest
Call to action
Let’s add a few extra test methods in HeroResourceTest
that would make sure OpenAPI and Swagger UI are packaged in the application:
@Test
void shouldPingOpenAPI() {
given()
.header(ACCEPT, APPLICATION_JSON)
.when().get("/q/openapi")
.then()
.statusCode(OK.getStatusCode());
}
@Test
void shouldPingSwaggerUI() {
given()
.when().get("/q/swagger-ui")
.then()
.statusCode(OK.getStatusCode());
}
Call to action
Execute the test using ./mvnw test
.
If you have any problem with the code, don’t understand or feel you are running, remember to ask for some help. Also, you can get the code of this entire workshop from https://github.com/Red-Hat-Developer-Games/quarkus-workshop/tree/main/quarkus-workshop-super-heroes. |
Application Lifecycle
Now that you know how is structured Quarkus, let’s continue using various extensions. You often need to execute custom actions when the application starts and clean up everything when the application stops. In this module we will display a banner in the logs once the Hero API has started.
Directory Structure
In this module we will add an extra class (HeroApplicationLifeCycle
) to handle the Hero API lifecycle.
You will end-up with the following directory structure:
Displaying a Banner
When our application starts, the logs are pretty boring… and lack of a banner (any decent application must have a banner nowadays). So the first thing that you need to do is to go to the following website and pick up your favourite "Hero API" text banner.
Call to action
Create a new class named HeroApplicationLifeCycle
(or pick another name, the name does not matter) in the io.quarkus.workshop.superheroes.hero
package, and copy your banner so you end up with a similar content:
package io.quarkus.workshop.superheroes.hero;
import io.quarkus.runtime.ShutdownEvent;
import io.quarkus.runtime.StartupEvent;
import io.quarkus.runtime.configuration.ProfileManager;
import org.jboss.logging.Logger;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Observes;
@ApplicationScoped
class HeroApplicationLifeCycle {
private static final Logger LOGGER = Logger.getLogger(HeroApplicationLifeCycle.class);
void onStart(@Observes StartupEvent ev) {
LOGGER.info(" _ _ _ ____ ___ ");
LOGGER.info(" | | | | ___ _ __ ___ / \\ | _ \\_ _|");
LOGGER.info(" | |_| |/ _ \\ '__/ _ \\ / _ \\ | |_) | | ");
LOGGER.info(" | _ | __/ | | (_) | / ___ \\| __/| | ");
LOGGER.info(" |_| |_|\\___|_| \\___/ /_/ \\_\\_| |___|");
LOGGER.info(" Powered by Quarkus");
}
void onStop(@Observes ShutdownEvent ev) {
LOGGER.info("The application HERO is stopping...");
}
}
Thanks to the CDI @Observes
, the HeroApplicationLifeCycle
is invoked:
-
on startup with the
StartupEvent
so it can execute code (here, displaying the banner) when the application is starting -
on shutdown with the
ShutdownEvent
when the application is terminating
Call to action
Run the application with: ./mvnw quarkus:dev
, the banner is printed to the console.
When the application is stopped, the second log message is printed.
If your application was still running, just send an HTTP request, like go to the hero endpoint url. As the application code changed, the application is restarted. |
Configuration Profiles
Quarkus supports the notion of configuration profiles. These allow you to have multiple configuration in the same file and select between them via a profile name.
By default Quarkus has three profiles, although it is possible to use as many as you like. The default profiles are:
-
dev
- Activated when in development mode (i.e.quarkus:dev
) -
test
- Activated when running tests -
prod
- The default profile when not running in development or test mode
Let’s change the HeroApplicationLifeCycle
so it displays the current profile.
Call to action
For that, just add a log invoking the ProfileManager.getActiveProfile()
method:
void onStart(@Observes StartupEvent ev) {
LOGGER.info(" _ _ _ ____ ___ ");
LOGGER.info(" | | | | ___ _ __ ___ / \\ | _ \\_ _|");
LOGGER.info(" | |_| |/ _ \\ '__/ _ \\ / _ \\ | |_) | | ");
LOGGER.info(" | _ | __/ | | (_) | / ___ \\| __/| | ");
LOGGER.info(" |_| |_|\\___|_| \\___/ /_/ \\_\\_| |___|");
LOGGER.info(" Powered by Quarkus");
LOGGER.infof("The application HERO is starting with profile `%s`", ProfileManager.getActiveProfile());
}
If not already done, you need to add the following import statement: |
In the application.properties
file, you can prefix a property to be defined in the running profile.
For example, we did add the %test.level.multiplier=1
property in the previous chapter.
This indicates that the property level.multiplier
is set to 1 in the test
profile.
Now, if you start your application in dev mode with mvn compile quarkus:dev
, you will get the dev
profile enabled.
If you start the tests, the test
profile is enabled (and so the multiplier
is set to 1).
Call to action
Package your application with mvn package
, and start it with java -Dquarkus.profile=foo -jar target/quarkus-app/quarkus-run.jar
.
You will see that the foo
profile is enabled.
As not overridden, the level.multiplier
property has the value 3.
Profiles are very useful to customize the configuration per environment. We are going to see an example of such customization in the next section.
One Microservice is no Microservices
So far we’ve built one microservice. In the following sections you will develop two extra microservices: a villain microservice, a mad copycat of the hero microservice, and a fight microservice where heroes and villains fight. We will also add an Angular front-end so we can fight graphically.
Each microservice is developed in it’s own directory.
Villain Microservice
New microservice, new project! In this section we will see the counterpart of the Hero microservice: the Villain microservice! The Villain REST Endpoint is really similar to the Hero Endpoint.
The code has already been provided in the /super-heroes/rest-villain/
directory.
There is almost no differences with the hero microservice, just that it provides super villains instead and uses the port 8084
.
Directory Structure
As for the hero microservice, you have the following directory structure:
If you look at the code, it’s very similar to the hero microservice.
Running, Testing and Packaging the Application
Call to action
First, make sure the tests pass by executing the command ./mvnw test
(or from your IDE).
Now that the tests are green, we are ready to run our application.
Call to action
Use ./mvnw quarkus:dev
to start it (notice the nice banner).
Once the application is started, create a new villain with the following cUrl command:
$ curl -X POST -d '{"level":2, "name":"Darth Vader", "powers":"Darkness, Longevity"}' -H "Content-Type: application/json" $(oc get route -n USERNAME-codeready | grep 8084 | awk '{ print $2 }')/api/villains -v
< HTTP/1.1 201 Created
The cUrl command returns the location of the newly created villain. Take this URL and do an HTTP GET on it.
$ curl $URL/api/villains/582 | jq
{
"id": 582,
"level": 4,
"name": "Darth Vader",
"powers": "Darkness, Longevity"
}
Remember that you can also check Swagger UI by going to $URL/q/swagger-ui.
Change the URL with yours. |
Fight Microservice
Ok, let’s develop another microservice. We have a REST API that returns a random Hero. Another REST API that returns a random Villain… we need a new REST API that invokes those two, gets one random hero and one random villain and makes them fight. Let’s call it the Fight API.
Bootstrapping the Fight REST Endpoint
Like for the Hero and Villain API, the easiest way to create this new Quarkus project is to use a Maven archetype.
Under the $CHE_PROJECTS_ROOT/quarkus-workshop-super-heroes/super-heroes
root directory where you have all your code.
Call to action
Open a terminal and run the following command:
mvn io.quarkus:quarkus-maven-plugin:2.2.2.Final:create \
-DprojectGroupId=io.quarkus.workshop.super-heroes \
-DprojectArtifactId=rest-fight \
-DclassName="io.quarkus.workshop.superheroes.fight.FightResource" \
-Dpath="api/fights" \
-Dextensions="jdbc-postgresql,hibernate-orm-panache,hibernate-validator,quarkus-resteasy-jsonb,smallrye-openapi"
cd rest-fight
Call to action
Also add Testcontainers and other test-related dependencies to your pom.xml
.
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<scope>test</scope>
</dependency>
Prefering Web UI
Instead of the Maven command, you can use https://code.quarkus.io. |
You can see that beyond the extensions we have used so far, we added the Kafka support which uses Eclipse MicroProfile Reactive Messaging. Stay tuned.
Directory Structure
At the end you should have the following directory structure:
Fight Entity
A fight is between a hero and a villain.
Each time there is a fight, there is a winner and a loser.
So the Fight
entity is there to store all these fights.
package io.quarkus.workshop.superheroes.fight;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import javax.persistence.Entity;
import javax.validation.constraints.NotNull;
import java.time.Instant;
@Entity
@Schema(description="Each fight has a winner and a loser")
public class Fight extends PanacheEntity {
@NotNull
public Instant fightDate;
@NotNull
public String winnerName;
@NotNull
public int winnerLevel;
@NotNull
public String winnerPicture;
@NotNull
public String loserName;
@NotNull
public int loserLevel;
@NotNull
public String loserPicture;
@NotNull
public String winnerTeam;
@NotNull
public String loserTeam;
// toString method
}
Fighters Bean
Now comes a trick.
The Fight REST API will ultimately invoke the Hero and Villain APIs (next sections) to get two random fighters.
The Fighters
class has one Hero
and one Villain
.
Notice that Fighters
is not an entity, it is not persisted in the database, just marshalled and unmarshalled to JSon.
package io.quarkus.workshop.superheroes.fight;
import io.quarkus.workshop.superheroes.fight.client.Hero;
import io.quarkus.workshop.superheroes.fight.client.Villain;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import javax.validation.constraints.NotNull;
@Schema(description="A fight between one hero and one villain")
public class Fighters {
@NotNull
public Hero hero;
@NotNull
public Villain villain;
}
The Fight REST API is just interested in the hero’s name, level, picture and powers (not the other name as described in the Hero API).
So the Hero
bean looks like this (notice the client
subpackage):
package io.quarkus.workshop.superheroes.fight.client;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import javax.validation.constraints.NotNull;
@Schema(description="The hero fighting against the villain")
public class Hero {
@NotNull
public String name;
@NotNull
public int level;
@NotNull
public String picture;
public String powers;
}
Villain
is pretty similar (also in the client
subpackage):
package io.quarkus.workshop.superheroes.fight.client;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import javax.validation.constraints.NotNull;
@Schema(description="The villain fighting against the hero")
public class Villain {
@NotNull
public String name;
@NotNull
public int level;
@NotNull
public String picture;
public String powers;
}
So, these classes are just used to map the results from the Hero and Villain microservices.
FightService Transactional Service
To transactionnally manipulate the Fight
entity we need a FightService
.
Notice the persistFight
method.
This method is the one creating a fight between a hero and a villain.
As you can see the algorithm to determine the winner is a bit random (even though it uses the levels).
If you are not happy about the way the fight operates, choose your own winning algorithm ;o)
package io.quarkus.workshop.superheroes.fight;
import io.quarkus.workshop.superheroes.fight.client.Hero;
import io.quarkus.workshop.superheroes.fight.client.Villain;
import org.jboss.logging.Logger;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.transaction.Transactional;
import java.time.Instant;
import java.util.List;
import java.util.Random;
import static javax.transaction.Transactional.TxType.REQUIRED;
import static javax.transaction.Transactional.TxType.SUPPORTS;
@ApplicationScoped
@Transactional(SUPPORTS)
public class FightService {
private static final Logger LOGGER = Logger.getLogger(FightService.class);
private final Random random = new Random();
public List<Fight> findAllFights() {
return Fight.listAll();
}
public Fight findFightById(Long id) {
return Fight.findById(id);
}
@Transactional(REQUIRED)
public Fight persistFight(Fighters fighters) {
// Amazingly fancy logic to determine the winner...
Fight fight;
int heroAdjust = random.nextInt(20);
int villainAdjust = random.nextInt(20);
if ((fighters.hero.level + heroAdjust)
> (fighters.villain.level + villainAdjust)) {
fight = heroWon(fighters);
} else if (fighters.hero.level < fighters.villain.level) {
fight = villainWon(fighters);
} else {
fight = random.nextBoolean() ? heroWon(fighters) : villainWon(fighters);
}
fight.fightDate = Instant.now();
fight.persist(fight);
return fight;
}
private Fight heroWon(Fighters fighters) {
LOGGER.info("Yes, Hero won :o)");
Fight fight = new Fight();
fight.winnerName = fighters.hero.name;
fight.winnerPicture = fighters.hero.picture;
fight.winnerLevel = fighters.hero.level;
fight.loserName = fighters.villain.name;
fight.loserPicture = fighters.villain.picture;
fight.loserLevel = fighters.villain.level;
fight.winnerTeam = "heroes";
fight.loserTeam = "villains";
return fight;
}
private Fight villainWon(Fighters fighters) {
LOGGER.info("Gee, Villain won :o(");
Fight fight = new Fight();
fight.winnerName = fighters.villain.name;
fight.winnerPicture = fighters.villain.picture;
fight.winnerLevel = fighters.villain.level;
fight.loserName = fighters.hero.name;
fight.loserPicture = fighters.hero.picture;
fight.loserLevel = fighters.hero.level;
fight.winnerTeam = "villains";
fight.loserTeam = "heroes";
return fight;
}
Call to action
For now, just implement an empty
|
FightResource Endpoint
To expose a REST API we also need a FightResource
(with OpenAPI annotations of course).
package io.quarkus.workshop.superheroes.fight;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.jboss.logging.Logger;
import javax.inject.Inject;
import javax.validation.Valid;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.util.List;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static javax.ws.rs.core.MediaType.TEXT_PLAIN;
@Path("/api/fights")
@Produces(APPLICATION_JSON)
public class FightResource {
private static final Logger LOGGER = Logger.getLogger(FightResource.class);
@Inject
FightService service;
@Operation(summary = "Returns two random fighters")
@APIResponse(responseCode = "200", content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = Fighters.class, required = true)))
@GET
@Path("/randomfighters")
public Response getRandomFighters() throws InterruptedException {
Fighters fighters = service.findRandomFighters();
LOGGER.debug("Get random fighters " + fighters);
return Response.ok(fighters).build();
}
@Operation(summary = "Returns all the fights from the database")
@APIResponse(responseCode = "200", content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = Fight.class, type = SchemaType.ARRAY)))
@APIResponse(responseCode = "204", description = "No fights")
@GET
public Response getAllFights() {
List<Fight> fights = service.findAllFights();
LOGGER.debug("Total number of fights " + fights);
return Response.ok(fights).build();
}
@Operation(summary = "Returns a fight for a given identifier")
@APIResponse(responseCode = "200", content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = Fight.class)))
@APIResponse(responseCode = "204", description = "The fight is not found for a given identifier")
@GET
@Path("/{id}")
public Response getFight(@Parameter(description = "Fight identifier", required = true) @PathParam("id") Long id) {
Fight fight = service.findFightById(id);
if (fight != null) {
LOGGER.debug("Found fight " + fight);
return Response.ok(fight).build();
} else {
LOGGER.debug("No fight found with id " + id);
return Response.noContent().build();
}
}
@Operation(summary = "Trigger a fight between two fighters")
@APIResponse(responseCode = "200", description = "The result of the fight", content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = Fight.class)))
@POST
public Fight fight(@RequestBody(description = "The two fighters fighting", required = true, content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = Fighters.class))) @Valid Fighters fighters, @Context UriInfo uriInfo) {
return service.persistFight(fighters);
}
@GET
@Produces(TEXT_PLAIN)
@Path("/hello")
public String hello() {
return "Hello RESTEasy";
}
}
FightApplication for OpenAPI
The FightApplication
class is just there to customize the OpenAPI contract.
package io.quarkus.workshop.superheroes.fight;
import org.eclipse.microprofile.openapi.annotations.ExternalDocumentation;
import org.eclipse.microprofile.openapi.annotations.OpenAPIDefinition;
import org.eclipse.microprofile.openapi.annotations.info.Contact;
import org.eclipse.microprofile.openapi.annotations.info.Info;
import org.eclipse.microprofile.openapi.annotations.servers.Server;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
@ApplicationPath("/")
@OpenAPIDefinition(
info = @Info(title = "Fight API",
description = "This API allows a hero and a villain to fight",
version = "1.0",
contact = @Contact(name = "Quarkus", url = "https://github.com/quarkusio")),
externalDocs = @ExternalDocumentation(url = "https://github.com/Red-Hat-Developer-Games/quarkus-workshop", description = "All the Quarkus workshops"),
tags = {
@Tag(name = "api", description = "Public that can be used by anybody"),
@Tag(name = "fight", description = "Anybody interested in fights"),
@Tag(name = "superheroes", description = "Well, superhero fights")
}
)
public class FightApplication extends Application {
}
Notice that there is no |
Adding Data
Call to action
To load some SQL statements when Hibernate ORM starts, download the SQL file import.sql and copy it under src/main/resources
.
INSERT INTO fight(id, fightDate, winnerName, winnerLevel, winnerPicture, loserName, loserLevel, loserPicture, winnerTeam, loserTeam)
VALUES (nextval('hibernate_sequence'), current_timestamp, 'Chewbacca', 5, 'https://www.superherodb.com/pictures2/portraits/10/050/10466.jpg', 'Buuccolo', 3, 'https://www.superherodb.com/pictures2/portraits/11/050/15355.jpg', 'heroes', 'villains');
INSERT INTO fight(id, fightDate, winnerName, winnerLevel, winnerPicture, loserName, loserLevel, loserPicture, winnerTeam ,loserTeam)
VALUES (nextval('hibernate_sequence'), current_timestamp, 'Galadriel', 10, 'https://www.superherodb.com/pictures2/portraits/11/050/11796.jpg', 'Darth Vader', 8, 'https://www.superherodb.com/pictures2/portraits/10/050/10444.jpg', 'heroes', 'villains');
INSERT INTO fight(id, fightDate, winnerName, winnerLevel, winnerPicture, loserName, loserLevel, loserPicture, winnerTeam ,loserTeam)
VALUES (nextval('hibernate_sequence'), current_timestamp, 'Annihilus', 23, 'https://www.superherodb.com/pictures2/portraits/10/050/1307.jpg', 'Shikamaru', 1, 'https://www.superherodb.com/pictures2/portraits/10/050/11742.jpg', 'villains', 'heroes');
...
Configuration
As usual, we need to configure the application.
Call to action
In the application.properties
file add:
quarkus.http.port=8082
quarkus.http.host=0.0.0.0
## Database configuration
quarkus.datasource.jdbc.url=jdbc:postgresql://fights-database.USERNAME-heroes:5432/fights_database
quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=superfight
quarkus.datasource.password=superfight
quarkus.datasource.max-size=8
quarkus.datasource.min-size=2
quarkus.hibernate-orm.database.generation=drop-and-create
quarkus.hibernate-orm.log.sql=true
## Logging configuration
quarkus.log.console.enable=true
quarkus.log.console.format=%d{HH:mm:ss} %-5p [%c{2.}] (%t) %s%e%n
quarkus.log.console.level=DEBUG
quarkus.log.console.color=true
## Production configuration
%prod.quarkus.hibernate-orm.log.sql=false
%prod.quarkus.log.console.level=INFO
%prod.quarkus.hibernate-orm.database.generation=update
process.milliseconds=0
quarkus.kubernetes-client.trust-certs=true
quarkus.kubernetes-client.namespace=USERNAME-heroes
quarkus.openshift.route.expose=true
Note that the fight service uses the port 8082.
FightResourceTest Test Class
We need to test our REST API.
Call to action
For that, copy the following FightResourceTest
class under the src/test/java/io/quarkus/workshop/superheroes/fight
directory.
package io.quarkus.workshop.superheroes.fight;
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.workshop.superheroes.fight.client.Hero;
import io.quarkus.workshop.superheroes.fight.client.Villain;
import io.restassured.common.mapper.TypeRef;
import org.hamcrest.core.Is;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import java.util.List;
import java.util.Random;
import static io.restassured.RestAssured.get;
import static io.restassured.RestAssured.given;
import static javax.ws.rs.core.HttpHeaders.ACCEPT;
import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static javax.ws.rs.core.Response.Status.*;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@QuarkusTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class FightResourceTest {
private static final String DEFAULT_WINNER_NAME = "Super Baguette";
private static final String DEFAULT_WINNER_PICTURE = "super_baguette.png";
private static final int DEFAULT_WINNER_LEVEL = 42;
private static final String DEFAULT_LOSER_NAME = "Super Chocolatine";
private static final String DEFAULT_LOSER_PICTURE = "super_chocolatine.png";
private static final int DEFAULT_LOSER_LEVEL = 6;
private static final int NB_FIGHTS = 10;
private static String fightId;
@Test
void shouldPingOpenAPI() {
given()
.header(ACCEPT, APPLICATION_JSON)
.when().get("/q/openapi")
.then()
.statusCode(OK.getStatusCode());
}
@Test
void shouldPingSwaggerUI() {
given()
.when().get("/q/swagger-ui")
.then()
.statusCode(OK.getStatusCode());
}
@Test
public void testHelloEndpoint() {
given()
.when().get("/api/fights/hello")
.then()
.statusCode(200)
.body(is("Hello RESTEasy"));
}
@Test
void shouldNotGetUnknownFight() {
Long randomId = new Random().nextLong();
given()
.pathParam("id", randomId)
.when().get("/api/fights/{id}")
.then()
.statusCode(NO_CONTENT.getStatusCode());
}
@Test
void shouldNotAddInvalidItem() {
Fighters fighters = new Fighters();
fighters.hero = null;
fighters.villain = null;
given()
.body(fighters)
.header(CONTENT_TYPE, APPLICATION_JSON)
.header(ACCEPT, APPLICATION_JSON)
.when()
.post("/api/fights")
.then()
.statusCode(BAD_REQUEST.getStatusCode());
}
@Test
@Order(1)
void shouldGetInitialItems() {
List<Fight> fights = get("/api/fights").then()
.statusCode(OK.getStatusCode())
.header(CONTENT_TYPE, APPLICATION_JSON)
.extract().body().as(getFightTypeRef());
assertEquals(NB_FIGHTS, fights.size());
}
@Test
@Order(2)
void shouldAddAnItem() {
Hero hero = new Hero();
hero.name = DEFAULT_WINNER_NAME;
hero.picture = DEFAULT_WINNER_PICTURE;
hero.level = DEFAULT_WINNER_LEVEL;
Villain villain = new Villain();
villain.name = DEFAULT_LOSER_NAME;
villain.picture = DEFAULT_LOSER_PICTURE;
villain.level = DEFAULT_LOSER_LEVEL;
Fighters fighters = new Fighters();
fighters.hero = hero;
fighters.villain = villain;
fightId = given()
.body(fighters)
.header(CONTENT_TYPE, APPLICATION_JSON)
.header(ACCEPT, APPLICATION_JSON)
.when()
.post("/api/fights")
.then()
.statusCode(OK.getStatusCode())
.body(containsString("winner"), containsString("loser"))
.extract().body().jsonPath().getString("id");
assertNotNull(fightId);
given()
.pathParam("id", fightId)
.when().get("/api/fights/{id}")
.then()
.statusCode(OK.getStatusCode())
.header(CONTENT_TYPE, APPLICATION_JSON)
.body("winnerName", Is.is(DEFAULT_WINNER_NAME))
.body("winnerPicture", Is.is(DEFAULT_WINNER_PICTURE))
.body("winnerLevel", Is.is(DEFAULT_WINNER_LEVEL))
.body("loserName", Is.is(DEFAULT_LOSER_NAME))
.body("loserPicture", Is.is(DEFAULT_LOSER_PICTURE))
.body("loserLevel", Is.is(DEFAULT_LOSER_LEVEL))
.body("fightDate", Is.is(notNullValue()));
List<Fight> fights = get("/api/fights").then()
.statusCode(OK.getStatusCode())
.header(CONTENT_TYPE, APPLICATION_JSON)
.extract().body().as(getFightTypeRef());
assertEquals(NB_FIGHTS + 1, fights.size());
}
private TypeRef<List<Fight>> getFightTypeRef() {
return new TypeRef<List<Fight>>() {
// Kept empty on purpose
};
}
Delete the generated NativeFightResourceIT
class, as we won’t run native test for this microservice.
Running, Testing and Packaging the Application
Call to action
First, make sure the tests pass by executing the command ./mvnw test
(or from your IDE).
Now that the tests are green, we are ready to run our application.
Use ./mvnw quarkus:dev
to start it (notice that there is no banner yet, it will come later).
Once the application is started, just check that it returns the fights from the database with the following cUrl command:
$ curl $URL/api/fights
Change the URL with yours. |
Remember that you can also check Swagger UI by going to $URL/q/swagger-ui.
User Interface
Now that we have the three main microservices, time to have a decent user interface to start fighting. The purpose of this workshop is not to develop a web interface and learn yet another web framework. This time you will just download an Angular application, install it, and run it on another Quarkus instance.
The Web Application
Navigate to the super-heroes/ui-super-heroes/ui-super-heroes
directory.
It contains the code of the microservice.
Being an Angular application, you will find a package.json
file which defines all the needed dependencies.
Notice that there is a pom.xml
file.
This is just a convenient way to install NodeJS and NPM so we can build the Angular application with Maven.
The pom.xml
also allows us to package the Angular application into Quarkus.
If you are not in a frontend mood, just scroll to Installing the Web Application on Quarkus
Looking at Some Code (optional)
You don’t need to be an Angular expert, but there are some pieces of code that are worth looking at.
If you look under the src/app/shared
directory, you will find an api
and a model
sub-directory.
Let’s look at fight.ts
.
export interface Fight {
id?: number;
fightDate: FightFightDate;
winnerName: string;
winnerLevel: number;
winnerPicture: string;
loserName: string;
loserLevel: number;
loserPicture: string;
}
As you can see, it matches our Fight
Java class.
Same for fighters.ts
, hero.ts
or villain.ts
.
Under api
there is the fight.service.ts
that defines all the methods to access to our Fight REST API through HTTP.
public apiFightsGet(observe?: 'body', reportProgress?: boolean): Observable<Array<Fight>>;
public apiFightsGet(observe?: 'response', reportProgress?: boolean): Observable<HttpResponse<Array<Fight>>>;
public apiFightsGet(observe?: 'events', reportProgress?: boolean): Observable<HttpEvent<Array<Fight>>>;
public apiFightsRandomfightersGet(observe?: 'body', reportProgress?: boolean): Observable<Fighters>;
public apiFightsRandomfightersGet(observe?: 'response', reportProgress?: boolean): Observable<HttpResponse<Fighters>>;
public apiFightsRandomfightersGet(observe?: 'events', reportProgress?: boolean): Observable<HttpEvent<Fighters>>;
Well, guess what?
We didn’t have to type this code either.
It was generated thanks to a tool called swagger-codegen
.[10]
Because our Fight REST API exposes an OpenAPI contract, swagger-codegen
just swallows it, and generates the TypeScript code to access it.
It’s just a matter of running:
$ swagger-codegen generate -i $(oc get route -n USERNAME-codeready | grep 8082 | awk '{ print $2 }')/openapi -l typescript-angular -o src/app/shared
Here, you see another advantage of exposing an OpenAPI contract: it documents the API which can be read by a human, or processed by tools.
Installing the Web Application on Quarkus
Call to action
Execute npm install
in the ui-super-heroes
directory for building the application.
You should now have a node_modules
directory with all the Angular dependencies.
At this stage, make sure the following commands work:
ng version (or ./node_modules/.bin/ng version)
node -v (or ./node/node -v)
To install the Angular application into a Quarkus instance, we just build the app and copy the bundles under the resources/META-INF/resources
directory.
Look at the package.sh
, that’s exactly what it does.
export DEST=src/main/resources/META-INF/resources
./node_modules/.bin/ng build --prod --base-href "."
rm -Rf ${DEST}
cp -R dist/* ${DEST}
Call to action
Execute the package.sh
script.
You will see all the Javascript files under resources/META-INF/resources
directory.
We are now ready to go.
If the |
Running the Web Application
Call to action
Set the following env var to allow the ui-super-heroes service contact the rest-fight.
$ export BASE_PATH=https://$(oc get route -n USERNAME-codeready | grep 8082 | awk '{ print $2 }')
As usual, use mvn quarkus:dev
to start the web application.
Be sure you have the hero and villain microservices running (dev mode is enough).
Once the application is started (mvn quarkus:dev
), open a browser on the ui-super-heroes url it should display the main web page. You can get the ui-super-heroes
url with this command:
$ oc get route -n USERNAME-codeready | grep 8080 | awk '{ print $2 }'
Notice that the 8080 is the default Quarkus port as we didn’t change it in the application.properties
this time.
Oups, not working yet! Not even the pictures, we must have been forgotten something! Let’s move on to the next section then and make the application work.
CORS
Cross-origin resource sharing (CORS) is a mechanism that allows restricted resources on a web page to be requested from another domain outside the domain from which the first resource was served.[11] So when we want our heroes and villains to fight, we actually cross several origins: we go from the UI to Fight API which invokes Hero API and Villain API. If you look at the console of your Browser you should see something similar to this:
Quarkus comes with a CORS filter which intercepts all incoming HTTP requests. It can be enabled in the Quarkus configuration file:
quarkus.http.cors=true
If the filter is enabled and an HTTP request is identified as cross-origin, the CORS policy and headers defined using the following properties will be applied before passing the request on to its actual target (servlet, JAX-RS resource, etc.):
Property | Description |
---|---|
|
The comma-separated list of origins allowed for CORS. The filter allows any origin if this is not set. |
|
The comma-separated list of HTTP methods allowed for CORS. The filter allows any method if this is not set. |
|
The comma-separated list of HTTP headers allowed for CORS. The filter allows any header if this is not set. |
|
The comma-separated list of HTTP headers exposed in CORS. |
|
The duration indicating how long the results of a pre-flight request can be cached. This value will be returned in a Access-Control-Max-Age response header. |
Call to action
So make sure you set the quarkus.http.cors
property to true
on the:
-
Fight microservice,
-
Hero microservice,
-
Villain microservice
But, even with this, the UI is still not working. The explanation is simple, we forgot another thing:
Remember the function to retrieve random fighters.
We are currently returning null
.
Let’s move to the next session to see how we can implement this method.
HTTP communication & Fault Tolerance
So far we’ve built one Fight microservice which need to invoke the Hero and Villain microservices. In the following sections you will develop this invocation thanks to the MicroProfile REST Client. We will also deal with fault tolerance thanks to timeouts and circuit breaker.
REST Client
This chapter explains how to use the MicroProfile REST Client in order to interact with REST APIs with very little effort.[12]
Directory Structure
Remember the structure of the Fight microservice:
We are going to rework the:
-
FightService
class -
FightResourceTest
class -
application.properties
Installing the REST Client Dependency
Call to action
To install the MicroProfile REST Client dependency, just run the following command:
$ ./mvnw quarkus:add-extension -Dextensions="rest-client"
This will add the following dependency in the pom.xml
file:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client</artifactId>
</dependency>
FightService Invoking External Microservices
Remember that in the previous sections we left the FightService.findRandomFighters()
method returns null
.
We have to fix this.
What we actually want is to invoke both the Hero and Villain APIs, asking for a random hero and a random villain.
Call to action
For that, replace the findRandomFighters
method with the following code to the FightService
class:
@Inject
@RestClient
HeroService heroService;
@Inject
@RestClient
VillainService villainService;
Fighters findRandomFighters() {
Hero hero = findRandomHero();
Villain villain = findRandomVillain();
Fighters fighters = new Fighters();
fighters.hero = hero;
fighters.villain = villain;
return fighters;
}
Hero findRandomHero() {
return heroService.findRandomHero();
}
Villain findRandomVillain() {
return villainService.findRandomVillain();
}
Note that in addition to the standard CDI @Inject
annotation, we also need to use the MicroProfile @RestClient
annotation to inject HeroService
and VillainService
.
If not done automatically by your IDE, add the following import statement: |
Creating the Interfaces
Using the MicroProfile REST Client is as simple as creating an interface using the proper JAX-RS and MicroProfile annotations.
Call to action
In our case both interfaces should be created under the client
subpackage and have the following content:
package io.quarkus.workshop.superheroes.fight.client;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
@Path("/api/heroes")
@Produces(MediaType.APPLICATION_JSON)
@RegisterRestClient
public interface HeroService {
@GET
@Path("/random")
Hero findRandomHero();
}
The findRandomHero
method gives our code the ability to query a random hero from the Hero REST API.
The client will handle all the networking and marshalling leaving our code clean of such technical details.
The purpose of the annotations in the code above is the following:
-
@RegisterRestClient
allows Quarkus to know that this interface is meant to be available for CDI injection as a REST Client -
@Path
and@GET
are the standard JAX-RS annotations used to define how to access the service -
@Produces
defines the expected content-type
The VillainService
is very similar and looks like this:
package io.quarkus.workshop.superheroes.fight.client;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
@Path("/api/villains")
@Produces(MediaType.APPLICATION_JSON)
@RegisterRestClient
public interface VillainService {
@GET
@Path("/random")
Villain findRandomVillain();
}
Call to action
Once created, go back to the FightService
class and add the following import statements:
import io.quarkus.workshop.superheroes.fight.client.HeroService;
import io.quarkus.workshop.superheroes.fight.client.VillainService;
Configuring REST Client Invocation
Call to action
In order to determine the base URL to which REST calls will be made, the REST Client uses configuration from application.properties
.
The name of the property needs to follow a certain convention which is best displayed in the following code:
%dev.io.quarkus.workshop.superheroes.fight.client.HeroService/mp-rest/url=http://${WKSPC_SERVICE:localhost}:8083
io.quarkus.workshop.superheroes.fight.client.HeroService/mp-rest/scope=javax.inject.Singleton
%dev.io.quarkus.workshop.superheroes.fight.client.VillainService/mp-rest/url=http://${WKSPC_SERVICE:localhost}:8084
io.quarkus.workshop.superheroes.fight.client.VillainService/mp-rest/scope=javax.inject.Singleton
Having this configuration means that all requests performed using HeroService
will use your specific rest-fight service exposed by the USERNAME-codeready
project, https://localhost:8083 by default, as the base URL.
Using this configuration, calling the findRandomHero
method of HeroService
would result in an HTTP GET request being made to ${rest-hero-url}/api/heroes/random.
This configuration works only for the dev
profile.
In order to make it work, you need to set up the WKSPC_SERVICE
env var. You can do it running the following command:
$ export WKSPC_SERVICE="$(oc get svc -n USERNAME-codeready | grep 8083 | awk '{ print $1 }')"
Having this configuration means that the default scope of HeroService
will be @Singleton
.
Supported scope values are @Singleton
, @Dependent
, @ApplicationScoped
and @RequestScoped
.
The default scope is @Dependent
.
The default scope can also be defined on the interface.
Now, go back in the UI and refresh, you should see some pictures!
Updating the Test with Mock Support
But, now we have another problem.
To run the tests of the Fight API we need the Hero and Villain REST APIs to be up and running.
To avoid this, we need to Mock the HeroService
and VillainService
interfaces.
Quarkus supports the use of mock objects using the CDI @Alternative
mechanism.[13]
Call to action
To use this simply override the bean you wish to mock with a class in the src/test/java
directory, and put the @Alternative
and @Priority(1)
annotations on the bean.
Alternatively, a convenient io.quarkus.test.Mock
stereotype annotation could be used.
This built-in stereotype declares @Alternative
, @Priority(1)
and @Dependent
.
So, to mock the HeroService
interface we just need to implement the following MockHeroService
class:
package io.quarkus.workshop.superheroes.fight.client;
import io.quarkus.test.Mock;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import javax.enterprise.context.ApplicationScoped;
@Mock
@ApplicationScoped
@RestClient
public class MockHeroService implements HeroService {
public static final String DEFAULT_HERO_NAME = "Super Baguette";
public static final String DEFAULT_HERO_PICTURE = "super_baguette.png";
public static final String DEFAULT_HERO_POWERS = "eats baguette really quickly";
public static final int DEFAULT_HERO_LEVEL = 42;
@Override
public Hero findRandomHero() {
Hero hero = new Hero();
hero.name = DEFAULT_HERO_NAME;
hero.picture = DEFAULT_HERO_PICTURE;
hero.powers = DEFAULT_HERO_POWERS;
hero.level = DEFAULT_HERO_LEVEL;
return hero;
}
}
Call to action
Do the same for the MockVillainService
:
package io.quarkus.workshop.superheroes.fight.client;
import io.quarkus.test.Mock;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import javax.enterprise.context.ApplicationScoped;
@Mock
@ApplicationScoped
@RestClient
public class MockVillainService implements VillainService {
public static final String DEFAULT_VILLAIN_NAME = "Super Chocolatine";
public static final String DEFAULT_VILLAIN_PICTURE = "super_chocolatine.png";
public static final String DEFAULT_VILLAIN_POWERS = "does not eat pain au chocolat";
public static final int DEFAULT_VILLAIN_LEVEL = 42;
@Override
public Villain findRandomVillain() {
Villain villain = new Villain();
villain.name = DEFAULT_VILLAIN_NAME;
villain.picture = DEFAULT_VILLAIN_PICTURE;
villain.powers = DEFAULT_VILLAIN_POWERS;
villain.level = DEFAULT_VILLAIN_LEVEL;
return villain;
}
}
Call to action
Finally, edit the FightResourceTest
and add the following method:
import io.quarkus.workshop.superheroes.fight.client.MockHeroService;
import io.quarkus.workshop.superheroes.fight.client.MockVillainService;
//....
@Test
void shouldGetRandomFighters() {
given()
.when().get("/api/fights/randomfighters")
.then()
.statusCode(OK.getStatusCode())
.header(CONTENT_TYPE, APPLICATION_JSON)
.body("hero.name", Is.is(MockHeroService.DEFAULT_HERO_NAME))
.body("hero.picture", Is.is(MockHeroService.DEFAULT_HERO_PICTURE))
.body("hero.level", Is.is(MockHeroService.DEFAULT_HERO_LEVEL))
.body("villain.name", Is.is(MockVillainService.DEFAULT_VILLAIN_NAME))
.body("villain.picture", Is.is(MockVillainService.DEFAULT_VILLAIN_PICTURE))
.body("villain.level", Is.is(MockVillainService.DEFAULT_VILLAIN_LEVEL));
}
}
You would need the following import statements:
|
Running and Testing the Application
Call to action
First, make sure the tests pass by executing the command ./mvnw test
(or from your IDE).
Now that the tests are green, we are ready to run our application.
Use ./mvnw compile quarkus:dev
to start it.
Once the application is started, go to the corresponding url ($(oc get route -n USERNAME-codeready | grep 8080 | awk '{ print $2 }')
) and start fighting (finally !).
Containers & Cloud (optional)
This chapter explores how you can deploy Quarkus applications in containers and Cloud platforms. There are many approaches to achieve these deployments. In this chapter, we are focusing on the creation of containers using Quarkus java executables and the deployment of our system in OpenShift using the Quarkus OpenShift extension.
First, add the OpenShift extension running the following command in the rest-hero directory:
./mvnw quarkus:add-extension -Dextensions="openshift"
Some properties need to be added to the application.properties
file of each microservice. Edit the file and add:
quarkus.kubernetes-client.trust-certs=true
quarkus.kubernetes-client.namespace=${PROJECT_NAME:USERNAME-heroes}
quarkus.openshift.route.expose=true
The expose parameter’s purpose is to expose the service to clients outside the cluster via Route
.
Make sure you change USERNAME with your own |
Build and Deploy in the OpenShift cluster
Generate the OpenShift descriptors by running the following command:
./mvnw clean package
The descriptors will appear in the target/kubernetes
folder as openshift.json
and openshift.yml
and they will contain the OpenShift resources needed to install the microservice in the OpenShift cluster.
To deploy your microservice in the cluster, make sure you have access to it, and you are using the USERNAME-heroes project:
$ oc login
$ oc project USERNAME-heroes
Then, apply the openshift.yml
descriptor as follows:
oc apply -f target/kubernetes/openshift.yml
You should see a few resources have been created:
[user@workspace57dbnhjoas24i47a rest-hero]$ oc apply -f target/kubernetes/openshift.yml
service/rest-hero created
imagestream.image.openshift.io/openjdk-11 created
imagestream.image.openshift.io/rest-hero created
buildconfig.build.openshift.io/rest-hero created
deploymentconfig.apps.openshift.io/rest-hero created
route.route.openshift.io/rest-hero created
We will trigger now the image build using s2i for packaging our microservice into a container image runnable by OpenShift.
For the rest-hero
microservice, this can be done with the following command:
oc start-build rest-hero --from-dir=target/quarkus-app
Uploading directory "target/quarkus-app" as binary input for the build ...
...
Uploading finished
build.build.openshift.io/rest-hero-1 started
The aforementioned command will trigger a container image build. To confirm the OpenShift resources have been created and the application deployed (has a pod running), you can run these commands:
oc get is
oc get pods
oc get svc
Go to the console and note that a new application appears in the Topology
view. Click in the Open URL
icon to access it via a browser:
Build and Deploy (in a single step)
To trigger the build and deployment in a single step, run the following command. Make sure that you are logged and using the USERNAME-heroes
project.
$ oc login
$ oc project USERNAME-heroes
$ ./mvnw clean package -Dquarkus.kubernetes.deploy=true
The aforementioned command will build a jar file locally, trigger a container image build and then apply the generated OpenShift resources. To confirm the above command has created an image stream, a service resource and has deployed the application (has a pod running), you can run these commands:
oc get is
oc get pods
oc get svc
If everything went well the URL of the microservice should be logged in the console where you ran the build&deploy command:
[INFO] [io.quarkus.container.image.openshift.deployment.OpenshiftProcessor] Push successful
[INFO] [io.quarkus.kubernetes.deployment.KubernetesDeployer] Deploying to openshift server: https://api.cluster-2599.2599.example.opentlc.com:6443/ in namespace: user2-heroes.
[INFO] [io.quarkus.kubernetes.deployment.KubernetesDeployer] Applied: Service rest-hero.
[INFO] [io.quarkus.kubernetes.deployment.KubernetesDeployer] Applied: ImageStream openjdk-11.
[INFO] [io.quarkus.kubernetes.deployment.KubernetesDeployer] Applied: ImageStream rest-hero.
[INFO] [io.quarkus.kubernetes.deployment.KubernetesDeployer] Applied: BuildConfig rest-hero.
[INFO] [io.quarkus.kubernetes.deployment.KubernetesDeployer] Applied: DeploymentConfig rest-hero.
[INFO] [io.quarkus.kubernetes.deployment.KubernetesDeployer] Applied: Route rest-hero.
[INFO] [io.quarkus.kubernetes.deployment.KubernetesDeployer] The deployed application can be accessed at: https://rest-hero-user2-heroes.apps.cluster-2599.2599.example.opentlc.com
[INFO] [io.quarkus.deployment.QuarkusAugmentor] Quarkus augmentation completed in 86106ms
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 02:39 min
[INFO] Finished at: 2020-11-11T12:57:24Z
[INFO] ------------------------------------------------------------------------
Deploying the others microservices
For villain microservice follow the same approach.
Fight microservice
-
Add the OpenShift extension running the following command in the rest-fights directory:
./mvnw quarkus:add-extension -Dextensions="openshift"
-
Add the following properties in the rest-fights
application.properties
file:
quarkus.kubernetes-client.trust-certs=true
quarkus.kubernetes-client.namespace=${PROJECT_NAME:USERNAME-heroes}
quarkus.openshift.route.expose=true
-
Also, we need to configure the new locations of the hero and villain microservice. Edit the
application.properties
file and modify the following properties:
io.quarkus.workshop.superheroes.fight.client.HeroService/mp-rest/url=http://rest-hero:8083
io.quarkus.workshop.superheroes.fight.client.VillainService/mp-rest/url=http://rest-villain:8084
Trigger the build and deployment, run the following command. Make sure that you are logged and using the USERNAME-heroes
project.
$ oc login
$ oc project USERNAME-heroes
$ ./mvnw clean package -Dquarkus.kubernetes.deploy=true
UI microservice
-
Add the OpenShift extension running the following command in the rest-fights directory:
./mvnw quarkus:add-extension -Dextensions="openshift"
-
Add the following properties in the ui-super-heroes
application.properties
file:
quarkus.kubernetes-client.trust-certs=true
quarkus.kubernetes-client.namespace=${PROJECT_NAME:USERNAME-heroes}
quarkus.openshift.route.expose=true
quarkus.openshift.env.vars.BASE_PATH=${FIGHT_ROUTE}
The BASE_PATH
property make the UI able to contact the fight microservice. As it depends on an env var, consider to define it using the following command:
$ export FIGHT_ROUTE=https://$(oc get route -n USERNAME-heroes | grep fight | awk '{ print $2 }')
Don’t forget use your own user |
Trigger the build and deployment, run the following command. Make sure that you are logged and using the USERNAME-heroes
project.
$ oc login
$ oc project USERNAME-heroes
$ ./mvnw clean package -Dquarkus.kubernetes.deploy=true
Once everything is configured and deployed, your system is now running on OpenShift.