Kickstart your first Quarkus application

Quarkus is a container-first framework for building cloud native applications.

Getting Started

First of all, make you have the following software installed.

  • Apache Maven 3.5+
  • GraalVM to build native image
  • Docker
  • Artifact: demo
  • Extensions: RESTEasy JAX-RS
Image for post
Image for post
@Path("/hello")
public class GreetingResource {
@Inject
GreetingService service;
@GET
@Produces(MediaType.TEXT_PLAIN)
@Path("/greeting/{name}")
public String greeting(@PathParam("name") String name) {
return service.greet(name);
}
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "hello";
}
}
import javax.enterprise.context.ApplicationScoped;@ApplicationScoped
public class GreetingService {
public String greet(String name) {
return "Hello " + name+"!";
}
}
mvn clean package
>java -jar target\demo-1.0.0-SNAPSHOT-runner.jar
mvn compile quarkus:dev
[INFO] --- quarkus-maven-plugin:1.0.0.CR1:dev (default-cli) @ demo ---
Listening for transport dt_socket at address: 5005
2019-09-06 11:39:08,764 INFO [io.qua.dep.QuarkusAugmentor] (main) Beginning quarkus augmentation
2019-09-06 11:39:09,560 INFO [io.qua.dep.QuarkusAugmentor] (main) Quarkus augmentation completed in 796ms
2019-09-06 11:39:10,072 INFO [io.quarkus] (main) Quark1.0.0.CR11.1 started in 1.464s. Listening on: http://[::]:8080
2019-09-06 11:39:10,072 INFO [io.quarkus] (main) Installed features: [cdi, resteasy]
>curl http://localhost:8080/hello
hello
<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>
@QuarkusTest
public class GreetingResourceTest {
@Test
public void testHelloEndpoint() {
given()
.when().get("/hello")
.then()
.statusCode(200)
.body(is("hello"));
}
@Test
public void testGreetingEndpoint() {
String uuid = UUID.randomUUID().toString();
given()
.pathParam("name", uuid)
.when().get("/hello/greeting/{name}")
.then()
.statusCode(200)
.body(is("Hello " + uuid + "!"));
}
}
@ExtendWith(QuarkusTestExtension.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface QuarkusTest {
}
@Transactional
@Stereotype
@QuarkusTest
public @interface TransactionalQuarkusTest{}

Building RESTful APIs

Let’s assume this simple blog system should satisfy the following requirements:

  • A user can post an new blog entry
  • A user can view all comments on the posts
  • A user can write comments on the posts
  • An admin role can moderate the post and delete the post
Image for post
Image for post
API desgin overview
public class Post implements Serializable {    String id;
String title;
String content;
LocalDateTime createdAt;
public static Post of(String title, String content) {
Post post = new Post();
post.setId(UUID.randomUUID().toString());
post.setCreatedAt(LocalDateTime.now());
post.setTitle(title);
post.setContent(content);
return post;
}
//getters and setters
}
@ApplicationScoped
public class PostRepository {
static Map<String, Post> data = new ConcurrentHashMap<>(); public List<Post> all() {
return new ArrayList<>(data.values());
}
public Post getById(String id) {
return data.get(id);
}
public Post save(Post post) {
data.put(post.getId(), post);
return post;
}
public void deleteById(String id) {
data.remove(id);
}
}
@Path("/posts")
@RequestScoped
public class PostResource {
private final static Logger LOGGER = Logger.getLogger(PostResource.class.getName());
private final PostRepository posts; @Context
ResourceContext resourceContext;
@Context
UriInfo uriInfo;
@Inject
public PostResource(PostRepository posts) {
this.posts = posts;
}
@GET
@Produces(MediaType.APPLICATION_JSON)
public Response getAllPosts() {
return ok(this.posts.all()).build();
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response savePost(@Valid Post post) {
Post saved = this.posts.save(Post.of(post.getTitle(), post.getContent()));
return created(
uriInfo.getBaseUriBuilder()
.path("/posts/{id}")
.build(saved.getId())
).build();
}
@Path("{id}")
@GET
@Produces(MediaType.APPLICATION_JSON)
public Response getPostById(@PathParam("id") final String id) {
Post post = this.posts.getById(id);
return ok(post).build();
}
@Path("{id}")
@PUT
@Consumes(MediaType.APPLICATION_JSON)
public Response updatePost(@PathParam("id") final String id, @Valid Post post) {
Post existed = this.posts.getById(id);
existed.setTitle(post.getTitle());
existed.setContent(post.getContent());
Post saved = this.posts.save(existed);
return noContent().build();
}
@Path("{id}")
@DELETE
public Response deletePost(@PathParam("id") final String id) {
this.posts.deleteById(id);
return noContent().build();
}
}
@ApplicationScoped
public class AppInitializer {
private final static Logger LOGGER = Logger.getLogger(AppInitializer.class.getName());
@Inject
private PostRepository posts;
void onStart(@Observes StartupEvent ev) {
LOGGER.info("The application is starting...");
Post first = Post.of("Hello Quarkus", "My first post of Quarkus");
Post second = Post.of("Hello Again, Quarkus", "My second post of Quarkus");
this.posts.save(first);
this.posts.save(second);
}
void onStop(@Observes ShutdownEvent ev) {
LOGGER.info("The application is stopping...");
}
}
mvn quarkus:dev
$ curl http://localhost:8080/posts
Could not find MessageBodyWriter for response object of type: java.util.ArrayList of media type: application/json
> mvn quarkus:list-extensions
...
Current Quarkus extensions available:
Agroal - Database connection pool quarkus-agroal
Amazon DynamoDB quarkus-amazon-dynamodb
Apache Kafka Client quarkus-kafka-client
Apache Kafka Streams quarkus-kafka-streams
Apache Tika quarkus-tika
Arc quarkus-arc
AWS Lambda quarkus-amazon-lambda
Flyway quarkus-flyway
Hibernate ORM quarkus-hibernate-orm
Hibernate ORM with Panache quarkus-hibernate-orm-panache
Hibernate Search + Elasticsearch quarkus-hibernate-search-elasticsearch
Hibernate Validator quarkus-hibernate-validator
Infinispan Client quarkus-infinispan-client
JDBC Driver - H2 quarkus-jdbc-h2
JDBC Driver - MariaDB quarkus-jdbc-mariadb
JDBC Driver - PostgreSQL quarkus-jdbc-postgresql
Jackson quarkus-jackson
JSON-B quarkus-jsonb
JSON-P quarkus-jsonp
Keycloak quarkus-keycloak
Kogito quarkus-kogito
Kotlin quarkus-kotlin
Kubernetes quarkus-kubernetes
Kubernetes Client quarkus-kubernetes-client
Mailer quarkus-mailer
MongoDB Client quarkus-mongodb-client
Narayana JTA - Transaction manager quarkus-narayana-jta
Neo4j client quarkus-neo4j
Reactive PostgreSQL Client quarkus-reactive-pg-client
RESTEasy quarkus-resteasy
RESTEasy - JSON-B quarkus-resteasy-jsonb
RESTEasy - Jackson quarkus-resteasy-jackson
Scheduler quarkus-scheduler
Security quarkus-elytron-security
Security OAuth2 quarkus-elytron-security-oauth2
SmallRye Context Propagation quarkus-smallrye-context-propagation
SmallRye Fault Tolerance quarkus-smallrye-fault-tolerance
SmallRye Health quarkus-smallrye-health
SmallRye JWT quarkus-smallrye-jwt
SmallRye Metrics quarkus-smallrye-metrics
SmallRye OpenAPI quarkus-smallrye-openapi
SmallRye OpenTracing quarkus-smallrye-opentracing
SmallRye Reactive Streams Operators quarkus-smallrye-reactive-streams-operators
SmallRye Reactive Type Converters quarkus-smallrye-reactive-type-converters
SmallRye Reactive Messaging quarkus-smallrye-reactive-messaging
SmallRye Reactive Messaging - Kafka Connector quarkus-smallrye-reactive-messaging-kafka
SmallRye Reactive Messaging - AMQP Connector quarkus-smallrye-reactive-messaging-amqp
REST Client quarkus-rest-client
Spring DI compatibility layer quarkus-spring-di
Spring Web compatibility layer quarkus-spring-web
Swagger UI quarkus-swagger-ui
Undertow quarkus-undertow
Undertow WebSockets quarkus-undertow-websockets
Eclipse Vert.x quarkus-vertx
>mvn quarkus:add-extension -Dextension=resteasy-jsonb
...
[INFO] --- quarkus-maven-plugin:1.0.0.CR1:add-extension (default-cli) @ demo ---
? Adding extension io.quarkus:quarkus-resteasy-jsonb
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-jsonb</artifactId>
</dependency>
> curl http://localhost:8080/posts
[{"content":"My second post of Quarkus","createdAt":"2019-09-06T16:48:03.0799469","id":"8059eb1d-5d26-4eb3-b9df-39151a5f71f5","title":"Hello Again, Quarkus"},{"content":"My first post of Quarkus","createdAt":"2019-09-06T16:48:03.0799469","id":"96974c19-cdca-4d34-a589-68728cf1e2ed","title":"Hello Quarkus"}]
public class Comment implements Serializable {
private String id;
private String post;
private String content;
private LocalDateTime createdAt;
public static Comment of(String postId, String content) {
Comment comment = new Comment();
comment.setId(UUID.randomUUID().toString());
comment.setContent(content);
comment.setCreatedAt(LocalDateTime.now());
comment.setPost(postId);
return comment;
}
//getters and setters

}
@ApplicationScoped
public class CommentRepository {
static Map<String, Comment> data = new ConcurrentHashMap<>();
public List<Comment> all() {
return new ArrayList<>(data.values());
}
public Comment getById(String id) {
return data.get(id);
}
public Comment save(Comment comment) {
data.put(comment.getId(), comment);
return comment;
}
public void deleteById(String id) {
data.remove(id);
}
public List<Comment> allByPostId(String id) {
return data.values().stream().filter(c -> c.getPost().equals(id)).collect(toList());
}
}
@Unremovable
@RegisterForReflect
@RequestScoped
public class CommentResource {
private final static Logger LOGGER = Logger.getLogger(CommentResource.class.getName());
private final CommentRepository comments;
@Context
UriInfo uriInfo;
@Context
ResourceContext resourceContext;
@PathParam("id")
String postId;
@Inject
public CommentResource(CommentRepository commentRepository) {
this.comments = commentRepository;
}
@GET
public Response getAllComments() {
return ok(this.comments.allByPostId(this.postId)).build();
}
@POST
public Response saveComment(Comment commentForm) {
Comment saved = this.comments.save(Comment.of(this.postId, commentForm.getContent()));
return created(
uriInfo.getBaseUriBuilder()
.path("/posts/{id}/comments/{commentId}")
.build(this.postId, saved.getId())
).build();
}
}
public class PostResource{
//other methods are omitted here.

@Path("{id}/comments")
public CommentResource commentResource() {
return resourceContext.getResource(CommentResource.class);
}
}
$ curl -v -X POST  http://localhost:8080/posts/e1ddbe3d-2964-4b1e-8aa8-1d3be8e30c3c/comments  -H "Content-Type:application/json" -H "Accept:application/json" -d "{\"content\":\"test comment\"}"> POST /posts/e1ddbe3d-2964-4b1e-8aa8-1d3be8e30c3c/comments HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.65.0
> Content-Type:application/json
> Accept:application/json
> Content-Length: 26
>
< HTTP/1.1 201 Created
< Connection: keep-alive
< Location: http://localhost:8080/posts/e1ddbe3d-2964-4b1e-8aa8-1d3be8e30c3c/comments/a4c14681-cc5e-42b6-aba2-02a83263cd95
< Content-Length: 0
< Date: Sat, 07 Sep 2019 07:31:28 GMT
<
>curl http://localhost:8080/posts/e1ddbe3d-2964-4b1e-8aa8-1d3be8e30c3c/comments -H "Accept:application/json"
[{"content":"test comment","createdAt":"2019-09-07T15:31:28.306218","id":"a4c14681-cc5e-42b6-aba2-02a83263cd95","post":"e1ddbe3d-2964-4b1e-8aa8-1d3be8e30c3c"}]

Exception Handling and Bean Validation

JAX-RS provides exception handling with it’s built-in ExceptionMapper.

public Response getPostById(@PathParam("id") final String id) {
Post post = this.posts.getById(id);
if (post == null) {
throw new PostNotFoundException(id);
}
return ok(post).build();
}
public class PostNotFoundException extends RuntimeException {
public PostNotFoundException(String id) {
super("Post:" + id + " was not found!");
}
}
@Provider
public class PostNotFoundExceptionMapper implements ExceptionMapper<PostNotFoundException> {
@Override
public Response toResponse(PostNotFoundException exception) {
return status(Response.Status.NOT_FOUND).entity(exception.getMessage()).build();
}
}
> curl -v http://localhost:8080/posts/nonexisted> GET /posts/nonexisted HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.55.1
> Accept: */*
>
> < HTTP/1.1 404 Not Found
> < Connection: keep-alive
> < Content-Type: application/json
> < Content-Length: 30
> < Date: Fri, 06 Sep 2019 09:58:28 GMT
> <
> Post:nonexisted was not found!
> mvn quarkus:add-extension -Dextension=hibernate-validator
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-validator</artifactId>
</dependency>
public class CommentForm implements Serializable {    @NotEmpty
private String content;
public static CommentForm of(String content) {
CommentForm form= new CommentForm();
form.setContent(content);
return form;
}
//getters and setters
}
public Response saveComment(@Valid CommentForm commentForm) {...}
@Provider
public class BeanValidationExceptionMapper implements ExceptionMapper<ConstraintViolationException> {
@Override
public Response toResponse(ConstraintViolationException exception) {
Map<String, String> errors = new HashMap<>();
exception.getConstraintViolations()
.forEach(v -> {
errors.put(lastFieldName(v.getPropertyPath().iterator()), v.getMessage());
});
return status(Response.Status.BAD_REQUEST).entity(errors).build();
}
private String lastFieldName(Iterator<Path.Node> nodes) {
Path.Node last = null;
while (nodes.hasNext()) {
last = nodes.next();
}
return last.getName();
}
}
>$ curl -v -X POST  http://localhost:8080/posts/e1ddbe3d-2964-4b1e-8aa8-1d3be8e30c3c/comments  -H "Content-Type:application/json" -H "Accept:application/json" -d "{\"content\":\"\"}"> POST /posts/e1ddbe3d-2964-4b1e-8aa8-1d3be8e30c3c/comments HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.65.0
> Content-Type:application/json
> Accept:application/json
> Content-Length: 14
>
< HTTP/1.1 400 Bad Request
< Connection: keep-alive
< Content-Type: application/json
< Content-Length: 31
< Date: Sat, 07 Sep 2019 03:20:37 GMT
<
...
{"content":"must not be empty"}

Visualizing APIs with MP OpenAPI and SwaggerUI

The original Swagger schema was standardized as OpenAPI, and Microprofile brings it into Java EE by Microprofile OpenAPI Spec.

mvn quarkus:add-extension -Dextension=openapi
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>
> curl http://localhost:8080/openapi
---
openapi: 3.0.1
info:
title: Generated API
version: "1.0"
paths:
/hello:
get:
responses:
200:
description: OK
content:
text/plain:
schema:
$ref: '#/components/schemas/String'
/hello/async:
get:
responses:
200:
description: OK
content:
text/plain:
schema:
type: string
/hello/greeting/{name}:
get:
parameters:
- name: name
in: path
required: true
schema:
$ref: '#/components/schemas/String'
responses:
200:
description: OK
content:
text/plain:
schema:
$ref: '#/components/schemas/String'
/posts:
get:
responses:
200:
description: OK
content:
application/json: {}
post:
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Post'
responses:
200:
description: OK
content:
'*/*': {}
/posts/{id}:
get:
parameters:
- name: id
in: path
required: true
schema:
$ref: '#/components/schemas/String'
responses:
200:
description: OK
content:
application/json: {}
put:
parameters:
- name: id
in: path
required: true
schema:
$ref: '#/components/schemas/String'
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Post'
responses:
200:
description: OK
content:
'*/*': {}
delete:
parameters:
- name: id
in: path
required: true
schema:
$ref: '#/components/schemas/String'
responses:
200:
description: OK
content:
'*/*': {}
components:
schemas:
String:
type: string
Post:
type: object
properties:
content:
type: string
createdAt:
format: date-time
type: string
id:
type: string
title:
type: string
quarkus.smallrye-openapi.path=/swagger
@Operation(
summary = "Get all Posts",
description = "Get all posts"
)
@APIResponse(
responseCode = "200",
name = "Post list",
content = @Content(
mediaType = "application/json",
schema = @Schema(
type = SchemaType.ARRAY,
implementation = Post.class
)
)
)
public Response getAllPosts() {...}
/posts:
get:
summary: Get all Posts
description: Get all posts
responses:
200:
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Post'
Image for post
Image for post
quarkus.swagger-ui.path=/my-custom-path
quarkus.swagger-ui.always-include=true

Data Persistence with JPA

Currently we are using a dummy Repository for retrieving and saving data. We will change it to use a real database. Several RDBMS and NoSQL products get support in Quarkus. Here we are focusing on RDBMS, we will discuss NoSQL support in future.

  • hibernate-orm — Hibernate is de facto JPA implementation.
  • hibernate-orm-panache — Provides more fluent APIs for JPA operations.
  • narayana-jta — Integrates JTA transaction supports, and you can use CDI @Transactional.
mvn quarkus:add-extension -Dextension=hibernate-orm-panache
  • PostgreSQL — jdbc-postgresql
  • MariaDB (and MySQL) — jdbc-mariadb
  • Microsoft SQL Server — jdbc-mssql
mvn quarkus:add-extension -Dextension=jdbc-postgresql
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>
  • PanacheRepository is some like the Repository from Spring Data and Apache Deltaspike, it is also a good match with the DDD Repository concept.
@Entity
public class Post implements Serializable {
@Id
@GeneratedValue(generator = "uuid")
@GenericGenerator(name = "uuid", strategy = "uuid2")
String id;

// other codes are not changed.
}
@ApplicationScoped
public class PostRepository implements PanacheRepositoryBase<Post, String> {...}
public List<Post> findAllPosts() {
return this.listAll(Sort.descending("createdAt"));
}
public List<Post> findByKeyword(String q, int offset, int size) {
if (q == null || q.trim().isEmpty()) {
return this.findAll(Sort.descending("createdAt"))
.page(offset / size, size)
.list();
} else {
return this.find("title like ?1 or content like ?1", Sort.descending("createdAt"), '%' + q + '%')
.page(offset / size, size)
.list();
}
}
public Optional<Post> getById(String id) {
Post post = null;
try {
post = this.find("id=:id", Parameters.with("id", id)).singleResult();
} catch (NoResultException e) {
e.printStackTrace();
}
return Optional.ofNullable(post);
}
@Transactional
public Post save(Post post) {
EntityManager em = JpaOperations.getEntityManager();
if (post.getId() == null) {
em.persist(post);
return post;
} else {
return em.merge(post);
}
}
@Transactional
public void deleteById(String id) {
this.delete("id=?1", id);
}
  • Dynamic query methods defined by conventions, #3966
  • Type-safe query via JPA Criteria APIs, #3965
  • Support Optional as query result type , #3967
  • Support query by example, #4015
  • QueryDSL integration, #4016
  • AutoMapper support for the query result, #4017
version: '3.1' # specify docker-compose versionservices:
blogdb:
image: postgres
ports:
- "5432:5432"
restart: always
environment:
POSTGRES_PASSWORD: password
POSTGRES_DB: blogdb
POSTGRES_USER: user
volumes:
- ./data:/var/lib/postgresql
quarkus.datasource.url = jdbc:postgresql://localhost:5432/blogdb
quarkus.datasource.driver = org.postgresql.Driver
quarkus.datasource.username = user
quarkus.datasource.password = password

Testing APIs

Currently Quarkus just provides a very few number of APIs for writing test codes. The most important one is @QuarkusTest .

@QuarkusTest
public class PostResourceTest {
@Test
public void testPostsEndpoint() {
given()
.when().get("/posts")
.then()
.statusCode(200)
.body(
"$.size()", is(2),
"title", containsInAnyOrder("Hello Quarkus", "Hello Again, Quarkus"),
"content", containsInAnyOrder("My first post of Quarkus", "My second post of Quarkus")
);
}
}
@Test
public void getNoneExistedPost_shouldReturn404() {
given()
.when().get("/posts/nonexisted")
.then()
.statusCode(404);
}
>mvn clean compile test -Dtest=PostResourceTest
...
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 15.274 s - in com.example.PostResourceTest
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-h2</artifactId>
<scope>test</scope>
</dependency>
quarkus.datasource.url=jdbc:h2:tcp://localhost/mem:test
quarkus.datasource.driver=org.h2.Driver
quarkus.hibernate-orm.database.generation = drop-and-create
quarkus.hibernate-orm.log.sql=true
[INFO] H2 database started in TCP server mode

Containerizing the Application

The most attractive feature provided in Quarkus is it provides the capability of building GraalVM compatible native image and run in a container environment.

./mvnw package -Pnative -Dnative-image.container-runtime=docker
docker built -f src/main/docker/Dockerfile.native - t hantsy/quarkus-post-service .
## Stage 1 : build with maven builder image with native capabilities
FROM quay.io/quarkus/centos-quarkus-maven:19.2.1 AS build
COPY src /usr/src/app/src
COPY pom.xml /usr/src/app
USER root
RUN chown -R quarkus /usr/src/app
USER quarkus
RUN mvn -f /usr/src/app/pom.xml -Pnative clean package -DskipTests
## Stage 2 : create the docker final image
FROM registry.access.redhat.com/ubi8/ubi-minimal
WORKDIR /work/
COPY --from=build /usr/src/app/target/*-runner /work/application
RUN chmod 775 /work
EXPOSE 8080
CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]
docker build -f src/main/docker/Dockerfile.multistage -t hantsy/quarkus-post-service .
docker run -i --rm -p 8080:8080 hantsy/quarkus-post-service
version: '3.1' # specify docker-compose versionservices:
blogdb:
...
post-service:
image: hantsy/quarkus-post-service
build:
context: ./post-service
dockerfile: src/main/docker/Dockerfile.multistage
environment:
QUARKUS_DATASOURCE_URL: jdbc:postgresql://blogdb:5432/blogdb
ports:
- "8080:8080" #specify ports forewarding
depends_on:
- blogdb
docker-compose up
post-service_1  | 2019-09-14 14:19:11,035 INFO  [io.quarkus] (main) Quarkus 1.0.0.CR1 started in 0.267s. Listening on: http://0.0.0.0:8080

Written by

Self-employed technical consultant, solution architect and full-stack developer

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store