Quarkus Tutorial

Quarkus Tutorial: Building Ultra-Speed Applications with Java’s Revolutionary Framework – Part 1

When it comes to building apps, Quarkus is the spark that lights the bonfire. It’s not about coding faster, it’s about supercharging your Java!

Welcome!

We’re thrilled to have you here with Quarkus Tutorial. First and foremost, let’s embark on an exciting journey of creating applications using Quarkus. As we dive into this process, remember that you’re not alone. Here, we will guide you every step of the way.

To start with, Quarkus is a revolutionary tool in the world of Java. Next, you will see how it brings speed, flexibility, and fun back into the process of app development. After this, we’ll dive into the nitty-gritty of coding with Quarkus, and you will surely find it engaging. Following this, you’ll see how the applications you build come alive in real-time, which is truly remarkable.

Moreover, we’ve arranged plenty of practical examples for you to practice with. Ultimately, our goal is to ensure that by the end of this journey, you feel confident and ready to take on any challenge with Quarkus. So, let’s get started!

This blog series will be broken down into the following sections.

  1. Introduction to Quarkus: Get acquainted with this revolutionary Java framework and understand why it’s becoming the go-to choice for developers.
  2. Setting up Your Development Environment: Learn how to install and configure Quarkus on your system, setting the stage for exciting projects ahead.
  3. Creating Your First Quarkus Application: Dive into the practical aspects of using Quarkus. From the first line of code to running your first app, we’ll guide you through it all.
  4. Exploring Quarkus Features: Understand the unique features that make Quarkus stand out, such as live coding, native compilation, and seamless integration with Kubernetes.
  5. Deep Dive into Quarkus Extensions: Discover the rich ecosystem of Quarkus extensions that can enhance your app’s functionality and take it to the next level.
  6. Building a Real-World Application: Put your newly acquired skills to test by building a fully functional application using Quarkus.
  7. Optimizing Your Quarkus Application: Learn how to utilize Quarkus’s full potential by optimizing your applications for speed, memory, and resource efficiency.
  8. Deploying Your Quarkus Application: Understand the deployment process and learn how to launch your Quarkus app on various platforms.
  9. Troubleshooting and Best Practices: Learn common issues and their solutions, along with recommended best practices to ensure your app runs smoothly.
  10. Conclusion and Next Steps: Reflect on what you’ve learned and plan your next steps in your Quarkus journey.

This comprehensive guide is designed to provide a thorough understanding of Quarkus, preparing you to build fast, efficient, and modern applications. Let’s begin this exciting journey!

Introduction to Quarkus

Quarkus Tutorial

Welcome to the first step of our journey: the introduction to Quarkus.

Quarkus is an innovative Kubernetes-native Java stack tailored for GraalVM and OpenJDK HotSpot. Crafted from the best-of-breed Java libraries and standards, Quarkus aims to help you create supremely fast, efficient, and lightweight applications. It’s not just another entry in the list of Java frameworks; Quarkus truly stands apart.

One of the fundamental selling points of Quarkus is its ability to significantly reduce the boot time of your applications, and their memory footprint. This is particularly beneficial for workloads on cloud and container-based environments where resources are often limited and expensive. By enabling you to write compact, fast-booting, and efficient applications, Quarkus has turned into a darling for developers, especially those working with microservices.

Quarkus is also designed for developer joy. Its live coding feature allows changes to be automatically and immediately reflected in your running application — no restarts required! This means you can quickly iterate, troubleshoot, and see the results of your work in real-time, making the development process more dynamic and fun.

Quarkus opens up the world of Java to the modern age of cloud, serverless, and containers. It is fully compatible with popular Java standards, libraries, and frameworks like Eclipse MicroProfile and Spring, allowing developers to leverage their existing knowledge while enjoying new efficiencies and capabilities. It’s like getting a sports car upgrade to your reliable old bicycle. Quarkus Tutorial.

Welcome to the second section of our Quarkus journey: setting up your development environment. A well-set environment is the cornerstone for efficient app development. This guide will walk you through the process of installing and configuring Quarkus on your system, thereby paving the way for your exciting projects. Let’s get started!

Setting up Your Development Environment for Quarkus:

Learn how to install and configure Quarkus on your system, setting the stage for exciting projects ahead.

Step 1: Installing Java

Quarkus is built with Java, so you need to have Java Development Kit (JDK) installed on your machine. As of now, Quarkus supports Java 11 and upwards. You can download and install the JDK from the official Oracle website or use OpenJDK.

Step 2: Installing Apache Maven

Apache Maven is a powerful project management tool that we’ll use to build our Quarkus applications. You can download Maven from the official Apache website and follow their installation instructions. Make sure you’ve set the environment variables correctly.

Step 3: Setting up your IDE

Quarkus does not impose any specific Integrated Development Environment (IDE). You can choose the one you are comfortable with, whether it’s Eclipse, IntelliJ IDEA, or Visual Studio Code. All these IDEs support Quarkus and its features like live coding.

Step 4: Installing Quarkus

Installing Quarkus doesn’t require any special steps as it gets downloaded automatically when we create a new Quarkus project. However, you can install the Quarkus CLI (Command Line Interface) for more convenience.

Use the following command to install Quarkus CLI:

curl -Ls https://sh.jbang.dev | bash -s - app install --fresh --force quarkus@quarkusio

You can verify your installation by checking the version of Quarkus with the following command:

quarkus --version

With these steps, your development environment should be all set for creating Quarkus applications.

Creating Your First Quarkus Application:

Dive into the practical aspects of using Quarkus. From the first line of code to running your first app, we’ll guide you through it all.

Welcome to the exciting part of our Quarkus journey: creating your first application. We will be developing ‘Agri-Assist’, a comprehensive tool that aids farmers with data-driven advice on crop selection, considering factors like market prices, weather patterns, and soil health. We will integrate ChatGPT for generating user-friendly, conversational advice. Let’s dive in!

Step 1: Creating the Quarkus Project

To create a new Quarkus project, we use the Quarkus Maven plugin from the command line:

mvn io.quarkus:quarkus-maven-plugin:3.1.1.Final:create 
-DprojectGroupId=org.agri
-DprojectArtifactId=agri-assist
-DclassName="org.agri.AdvisoryResource"
-Dpath="/advisory"

In the command above, the -D flag is used to specify various properties like the groupIdartifactId, the initial className and the path where our resource will be available.

This command will create a new directory named agri-assist containing our new Quarkus project.

Step 2: Exploring the Project Structure

Navigate into the agri-assist directory. You’ll find the following main folders and files:

  • src/main/java: This directory contains the Java source files.
  • src/main/resources: This directory contains resources such as configuration files.
  • src/test: This directory contains test files.
  • src/main/docker: This directory contains Dockerfiles for both JVM and native modes.
  1. Dockerfile.jvm: This Dockerfile is used to build a container image of your application in JVM mode. The resultant image is not as compact as the native one, but the build process is significantly faster.
  2. Dockerfile.native: This Dockerfile is used to build a container image of your application in native mode. The build process for this Dockerfile is slower, as it involves ahead-of-time compilation to a native binary. However, the resultant image is much smaller and starts faster.
  3. Dockerfile.legacy-jar: This Dockerfile is designed to handle “legacy” JARs, which are typical fat JARs containing the application and all its dependencies. If you choose to package your application as a fat JAR (using the quarkus.package.type=legacy-jar option), this Dockerfile will be used.
  4. Dockerfile.native-micro: This Dockerfile is used to create a minimal Docker image for your native executable. The output is based on the distroless/static base image from Google, which is smaller than typical images and contains only your native executable and the bare essentials to run it (such as CA certificates). This Dockerfile is used when your focus is on creating the smallest possible Docker image.

Our primary focus will be on src/main/java/org/agri/AdvisoryResource.java, where we’ll develop our advisory service.

Step 3: Developing the Advisory Service

In AdvisoryResource.java, let’s start by creating a simple REST endpoint that returns a hello message:

package org.agri;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("/advisory")
public class AdvisoryResource {

@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "Welcome to Agri-Assist!";
}
}

This simple RESTful API listens to GET requests at the /advisory endpoint and returns the text “Welcome to Agri-Assist!”.

As we progress, this class will be the home to our crop advisory logic, integrating factors like current market prices, weather patterns, and soil health, and utilizing the power of ChatGPT to deliver the advice in a conversational manner. Quarkus Tutorial.

Step 4: Running Your Application

Let’s run the application using the Quarkus Dev Mode for live coding:

./mvnw compile quarkus:dev

Now, open your browser and navigate to http://localhost:8080/advisory. You should see “Welcome to Agri-Assist!”.

Congratulations, you have successfully created and run your first Quarkus application!

Exploring Quarkus Features:

Understand the unique features that make Quarkus stand out, such as live coding, native compilation, unified configuration, its extensive ecosystem of extension and seamless integration with Kubernetes.

Welcome to the fourth section of our Quarkus journey: exploring Quarkus features. Quarkus comes packed with a multitude of features that sets it apart from traditional Java frameworks. From live coding to native compilation, unified configuration, extensive extension ecosystem, and seamless Kubernetes integration, Quarkus is redefining the Java landscape. Let’s explore these features in detail:

1. Live Coding: One of the most striking features of Quarkus is its live coding capability. With Quarkus Dev Mode, developers can make changes to their code and see them reflected instantly without needing to manually restart their server. This not only accelerates the development process but also makes it more dynamic and interactive.

2. Native Compilation: Quarkus supports ahead-of-time (AOT) compilation via GraalVM which allows your Java application to be compiled into a standalone native executable. This native image not only has faster startup times but also consumes less memory, making it ideal for containerized and serverless environments where these characteristics are highly desirable.

3. Unified Configuration: Quarkus uses the MicroProfile Config specification for its configuration. It provides a unified configuration system that works uniformly across all Quarkus extensions. This makes it easier to manage and consolidate configurations in one place, whether they come from application.properties files or external sources like environment variables and system properties.

In a Quarkus application (Quarkus Tutorial), most of your configuration options will be located in the application.properties file, which resides in the src/main/resources directory. Let’s consider a simple example:

# src/main/resources/application.properties
quarkus.datasource.db-kind = postgresql
quarkus.datasource.username = dbuser
quarkus.datasource.password = dbpass
quarkus.datasource.jdbc.url = jdbc:postgresql://localhost:5432/mydb

In this example, we’re configuring a PostgreSQL database for our Quarkus application. We specify the type of the database, the username, password, and the JDBC URL in our application.properties file.

However, you may not want to store sensitive information like database passwords in your application.properties file. Here’s where Quarkus’s unified configuration shines. You can easily override these properties with environment variables or system properties. Quarkus uses a unified configuration where the priority goes from environment variables, system properties, to application.properties file.

For instance, you could provide a value for the quarkus.datasource.password property via an environment variable like so:

export QUARKUS_DATASOURCE_PASSWORD=secret

Or, you could use a system property when starting your Quarkus application:

./mvnw compile quarkus:dev -Dquarkus.datasource.password=secret

In both cases, the quarkus.datasource.password value will be set to secret, overriding the value in the application.properties file.

This flexibility in managing configuration values is one of the key strengths of Quarkus. With the unified configuration system, you can easily adapt your application to different environments, making it ideal for the ever-changing landscapes of development, staging, and production environments. For Agri-Assist, this will allow us to handle different configurations for various environments seamlessly.

4. Extensive Ecosystem of Extensions: Quarkus boasts a rich ecosystem of extensions, including support for all popular Java standards, libraries, and frameworks. From RESTEasy and Hibernate for creating web services and handling databases, to Camel for integration and Kafka for messaging, these extensions make it easy to add capabilities to your Quarkus applications.

5. Seamless Integration with Kubernetes: Quarkus is designed with Kubernetes in mind. It offers features such as automatic generation of Kubernetes manifests and container images, making it easier than ever to deploy Quarkus applications on Kubernetes. It also works seamlessly with the wider Kubernetes ecosystem including OpenShift and Knative.

These are just some of the features that make Quarkus an appealing choice for modern Java development. In the context of our Agri-Assist application, these features will help us develop efficiently, optimize resource consumption, manage configurations effectively, and eventually deploy seamlessly in a Kubernetes environment.

Deep Dive into Quarkus Extensions:

Quarkus provides a wide array of extensions that simplifies the integration of popular libraries and frameworks in your application. We’ll learn how to add and use these extensions to bring additional capabilities to our Agri-Assist application.

Welcome to our deep dive into Quarkus Extensions! Quarkus offers a rich set of extensions, which are essentially modules that you can add to your project to integrate popular libraries and frameworks. These extensions have been carefully designed to work seamlessly with Quarkus, optimizing startup time, memory usage, and reducing the boilerplate code.

An extension can provide several things, such as automatic configuration, native mode support, and a simplified programming model. The result is a simplified and optimized development experience.

Let’s explore how to enhance our Agri-Assist application using Quarkus extensions. Imagine we want to incorporate a REST API into our application. In such a case, the Quarkus RESTEasy Reactive extension would be an ideal choice. This extension not only allows us to define our REST API, but it also provides an innovative paradigm for writing highly efficient REST services that are optimized for non-blocking I/O. Here’s how we can add it to our project:

./mvnw quarkus:add-extension -Dextensions="quarkus-resteasy-reactive"

This command integrates the RESTEasy Reactive extension into our project. We can now utilize RESTEasy Reactive’s features, using its annotations to construct our REST API. Quarkus takes care of all necessary configurations and optimizations, ensuring our API is reactive, robust, and scalable.

By using the Quarkus RESTEasy Reactive extension, we’re not only making our Agri-Assist application more feature-rich, but we’re also ensuring it’s built to handle high loads effectively.

Next, let’s say we need to connect to a PostgreSQL database. The Quarkus Agroal extension provides a simple and efficient connection pooling solution. To add this extension, we’d run:

./mvnw quarkus:add-extension -Dextensions="agroal"

With the Agroal extension, we can define our database connection in the application.properties file and use the @Inject annotation to obtain a reference to a DataSource in our code.

In addition to these, there are hundreds of other extensions for various tasks, like security, scheduling, messaging, and more. If you ever find yourself needing a certain feature, chances are there’s a Quarkus extension that can help you out.

These extensions allow Quarkus to provide an effective platform for microservices development in Java. With Agri-Assist, we’re going to make full use of these extensions to develop an application that’s feature-rich, efficient, and easy to maintain.

Building a Real-World Application:

We’ll further develop our Agri-Assist application by adding more features and making it more robust. We’ll learn how to structure our application effectively, and understand how Quarkus fits into the bigger picture of application development.

As we delve into the practicalities of building a real-world application with Quarkus, it’s time to further develop our Agri-Assist project. Our focus will not only be on adding more features but also ensuring that our application is robust and can handle real-world scenarios.

Firstly, we’ll consider the application structure. While structuring an application might seem straightforward, it’s an aspect that significantly influences the maintainability and scalability of our project. An effective application structure separates concerns, making it easier to locate relevant code and reducing the risk of errors that can occur from a tangled codebase.

In Quarkus, we typically structure our application around the domain model, separating code into meaningful layers. For instance, our Agri-Assist application might have separate packages for modelservice, and resource, each containing classes relevant to their role in the application.

Next, we’ll build out more features. Using Quarkus and its rich set of extensions, we’ll incorporate capabilities such as:

  1. Database Interaction: We’ll use Panache, a Quarkus extension, to make it easier to interact with our database, using entity classes and repositories.
  2. Security: We’ll explore how to secure our application using Quarkus’s security extensions, allowing us to authenticate and authorize users.
  3. Testing: Writing effective tests is crucial to ensure our application behaves as expected. We’ll use Quarkus’s built-in testing support to write unit and integration tests.
  4. Logging and Monitoring: Essential to maintaining a healthy application, we’ll integrate logging and monitoring into our application using appropriate Quarkus extensions.

By understanding how Quarkus fits into the bigger picture, we can effectively use its capabilities to facilitate application development. Quarkus’s design for container-first, cloud-native applications makes it an excellent choice for modern application development.

For our Agri-Assist application, we will start by setting up the basic REST endpoints and the model. Here’s a simple example to illustrate how this can be done.

To use the @Entity annotation in Quarkus, you would typically include the Quarkus Hibernate ORM extension, as Hibernate is the most commonly used JPA implementation. You can add this extension to your project with the following command:

./mvnw quarkus:add-extension -Dextensions="quarkus-hibernate-orm-panache"

The quarkus-hibernate-orm-panache extension is a layer on top of Hibernate ORM and JPA, and it simplifies database operations. It makes it easier to write the types of queries that are common in a lot of applications without needing a lot of boilerplate code.

Let’s consider that we are using a PostgreSQL database for our application.

First, add the PostgreSQL JDBC driver extension to your project with the following command:

./mvnw quarkus:add-extension -Dextensions="quarkus-jdbc-postgresql"

Next, you need to configure your database connection in your application.properties file. The settings you need to include are the database URL, the username, and the password:

# Quarkus Datasource Configuration
quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=your_username
quarkus.datasource.password=your_password
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/your_database_name

quarkus.hibernate-orm.database.generation=update

The above properties tell Quarkus to:

  1. Connect to a PostgreSQL database (quarkus.datasource.db-kind).
  2. Use the provided username and password to authenticate (quarkus.datasource.username and quarkus.datasource.password).
  3. Connect to the database at the specified URL (quarkus.datasource.jdbc.url).
  4. Use Hibernate ORM to automatically manage the database schema (quarkus.hibernate-orm.database.generation). The update value means Hibernate will update the schema whenever your application starts.

Remember to replace your_usernameyour_password, and your_database_name with your actual PostgreSQL username, password, and database name.

For other types of databases, the process would be similar but with different extensions and JDBC URLs. For example, if you were using MySQL, you would add the quarkus-jdbc-mysql extension and change the quarkus.datasource.db-kind and quarkus.datasource.jdbc.url accordingly.

Let’s create Entities:

Now that we’re going into more specific details, let’s take a look at how you could structure your Agri-Assist application with a Service-Repository pattern using Panache, DTOs, and mappers.

First, let’s create our Crop an entity:

package org.agri.agriassist.model;

import jakarta.persistence.*;

@Entity
public class Crop{

@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@Column(name = "id", nullable = false)
private Long id;
public String name;
public String type;
public String description;
}

Next, we create a Data Transfer Object (DTO). DTOs are used to transfer data between processes or API calls:

package org.agri.agriassist.dto;

public class CropDTO {
public Long id;
public String name;
public String type;
public String description;
}

We will also need a Mapper to map between our entity and DTO. This could be done manually, but using a library like MapStruct can make this easier and more robust:

package org.agri.agriassist.mapper;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Singleton;
import org.agri.agriassist.dto.CropDTO;
import org.agri.agriassist.model.Crop;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

@Mapper(componentModel = "cdi")
@ApplicationScoped
@Singleton
public interface CropMapper {

CropMapper INSTANCE = Mappers.getMapper(CropMapper.class);

CropDTO toDTO(Crop crop);
Crop toEntity(CropDTO cropDTO);
}

We use the @Mapper annotation to define our mapper and the methods toDTO and toEntity for conversion. MapStruct will automatically generate the implementation for us.

add the below dependency to use @Mapper from MapStruct.

<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.3.Final</version>
</dependency>

Next, let’s create a repository for our Crop entity:

package org.agri.agriassist.repository;

import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped;
import org.agri.agriassist.model.Crop;

@ApplicationScoped
public class CropRepository implements PanacheRepository<Crop> {
}

The PanacheRepository the interface provides methods for common database operations.

Finally, let’s create a service to handle the business logic for our Crop entity:

package org.agri.agriassist.service;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.agri.agriassist.dto.CropDTO;
import org.agri.agriassist.mapper.CropMapper;
import org.agri.agriassist.model.Crop;
import org.agri.agriassist.repository.CropRepository;

@ApplicationScoped
public class CropService {

@Inject
CropRepository cropRepository;


public CropDTO getCrop(Long id) {
Crop crop = cropRepository.findById(id);
return CropMapper.INSTANCE.toDTO(crop);
}
}

In this service, we inject the CropRepository and CropMapper, and we provide a method getCrop to retrieve a Crop by its ID and convert it to a CropDTO.

Of course, let’s flesh out the CropService with additional CRUD (Create, Read, Update, Delete) operations:

package org.agri.agriassist.service;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import org.agri.agriassist.dto.CropDTO;
import org.agri.agriassist.exceptions.CropNotFoundException;
import org.agri.agriassist.mapper.CropMapper;
import org.agri.agriassist.model.Crop;
import org.agri.agriassist.repository.CropRepository;

@ApplicationScoped
public class CropService {

@Inject
CropRepository cropRepository;


public CropDTO getCrop(Long id) throws CropNotFoundException {
Crop crop = cropRepository.findById(id);
if (crop == null) {
throw new CropNotFoundException("Crop with id " + id + " not found");
}
return CropMapper.INSTANCE.toDTO(crop);
}

@Transactional
public CropDTO addCrop(CropDTO cropDTO) {
Crop crop = CropMapper.INSTANCE.toEntity(cropDTO);
cropRepository.persist(crop);
return CropMapper.INSTANCE.toDTO(crop);
}

@Transactional
public CropDTO updateCrop(Long id, CropDTO cropDTO) throws CropNotFoundException {
Crop crop = cropRepository.findById(id);
if (crop == null) {
throw new CropNotFoundException("Crop with id " + id + " not found");
}
crop.name = cropDTO.name;
crop.type = cropDTO.type;
crop.description = cropDTO.description;
return CropMapper.INSTANCE.toDTO(crop);
}

@Transactional
public void deleteCrop(Long id) throws CropNotFoundException {
Crop crop = cropRepository.findById(id);
if (crop == null) {
throw new CropNotFoundException("Crop with id " + id + " not found");
}
cropRepository.delete(crop);
}
}

The @Transactional annotation is used to indicate that the method should be executed within a transaction. This means that either all the changes made within the method are applied to the database, or if an error occurs, none of them are. This ensures data consistency.

The addCrop method receives a CropDTO object, maps it to a Crop entity and persists it in the database, then returns the saved entity mapped back to a DTO.

The updateCrop method takes an id and a CropDTO object, finds the corresponding Crop entity in the database, updates its fields, and returns the updated entity mapped back to a DTO. If there is no Crop with the provided id, it should return null or handle the situation appropriately.

The deleteCrop method deletes the Crop entity corresponding to the given id from the database.

we’re throwing a CropNotFoundException when we can’t find a crop with the given id in getCropupdateCrop, and deleteCrop methods. We’re also declaring this exception in the method signatures with throws CropNotFoundException, which means that the calling code needs to handle this exception.

Here’s how you could define CropNotFoundException:

package org.agri.agriassist.exceptions;

public class CropNotFoundException extends Exception {

public CropNotFoundException(String message) {
super(message);
}
}

In this code, we’re mapping CropNotFoundException to a 404 Not Found HTTP response with the error message as the body.

Let’s include validation, to check that the given CropDTO is valid. Absolutely, validation is an essential part of any application. It helps ensure that the input received is correct and safe to be processed. In Java, we can use Bean Validation, which is a standard validation specification that can be used in Java applications.

To use it in Quarkus, add the Hibernate Validator extension, which is an implementation of Bean Validation, to your project with the following command:

./mvnw quarkus:add-extension -Dextensions="quarkus-hibernate-validator"

Next, let’s add some validation to our CropDTO :

package org.agri.agriassist.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public class CropDTO {

@NotBlank(message = "Name cannot be blank")
@Size(max = 200, message = "Name must be less than 200 characters")
public String name;

@NotBlank(message = "Type cannot be blank")
@Size(max = 100, message = "Type must be less than 100 characters")
public String type;

@Size(max = 1000, message = "Description must be less than 1000 characters")
public String description;
}

In this code, we’re using the @NotBlank annotation to validate that the name and type fields are not blank, and the @Size annotation to validate the maximum length of nametype, and description fields.

Next, let’s modify our CropService to validate the CropDTO before processing it:

package org.agri.agriassist.service;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import org.agri.agriassist.dto.CropDTO;
import org.agri.agriassist.exceptions.CropNotFoundException;
import org.agri.agriassist.mapper.CropMapper;
import org.agri.agriassist.model.Crop;
import org.agri.agriassist.repository.CropRepository;

@ApplicationScoped
public class CropService {

@Inject
CropRepository cropRepository;


public CropDTO getCrop(@NotNull Long id) throws CropNotFoundException {
// ...
}

@Transactional
public CropDTO addCrop(@Valid CropDTO cropDTO) {
// ...
}

@Transactional
public CropDTO updateCrop(@NotNull Long id, @Valid CropDTO cropDTO) throws CropNotFoundException {
// ...
}

@Transactional
public void deleteCrop(@NotNull Long id) throws CropNotFoundException {
// ...
}
}

In this code, we’re using the @Valid annotation to tell Hibernate Validator to validate CropDTO and the @NotNull annotation to validate that the id parameter is not null.

If a method is invoked with invalid parameters, a ConstraintViolationException will be thrown. You can handle this exception in a similar way to the CropNotFoundException using an exception mapper. Here’s an example:

package org.agri.agriassist.exceptions;

import jakarta.validation.ConstraintViolationException;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;

@Provider
public class ConstraintViolationExceptionMapper implements ExceptionMapper<ConstraintViolationException> {

@Override
public Response toResponse(ConstraintViolationException exception) {
return Response
.status(Response.Status.BAD_REQUEST)
.entity(exception.getMessage())
.build();
}
}

In this code, we’re mapping ConstraintViolationException to a 400 Bad

Here is the final version of CropService:

package org.agri.agriassist.service;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import org.agri.agriassist.dto.CropDTO;
import org.agri.agriassist.exceptions.CropNotFoundException;
import org.agri.agriassist.mapper.CropMapper;
import org.agri.agriassist.model.Crop;
import org.agri.agriassist.repository.CropRepository;

import java.util.Optional;

@ApplicationScoped
public class CropService {

@Inject
CropRepository cropRepository;


public CropDTO getCrop(@NotNull Long id) throws CropNotFoundException {
Optional<Crop> cropOptional = cropRepository.findByIdOptional(id);
if (cropOptional.isEmpty()) {
throw new CropNotFoundException("No Crop found with id = " + id);
}
return CropMapper.INSTANCE.toDTO(cropOptional.get());
}

@Transactional
public CropDTO addCrop(@Valid CropDTO cropDTO) {
Crop newCrop = cropMapper.toEntity(cropDTO);
cropRepository.persist(newCrop);
return CropMapper.INSTANCE.toDTO(newCrop);
}

@Transactional
public CropDTO updateCrop(@NotNull Long id, @Valid CropDTO cropDTO) throws CropNotFoundException {
Optional<Crop> cropOptional = cropRepository.findByIdOptional(id);
if (cropOptional.isEmpty()) {
throw new CropNotFoundException("No Crop found with id = " + id);
}

Crop existingCrop = cropOptional.get();
existingCrop.name = cropDTO.name;
existingCrop.type = cropDTO.type;
existingCrop.description = cropDTO.description;

return CropMapper.INSTANCE.toDTO(existingCrop);
}

@Transactional
public void deleteCrop(@NotNull Long id) throws CropNotFoundException {
Optional<Crop> cropOptional = cropRepository.findByIdOptional(id);
if (cropOptional.isEmpty()) {
throw new CropNotFoundException("No Crop found with id = " + id);
}
cropRepository.delete(cropOptional.get());
}
}

This version includes all CRUD operations — get, add, update, and delete for a Crop object, along with necessary error handling and validation. The CropService handles the business logic of these operations while delegating the persistence logic to the CropRepository and the conversion between Crop and CropDTO to the CropMapper.

RESTful API layer

Now that we have created the Service layer, the next step is to create the RESTful API layer or the Controller layer which exposes these services to the client. This layer is responsible for handling HTTP requests and responses.

Let’s create a CropResource class which will expose the RESTful API endpoints.

Here is an example of how you can create the RESTful API layer using Quarkus:

package org.agri.agriassist.resource;

import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.agri.agriassist.dto.CropDTO;
import org.agri.agriassist.exceptions.CropNotFoundException;
import org.agri.agriassist.service.CropService;

@Path("/crops")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class CropResource {

@Inject
CropService cropService;

@GET
@Path("/{id}")
public Response getCrop(@PathParam("id") Long id) {
try {
CropDTO crop = cropService.getCrop(id);
return Response.ok(crop).build();
} catch (CropNotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
}
}

@POST
public Response addCrop(CropDTO newCrop) {
CropDTO crop = cropService.addCrop(newCrop);
return Response.ok(crop).build();
}

@PUT
@Path("/{id}")
public Response updateCrop(@PathParam("id") Long id, CropDTO updatedCrop) {
try {
CropDTO crop = cropService.updateCrop(id, updatedCrop);
return Response.ok(crop).build();
} catch (CropNotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
}
}

@DELETE
@Path("/{id}")
public Response deleteCrop(@PathParam("id") Long id) {
try {
cropService.deleteCrop(id);
return Response.noContent().build();
} catch (CropNotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
}
}
}

In this CropResource class, the @Path annotation is used to define the path for the RESTful API. The @GET@POST@PUT, and @DELETE annotations are used to define the HTTP method for the respective methods.

The @Produces and @Consumes annotations are used to specify the Media Type of the HTTP request and response.

Also, we have exception handling in place, in case any CropNotFoundException is thrown from the service layer. We return an HTTP 404 status code along with a meaningful error message.

Next, you can test your application using a tool like Postman or curl to send HTTP requests to your RESTful API endpoints.

Please remember to start your application with ./mvnw quarkus:dev command before testing.

Finally, you’ll want to build your project for production. With Quarkus, this is as simple as running ./mvnw package. This will produce a runnable jar in the target directory.

With your REST API endpoints established and the core functionality of your Agri-Assist application in place, it’s time to shift focus towards refining your application and adding further functionality. Here are a few next steps:

  1. Implementing Security: Use Quarkus security extensions to secure your application. This includes user authentication and authorization to ensure only authenticated users can access your API endpoints. Quarkus offers various security extensions that can integrate with security providers like OAuth, JWT, LDAP, and others.
  2. Writing Tests: Implement unit tests and integration tests to ensure your application behaves as expected. Quarkus provides built-in support for testing with JUnit, Mockito, and RestAssured, which makes it easy to test your application’s functionality.
  3. Logging and Monitoring: Integrate logging and monitoring into your application. Quarkus supports numerous logging frameworks, and you can choose the one that best fits your needs. Monitoring can be implemented with the help of extensions such as Micrometer, which can integrate with popular monitoring systems like Prometheus and Grafana.
  4. Microservice Architecture: If your application grows in complexity, you might want to consider breaking it down into microservices. Quarkus supports microservice patterns and provides extensions for common microservice needs such as service discovery, distributed data management, etc.
  5. Continuous Integration/Continuous Deployment (CI/CD): Finally, set up a CI/CD pipeline for your application. You can use various tools like Jenkins, GitLab CI/CD, or GitHub Actions. This will allow you to automate your build, test, and deploy processes.
  6. Documentation: Document your API so that it can be easily understood and used by others. Quarkus supports the OpenAPI specification which can be used for generating API documentation.

Inconclusion, we have embarked on an exciting journey towards creating a highly efficient, scalable, and feature-rich Agri-Assist application using Quarkus, Java’s next-gen framework. We’ve understood the importance of Quarkus extensions, set up REST APIs, and built the foundation of our application. We’ve also adopted the principles of structuring our code effectively and considered the next steps in enhancing our application with further functionalities. Remember, each step forward is part of a larger iterative process, with cycles of development, testing, deployment, and user feedback. This dynamic, evolving approach to building our application sets the stage for our forthcoming exploration. In Part 2, we’ll dive deeper into mastering database operations and CRUD functionalities. See you in the next part of our Quarkus Unleashed series, where we will continue to unlock the potential of Quarkus in building lightning-fast applications.