Here are step by step notes for the live coding session I did during my Tools-In-Action session @ DevoxxUK 2023.
- scaffold sample project either at https://code.quarkus.io or use the Quarkus CLI to do the same thing by running the following command:
quarkus create app com.redhat.developers:devoxx-quarkus-love:1.0.1
-
got to project folder and start the Quarkus
dev mode
in either of the following ways- CLI:
quarkus dev
- Maven:
mvn quarkus:dev
- Maven Wrapper:
./mvnw quarkus:dev
- CLI:
-
after the app has been started in dev mode, in the terminal session press
w
to reach the default index page ord
to visit the DEV UI and inspect what's offered there (Extension Overview, Configuration, Continous Testing, Dev Services, ...)
- add movie class
import java.util.List;
public class Movie {
public String title;
public List<String> genre;
public Integer duration;
public Boolean released;
public Integer year;
}
- add mongodb panache extension
quarkus ext add mongodb-panache
- add database name to
application.properties
[SUCCESS] ✅ Extension io.quarkus:quarkus-mongodb-panache has been installed
quarkus.mongodb.database=devoxxuk
- active record pattern make data class a mongodb panache entity and annotate it accordingly
import java.util.List;
import io.quarkus.mongodb.panache.PanacheMongoEntity;
import io.quarkus.mongodb.panache.common.MongoEntity;
@MongoEntity(collection = "movies")
public class Movie extends PanacheMongoEntity {
public String title;
public List<String> genre;
public Integer duration;
public Boolean released;
public Integer year;
}
- OPTIONAL: use the repository pattern as alternative to active record
import io.quarkus.mongodb.panache.PanacheMongoRepository;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class MovieRepository implements PanacheMongoRepository<Movie> {
}
- for json (de)serialization purposes let's add an extension
quarkus ext add resteasy-reactive-jackson
[SUCCESS] ✅ Extension io.quarkus:quarkus-resteasy-reactive-jackson has been installed
MovieResource.java
with the active record pattern approach:
import java.net.URI;
import java.util.List;
import org.bson.types.ObjectId;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
@Path("api/movie")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class MovieResource {
@GET
public List<Movie> allMovies() {
return Movie.listAll();
}
@GET
@Path("{id}")
public Response getMovie(@PathParam("id") String id) {
var movie = Movie.findById(new ObjectId(id));
return movie != null
? Response.ok(movie).build()
: Response.status(Response.Status.NOT_FOUND).build();
}
@POST
public Response addMovie(Movie movie) {
movie.persist();
return Response.created(URI.create("/api/movies"+movie.id)).entity(movie).build();
}
}
- optional:
MovieResource.java
with the repository pattern approach:
import java.net.URI;
import java.util.List;
import org.bson.types.ObjectId;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
@Path("api/movie")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class MovieResource {
private MovieRepository repository;
public MovieResource(MovieRepository repository) {
this.repository = repository;
}
@GET
public List<Movie> getAllMovies() {
return repository.listAll();
}
@GET
@Path("{id}")
public Response getMovie(@PathParam("id") String id) {
var movie = repository.findById(new ObjectId(id));
return movie != null
? Response.ok(movie).build()
: Response.status(Response.Status.NOT_FOUND).build();
}
@POST
public Response addMovie(Movie movie) {
repository.persist(movie);
return Response.created(URI.create(("/api/movie/"+movie.id)))
.entity(movie).build();
}
}
- add sample data during application bootstrap with liquibase mongodb extension
quarkus ext add liquibase-mongodb
[SUCCESS] ✅ Extension io.quarkus:quarkus-liquibase-mongodb has been installed
- add
src/main/resources/mongo/import.xml
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
<changeSet id="1" author="hanspeter">
<ext:createCollection collectionName="movies"/>
<ext:insertMany collectionName="movies">
<ext:documents>
[
{
"title": "Grown Ups",
"genre": [
"Comedies"
],
"duration": 103,
"released": true,
"year": 2010
},
{
"title": "Dark Skies",
"genre": [
"Horror Movies",
"Sci-Fi & Fantasy"
],
"duration": 97,
"released": true,
"year": 2013
},
{
"title": "Paranoia",
"genre": [
"Thrillers"
],
"duration": 106,
"released": true,
"year": 2013
},
{
"title": "Ankahi Kahaniya",
"genre": [
"Dramas",
"Independent Movies",
"International Movies"
],
"duration": 111,
"released": true,
"year": 2021
},
{
"title": "The Father Who Moves Mountains",
"genre": [
"Dramas",
"International Movies",
"Thrillers"
],
"duration": 110,
"released": true,
"year": 2021
}
]
</ext:documents>
</ext:insertMany>
</changeSet>
</databaseChangeLog>
- add liquibase config props
quarkus.liquibase-mongodb.change-log=mongo/import.xml
quarkus.liquibase-mongodb.migrate-at-start=true
-
you can setup your free managed mongodb instance on atlas here https://www.mongodb.com/cloud/atlas/register
-
make sure that you set the database connection string for the prod profile because when running the application in a container in prod mode there are obviously no dev services available
# NOTE KEEP THE %prod. prefix so that you can continue to use the local containerized database when running quarkus in dev mode
%prod.quarkus.mongodb.connection-string=<YOUR_MONGODB_CONNECTION_STRING_HERE>
# if you want to load the sample data to the production database you have to keep the setting for liquibase like so
quarkus.liquibase-mongodb.migrate-at-start=true
# otherwise append a %dev prefix to only run the sample data initialization when running quarkus in dev mode
%dev.quarkus.liquibase-mongodb.migrate-at-start=true
- add JIB extension
quarkus ext add container-image-jib
[SUCCESS] ✅ Extension io.quarkus:quarkus-container-image-jib has been installed
- set
application.properties
config for fully qualified image name e.g.
#replace the fully-qualified image name above with your own specific one
quarkus.container-image.image=quay.io/hgrahsl/devoxx-quarkus-love:1.0.1
- build the container image for the application
quarkus image build jib
-
optional: push this image in case you plan to deploy it to a kubernetes that's not running of your local machine
-
you can push in various different ways using
docker push ...
orpodman push ...
commands or by using the quarkus CLI like so
quarkus image push
- add the quarkus minikube extension and build the project to generate yml manifests
quarkus ext add quarkus-minikube
[SUCCESS] ✅ Extension io.quarkus:quarkus-minikube has been installed
quarkus build --no-tests
-
explore the
target/kubernetes
folder where you should see auto-generated YAML manifests and briefly inspect theminikube.yml
-
this file contains a service + deployment for the containerized Quarkus application
-
once you pointed your kubernetes context to the targeted cluster run
quarkus deploy minikube
- the auto-generated yaml manifests will be applied to your kubernetes cluster and the app is deployed and running in a few moments
-> rename config in application.properties
e.g. by changing the image tag to end with -native
, otherwise if you keep the same name this would overwrite your original image that was previously created
quarkus.container-image.image=quay.io/hgrahsl/devoxx-quarkus-love:1.0.1-native
quarkus image build --native --no-tests -Dquarkus.native.container-build=true
- this process takes a while (usually between 2-3 min) and once it's successfully finished you can deploy the resulting container image with the native executable like before
quarkus deploy minikube
- add smallrye reactive messaging kafka
quarkus ext add smallrye-reactive-messaging-kafka
[SUCCESS] ✅ Extension io.quarkus:quarkus-smallrye-reactive-messaging-kafka has been installed
- active record pattern: modifiy the
Movie.java
class by adding the following static method to the class which randomly selects a movie from the database
import java.util.List;
import org.bson.Document;
import io.quarkus.mongodb.panache.PanacheMongoEntity;
import io.quarkus.mongodb.panache.common.MongoEntity;
@MongoEntity(collection = "movies")
public class Movie extends PanacheMongoEntity {
public String title;
public List<String> genre;
public Integer duration;
public Boolean released;
public Integer year;
public static Movie getRandomMovie() {
return mongoCollection().aggregate(
List.of(new Document("$sample",new Document("size",1L))),
Movie.class
).first();
}
}
- optional for the repository pattern: modify the
MovieRepository.java
class by adding the following method to randomly select a movie from the database
import org.bson.Document;
import io.quarkus.mongodb.panache.PanacheMongoRepository;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class MovieRepository implements PanacheMongoRepository<Movie> {
public Movie getRandomMovie() {
return mongoCollection().aggregate(
List.of(new Document("$sample",new Document("size",1L))),
Movie.class
).first();
}
}
-
add user activity simulator that generates random events that represent users watching movies:
-
with the active record pattern:
WatchersSimulator.java
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Random;
import org.eclipse.microprofile.reactive.messaging.Outgoing;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.quarkus.logging.Log;
import io.smallrye.mutiny.Multi;
import io.smallrye.reactive.messaging.kafka.KafkaRecord;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class WatchersSimulator {
final static List<String> USERNAMES = List.of("Alex","Natale","Cedric","Evan","Kevin","Ryan","Josh","Ian","Hans-Peter");
public static record WatcherData(
String movieId,
String username,
@JsonFormat(pattern="yyyy-MM-dd'T'HH:mm:ss.SSS")
LocalDateTime timestamp
) {};
@Outgoing("movie-watchers")
public Multi<KafkaRecord<String, WatcherData>> simulateRandomWatcherActivity() {
Log.info("starting to simulate random watchers activity every ... ms");
return Multi.createFrom().ticks().every(Duration.ofMillis(1000))
.map(t -> {
var movie = Movie.getRandomMovie();
var user = USERNAMES.get(new Random().nextInt(USERNAMES.size()));
Log.infov("randomly selected user ''{0}'' and movie ''{1}'' (id={2})",user,movie.title,movie.id);
return new WatcherData(movie.id.toHexString(),user,LocalDateTime.now());
})
.invoke(wd -> Log.infov("producing watcher data to kafka topic -> {0}", wd))
.map(wd -> KafkaRecord.of(wd.movieId,wd));
}
}
- optional with the repository pattern:
WatchersSimulator.java
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Random;
import org.eclipse.microprofile.reactive.messaging.Outgoing;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.quarkus.logging.Log;
import io.smallrye.mutiny.Multi;
import io.smallrye.reactive.messaging.kafka.KafkaRecord;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@ApplicationScoped
public class WatchersSimulator {
@Inject
MovieRepository repository;
final static List<String> USERNAMES = List.of("Alex","Natale","Cedric","Evan","Kevin","Ryan","Josh","Ian","Hans-Peter");
public static record WatcherData(
String movieId,
String username,
@JsonFormat(pattern="yyyy-MM-dd'T'HH:mm:ss.SSS")
LocalDateTime timestamp
) {};
@Outgoing("movie-watchers")
public Multi<KafkaRecord<String, WatcherData>> simulateRandomWatcherActivity() {
Log.info("starting to simulate random watchers activity every ... ms");
return Multi.createFrom().ticks().every(Duration.ofMillis(1000))
.map(t -> {
var movie = repository.getRandomMovie();
var user = USERNAMES.get(new Random().nextInt(USERNAMES.size()));
Log.infov("randomly selected user ''{0}'' and movie ''{1}'' (id={2})",user,movie.title,movie.id);
return new WatcherData(movie.id.toHexString(),user,LocalDateTime.now());
})
.invoke(wd -> Log.infov("producing watcher data to kafka topic -> {0}", wd))
.map(wd -> KafkaRecord.of(wd.movieId,wd));
}
}
-
finally start quarkus in dev mode using
quarkus dev
which should now launch 2 dev service backed containers in the background:- the mongodb database just like before
- redpanda as event streaming platform that is kafka API compatible
-
if you follow the logs you see the randomly generated user activity written to a kafka topic
...
2023-05-12 15:12:53,251 INFO [com.red.dev.WatchersSimulator] (executor-thread-1) randomly selected user 'Josh' and movie 'System Crasher' (id=645e3b54c25410612e120423)
2023-05-12 15:12:53,253 INFO [com.red.dev.WatchersSimulator] (executor-thread-1) producing watcher data to kafka topic -> WatcherData[movieId=645e3b54c25410612e120423, username=Josh, timestamp=2023-05-12T15:12:53.253174]
2023-05-12 15:12:54,253 INFO [com.red.dev.WatchersSimulator] (executor-thread-1) randomly selected user 'Alex' and movie 'Breaking Free' (id=645e3b54c25410612e120d2a)
2023-05-12 15:12:54,255 INFO [com.red.dev.WatchersSimulator] (executor-thread-1) producing watcher data to kafka topic -> WatcherData[movieId=645e3b54c25410612e120d2a, username=Alex, timestamp=2023-05-12T15:12:54.255846]
2023-05-12 15:12:55,283 INFO [com.red.dev.WatchersSimulator] (executor-thread-1) randomly selected user 'Elder' and movie 'How to Change the World' (id=645e3b54c25410612e120f1b)
2023-05-12 15:12:55,284 INFO [com.red.dev.WatchersSimulator] (executor-thread-1) producing watcher data to kafka topic -> WatcherData[movieId=645e3b54c25410612e120f1b, username=Elder, timestamp=2023-05-12T15:12:55.284188]
2023-05-12 15:12:56,275 INFO [com.red.dev.WatchersSimulator] (executor-thread-1) randomly selected user 'Josh' and movie 'Avicii: True Stories' (id=645e3b54c25410612e120ca0)
2023-05-12 15:12:56,275 INFO [com.red.dev.WatchersSimulator] (executor-thread-1) producing watcher data to kafka topic -> WatcherData[movieId=645e3b54c25410612e120ca0, username=Josh, timestamp=2023-05-12T15:12:56.275932]
2023-05-12 15:12:57,280 INFO [com.red.dev.WatchersSimulator] (executor-thread-1) randomly selected user 'Ian' and movie 'Chris D'Elia: Man on Fire' (id=645e3b54c25410612e120a4a)
2023-05-12 15:12:57,282 INFO [com.red.dev.WatchersSimulator] (executor-thread-1) producing watcher data to kafka topic -> WatcherData[movieId=645e3b54c25410612e120a4a, username=Ian, timestamp=2023-05-12T15:12:57.281942]
...