To start exploring the project’s structure, let us go through the following steps:
- Extract the generated ZIP package from the preceding bootstrapping exercise to the project’s definitive location. I extracted the ZIP to my dedicated directory for projects – you can extract it wherever you prefer but make sure you remember this location since this is the project we will be working on throughout the book.
- Open the project in IntelliJ IDEA or the IDE of your choice.
IntelliJ should automatically detect your Maven project and load its dependencies. In case it doesn’t, you can perform this step manually by right-clicking the pom.xml
file and clicking on the Add as Maven Project menu item:
Figure 1.3 – A screenshot of IntelliJ IDEA and the Add as Maven Project context menu
Let’s now explore the content and structure of the project and the provided sample code.
Maven Wrapper
The project includes a Maven Wrapper setup. Maven Wrapper is a tool that allows project users to run a consistent version of Maven across different build environments. The tool also allows you to run Maven without the need to have a global Maven installation. The project includes the .mvn
directory and the mvnw
and mvnw.cmd
executable files.
You should be able to invoke Maven goals from a terminal in your project root. If you are in a Linux or macOS environment, you should be able to execute the wrapper by running ./mvnw
. If you are on Windows, you can execute the Wrapper by running ./mvnw
from a PowerShell terminal, or mvnw
from a standard cmd.exe terminal. Now that we’ve seen the provided Maven Wrapper setup, let’s focus, examine the Maven project configuration more closely, and analyze what each section accomplishes.
Maven project (pom.xml)
The Maven project is defined in the Project Object Model pom.xml
file. This XML file is the main unit of work for Maven and collects all the information and configuration details that will be used by Maven to build the project.
Let’s examine some of the sections of the pom.xml
file that were bootstrapped for us from the Quarkus website.
Maven coordinates (GAV)
The Maven coordinates, also known as GAV, are the minimum required references for a project. These are the groupId
, artifactId,
and version
fields that we defined in the web-based wizard when we bootstrapped the project:
<groupId>com.example.fullstack</groupId>
<artifactId>reactive</artifactId>
<version>1.0.0-SNAPSHOT</version>
These fields act as a unique identifier for the project and enable you to reference it in other projects just like a coordinate system.
Maven properties
The project comes with a set of predefined properties in an XML <properties>
block. The following are the most important:
This property sets the Java version for the project. In this case, both the sources and target classes will require a Java 17 version. This property is used by the Maven Compiler Plugin, and it was introduced in version 3.6 of the plugin. This property relies on the other compiler-plugin.version
property, which you shouldn’t change – or at least make sure it’s always later than 3.6.
This property specifies the Quarkus version in use. Whenever a new Quarkus version is released, this is the property that you should update to upgrade your project. For patch versions and non-breaking releases, this change should be enough. For other version updates, you might need to change some parts of your code too.
Dependency management
The pom.xml
file contains a dependency management block with the following content:
<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>
This definition is important to set the version of the Quarkus extension dependencies. It’s using placeholders for the following Maven properties found in the properties
section to reference the effective dependency:
<quarkus.platform.artifact-id>quarkus-bom
</quarkus.platform.artifact-id>
<quarkus.platform.group-id>io.quarkus.platform
</quarkus.platform.group-id>
Under the hood, Maven is copying the dependency management section of the io.quarkus.platform:quarkus-bom
, Quarkus’ Bill of Materials (BOM), artifact to the current project. This process enforces the use of a consistent version for all of the provided Quarkus extensions that we’ll see in the next section, Dependencies.
Dependencies
The following block in the project object model is the dependencies definition. These are the actual library dependencies of our project. Let’s see what each of the bootstrapped dependencies does.
RESTEasy Reactive
In this book, we are going to explore the new reactive capabilities of Quarkus. RESTEasy Reactive is a Quarkus-specific implementation of the JAX-RS specification based on Vert.x. It takes full advantage of Quarkus’ reactive non-blocking capabilities, which improve the overall application performance. The following code snippet defines the dependency for this library:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive</artifactId>
</dependency>
JAX-RS is a Java EE or Jakarta EE API specification that enables the implementation of REST web services. It provides common annotations such as @Path
, @GET
, and @POST
, which can be used to annotate classes and methods to implement HTTP endpoints. If you’ve dealt with J2EE, Java EE, or Jakarta EE before, you might already be familiar with these annotations.
This highlights one of the main advantages of Quarkus. The learning curve is very gentle since most of it is based on proven community standards and libraries.
Quarkus ArC
Quarkus ArC is the dependency injection solution provided by Quarkus. It is based on the Java EE CDI 2.0 specification – again, a proven, long-lived standard. The following code snippet specifies this dependency:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
One of the advantages of ArC, and most Quarkus extensions in general, is that it’s build-time oriented. Most analysis and optimizations happen at build time, so none of this processing needs to be performed during the application startup. The result is an application that starts up nearly instantly.
Quarkus JUnit5
Quarkus JUnit5 is the main dependency for the Quarkus testing framework. It provides the @QuarkusTest
annotation, which is the main entry point for the test framework. The next code snippet configures this dependency:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
We’ll examine this dependency and its features in more detail in Chapter 5, Testing Your Backend.
Rest Assured
Rest Assured is the last test dependency that was bootstrapped in the project. Although it’s not provided by Quarkus, it’s the recommended way to test its endpoints. The following code snippet is used to define this dependency; notice the groupId
value is not io.quarkus
anymore:
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
We’ll be using it to create the integration tests for our application.
Plugins
Along with the more common Maven plugins, the build plugins section contains an entry for the Quarkus Maven plugin. This plugin provides Maven goals for most of the Quarkus features. Whenever we invoke any Maven command with a quarkus:
prefix, this is the plugin that will be responsible for the execution.
Profiles
The last section in the pom.xml
file is the one dedicated to profiles. The bootstrapped project contains a single profile with the native
ID. We can activate this profile either by using the Maven profile selection flag, -Pnative
, or by providing a -Dnative
system property (see the activation configuration):
Figure 1.4 – A screenshot of the beginning of the profiles section in pom.xml
The profile provides some specific configurations to run tests that partially override the one provided in the build or plugins section. However, the most important part is the quarkus.package.type
property. This is the property that instructs Quarkus to build a native binary for our platform. When we package our application with this profile (./mvnw clean package -Pnative
), we’ll get a binary file instead of a standard Java archive (JAR) package.
We’ll explore profiles in more detail in Chapter 6, Building a Native Image.
Source files
The bootstrapped project has the regular Java project structure. In addition to the pom.xml
project file in the root directory, you will find a src
subdirectory that contains the project sources.
Application properties
The application.properties
file is located in the src/main/resources
directory. This file contains the main configuration for our project. We’ll be modifying the application configuration and behavior by adding entries to it.
Under the hood, Quarkus uses SmallRye Config, which is an implementation of the Eclipse MicroProfile Configuration feature spec. This is another of the battle-tested standards on which Quarkus is based.
This is a standard property file. Each entry is added in a new line. For each line, the config key and the config value are separated by an =
sign.
For example, the code to set the application server port would be as follows:
quarkus.http.port=8082
The application.properties
file can also be used to define values that can be injected into your application.
Let’s say you defined the following property:
publisher.name=Packt
You could inject the preceding property into your application using the following snippet:
@ConfigProperty(name = "publisher.name")
String publisherName;
Profiles
Quarkus provides the option to build and execute the application based on different profile configurations. Depending on the target environment, you might want to select a specific profile that provides a valid configuration for that environment.
Quarkus has three profiles – dev
, which is activated in development mode, test
, which is activated when the tests are executed, and prod
, which is the default profile when the others don’t apply.
The same file is used for all profiles; to configure an option for a specific profile, you need to prefix the configuration key with %
and the profile name, except for prod
, which is the default profile and doesn’t require a prefix.
For the previous server port example, we can set the server port in dev
mode as follows:
%dev.quarkus.http.port=8082
In general, we’ll be adding configuration for prod
, and provide overrides for dev
mode when needed.
Static resources
The project contains an index.html
file in the src/main/resources/META-INF/resources
directory. This file will be automatically served from the underlying application’s HTTP server. When pointing a browser to the root path of the application (http://localhost:8080
), you will be greeted with this landing page that was bootstrapped for us:
Figure 1.5 – A screenshot of a browser pointing to http://localhost:8080
By default, Quarkus will serve any static file that is located in this directory. However, for our application, we’ll be using an alternative method since this approach is not compatible with frontend routing. In Chapter 11, Quarkus Integration, I’ll show you how to implement an API gateway that will be used as an alternative to serving the static resources.
Java code
The bootstrapped project contains some sample code. A GreetingResource
class is located in the standard src/main/java
directory under the com.example.fullstack
package. You will also find two tests for this class in the src/main/test
directory under the same package: GreetingResourceTest
and GreetingResourceIT
. We will place the new code that we implement in the same root package grouped by features.
Docker files
The project contains some example Docker files in src/main/docker
. These files can be used to create container images for your application. In Chapter 12, Deploying Your Application to Kubernetes, I’ll show you how to create container images for the application. However, we’ll be using Eclipse JKube, which requires a simpler configuration and doesn’t need these Docker files. JKube is a Maven plugin that generates all of the required configurations for your application to be able to deploy it to Kubernetes; for this reason, it’s not necessary to keep extra configuration files such as Docker or Kubernetes YAML files.
Now that we’ve seen the files and directory structure of the bootstrapped project, let us see how to perform the basic tasks that we will need to develop new features and deploy and run the application.
Development mode
For years, one of the main pain points for Java developers has been the lack of or very little support for hot reloading or live reloading. Traditionally, when you made some changes to your code, you had to recompile the application, package it, and redeploy it. This process was something that could take anywhere from a few seconds to several minutes or even hours in the worst cases. This is usually one of the disadvantages cited when people compare Java to other programming languages.
One of Quarkus’ main goals is bringing joy back to developers, so, naturally, this was one of the priority points to address. Quarkus development mode runs your application and monitors your code. Whenever you change any of the Java application source or resource files, Quarkus detects these changes and performs a hot deployment. You just need to refresh your browser for the changes to take effect.
We can start the development mode by running the quarkus:dev
Maven goal from the project’s root directory as follows:
./mvnw quarkus:dev
You will see the following result:
Figure 1.6 – A screenshot of the IntelliJ terminal running Quarkus development mode
If you check the preceding messages, you’ll notice that Quarkus automatically selected for us the dev
profile and started the live coding mode.
We can now point our browser to the URL of the sample endpoint that was bootstrapped (http://localhost:8080/hello
). If everything went well, the browser will show the Hello RESTEasy Reactive message:
Figure 1.7 – A screenshot of the browser pointing to the hello endpoint
If we open the GreetingResource
class in our IDE, we should be able to see the definition for this endpoint. We can change the greeting message to something else:
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "Hello Quarkus live coding!";
}
In the traditional Java world, we would now need to recompile and redeploy the application to be able to see the changes. However, if we reload the browser window, our modified message should be visible.
Debugging in development mode
If you check the messages in Figure 1.6 closely, you’ll notice that Quarkus has also enabled a remote debugging port:
Listening for transport dt_socket at address: 5005
This means we can easily start a debug session from IntelliJ IDEA. For this, we need to create a new debug configuration from the Run > Edit Configurations… menu:
Figure 1.8 – A screenshot of the IntelliJ IDEA Run menu
From the Run/Debug Configurations screen, we need to create a new Remote JVM Debug configuration. The default options should be fine for Quarkus, so we only need to specify a name for this configuration:
Figure 1.9 – A screenshot of IntelliJ IDEA Quarkus debug configuration
Once we save the configuration, we can run it and should be able to set a breakpoint on our endpoint definition. If we reload the browser window, the debugger should stop at our breakpoint.
When combined with easy debugging, live reloading is very powerful and will certainly improve our developer performance. Now that we know how to use the Quarkus development mode to implement and debug our code, let us see how to run the tests for our application.
Continuous testing
One of Quarkus 2.X’s features is its ability to run tests continuously. This is a feature borrowed from other programming languages, such as Ruby, that have offered it for a long time. It is also a further step in achieving Quarkus’s goal to bring back developer joy to Java.
For users who practice test-driven development (TDD), this will massively improve their development cycle performance. In a usual TDD process, developers first write a test for a feature and then implement the code that will make that test pass. This process is repeated for each of the properties of the feature and for each code refactor. Continuous test execution provides instant feedback and allows the developer to concentrate and focus on the implementation and not on the process.
When Quarkus is run in continuous testing mode, it will detect code changes to both code and tests. For each change it detects, it will re-run the relevant tests for the affected code.
Just as with the development mode, we can run a Maven command to start the continuous testing mode as follows:
./mvnw quarkus:test
If you recall, in the Development mode section, we changed the greeting in the GreetingResource
class but we didn’t change the test. The first thing we’ll see once we invoke the quarkus:test
Maven goal is a test failure:
Figure 1.10 – A screenshot of quarkus:test failing to be invoked
We can now open GreetingResourceTest
and update the expected response body to the new greeting, Hello Quarkus
live coding!
:
@Test
public void testHelloEndpoint() {
given()
.when().get("/hello")
.then()
.statusCode(200)
.body(is("Hello Quarkus live coding!"));
}
If we save the changes, the test should automatically re-run and it will be green again:
Figure 1.11 – A screenshot of quarkus:test passing invocation
Expanding on the TDD use case, if there was a new requirement to expose a /hello/world
endpoint, the first step would be to add a new test:
@Test
public void testHelloWorldEndpoint() {
given()
.when().get("/hello/world")
.then()
.statusCode(200)
.body(is("Hello world!"));
}
There is no implementation yet, so the execution would fail. We could then implement the new endpoint to make the test pass as follows:
@GET
@Produces(MediaType.TEXT_PLAIN)
@Path("/world")
public String helloWorld() {
return "Hello world!";
}
Once tests pass, the next step could be to retrieve the endpoint value from an external service. So, we would modify the test, then the implementation, and start the cycle again. It should be clear now how the experience of the overall process is notably improved by continuous testing.
TDD ensures that the features defined in the provided specs are working using unit tests. This allows you to write code with fewer bugs and spend less time on long debugging sessions trying to fix errors. Now that we’ve seen how to perform TDD in Quarkus, let us see how to package the application for its distribution.
Packaging the application
The final step to being able to distribute and run the application would be to package it. Besides the native mode, which we already analyzed in the Profiles section, Quarkus offers the following package types:
- fast-jar: This is the default packaging mode. It creates a highly optimized runner JAR package, along with a directory and its dependencies.
- uber-jar: This mode will generate a fat JAR containing all of the required dependencies. This JAR package is suitable for distribution of the application on its own.
- native: This mode uses GraalVM to package your application into a single native binary executable file for your platform.
- native-sources: This type is intended for advanced users. It generates the files that will be needed by GraalVM to create the native image binary. It’s like the native packaging type but stops before triggering the actual GraalVM invocation. This allows performing the GraalVM invocation in a separate step, which might be useful for CI/CD pipelines.
You can control the packaging mode by setting the quarkus.package.type
Maven property. You can set this property in the pom.xml
properties section or via the command line when running the Maven commands:
./mvnw -D"quarkus.package.type=uber-jar" clean package
For the moment, we’ll be using the default packaging mode. You can package the application running the following command:
./mvnw clean package
If everything went well, you should now be able to run the application by executing the following:
java -jar target/quarkus-app/quarkus-run.jar
You should now be able to navigate to http://localhost:8080
or any of the HTTP endpoints we created in the previous steps.