Microservices With Apache Camel and Quarkus (Part 4)
After learning how to run our microservices in JVM mode in Part 2 of this series, let's now look at how to do the same in native mode.
Join the DZone community and get the full member experience.
Join For FreeAs we've seen previously, running our microservices in JVM mode means packaging and running them as executable JARs. But Quarkus also allows us to compile them into machine code and run them as native processes. This has the advantage of dramatically improving the application start-up time and memory usage. Hence, the native mode is the preferred execution mode of the Quarkus applications in production.
All the whys and wherefores of the native mode are explained in a very clear and detailed manner in the Quarkus documentation. To resume, in order to compile Java code into machine one, a so-called "C native compilation environment" is required, as well as a GraalVM distribution. However, given the relative complexity of the GraalVM installation and configuration process, Quarkus is able to avoid it to us and offers the possibility to create Linux executable code without having to install and configure GraalVM. This is the modus operandi that we'll be adopting here.
Compiling Java code into machine one isn't unfortunately an easy feat, and, in the case of our prototype here, the challenge was more important than expected. As a matter of fact, it took me more time than originally planned in order to achieve that and it also required me to register a couple of issues on the Quarkus GitHub forum. The reason is that a Java application running in JVM mode needs some refactoring before running in native mode.
The first and most important modification that we need to operate into a Java application in order to get it running in native mode is to set the property quarkus.package.type
to native
. There are several ways to do that, but in our case, we're doing it in the project's Maven master POM as shown below:
<properties>
...
<quarkus.package.type>native</quarkus.package.type>
</properties>
This property will be in effect only for the project's Quarkus modules, meaning the modules using the quarkus-maven-plugin
. In our case, these modules are our microservices; i.e., aws-camelk-file
, aws-camelk-s3
, aws-kamelk-jaxrs
and aws-kamelk-sqs
. There won't be any impact on the modules aws-camelk-model
, aws-camelk-api
and aws-camelk-provider
.
And since we're talking about properties, we need to mention also the one named quarkus.native.resources.includes
, which is mandatory when we want to declare additional resources to be loaded from the classpath
. By default, GraalVM will not include any resource in the classpath. As for Quarkus, it will only include META-INF/resources
. All the other resources require to be declared via the mentioned property.
For example, the aws-camelk-file
microservice is polling the input directory for XML files and as soon as such a file lands inside, it tries to validate it against the associated schema stored in the src/main/resources/xsd
folder. If we try to compile and execute our microservice as such, it will raise a FileNotFoundException
when trying to access the files money-transfers.xsd
and money-transfer.xsd
. To fix this issue, we need to add the following property to the application.properties file of this module.
...
quarkus.native.resources.includes=xsd/*.xsd
...
This way, all the resources found in the xsd
subdirectory having the type of xsd
will be added to the native executable.
Another important point, which isn't related to the Quarkus native mode in general but rather to the peculiarities of our prototype, is the use of the AWS SDK in order to handle S3 buckets and SQS queues and messages. This SDK is configured via the following Maven artifacts:
...
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-bom</artifactId>
<version>1.12.454</version>
<type>pom</type>
<scope>import</scope>
</dependency>
...
<dependencies>
...
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-sqs</artifactId>
</dependency>
...
</dependencies>
Trying to run our microservices in native mode with these dependencies will raise a strange mixture of exceptions. The reason is that the libraries which form the AWS SDK aren't quarkified, so to speak. What this exactly means is that, in order to build native GraalVM applications, Quarkus needs to perform a process named build time augmentation, and in order to perform this process, Quarkus needs extensions. A Quarkus extension is a library having the ability to process metadata like annotations or XML descriptors at the build time. Hence, running in native mode is only possible as far as all the application's libraries and other artifacts are quarkified; i.e., have associated Quarkus extensions.
This is the problem here, as the AWS SDK that we're using here doesn't have an associated Quarkus extension. This wasn't an issue in JVM mode where all the augmentations described above are done at the run time rather than at the build time and, consequently, they didn't require specific quarkified versions of libraries.
So, what's the solution when we need to use a given library in a Quarkus application running in native mode and it happens that there isn't any Quarkus extension for the given library? Well, the only solution is to find another one, waiting for the editor of the former one to provide a Quarkus extension.
The good news is that, in our case, there is such another library called Quarkiverse. So, in order to use Quarkiverse instead of the AWS SDK, we need to replace the Maven dependencies shown above with the following:
...
<dependencyManagement>
...
<dependency>
<groupId>io.quarkus.platform</groupId>
<artifactId>quarkus-amazon-services-bom</artifactId>
<version>${quarkus.platform.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
...
</dependencyManagement>
...
<dependencies>
...
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>url-connection-client</artifactId>
</dependency>
<dependency>
<groupId>io.quarkiverse.amazonservices</groupId>
<artifactId>quarkus-amazon-s3</artifactId>
</dependency>
<dependency>
<groupId>io.quarkiverse.amazonservices</groupId>
<artifactId>quarkus-amazon-sqs</artifactId>
</dependency>
...
</dependencies>
Once these modifications are done, our code won't compile anymore and we need to convert the statements using the AWS SDK such that they use Quarkiverse. For example, in the module aws-camelk-file
, in order to get the list of the S3 buckets in the current AWS account, we use the following code sequence:
...
private static AmazonS3 amazonS3Client;
private static List<Bucket> buckets;
@BeforeAll
public static void beforeAll()
{
amazonS3Client = AmazonS3ClientBuilder.standard().build();
buckets = amazonS3Client.listBuckets();
}
...
To do the same thing with Quarkiverse, we need to modify the code above as follows:
...
@Inject
S3Client s3client;
private static List<Bucket> buckets;
...
@BeforeAll
public void beforeAll()
{
buckets = s3client.listBuckets(ListBucketsRequest.builder().build()).buckets();
}
...
As you can see, Quarkiverse is a more modern library supporting CDI and allowing us to simply inject the S3 client. It also supports the builder design pattern, as shown in the code fragment above.
These modifications have to be done across the full application in all the microservices where the AWS SDK is used. Sometimes this might be fastidious and time-consuming. The Quarkiverse library works as expected in both JVM and native mode, accordingly. A better idea would have been to use it from the beginning instead of using the AWS SDK. If I didn't, this was because the AWS SDK is more known than Quarkiverse, which I discovered just on the occasion of trying to fix the mentioned issues.
Another important point is that as you have seen, Quarkiverse is using the notion of client. There is an S3 client, an SQS client, and so on. These clients are a kind of HTTP client, and there are two categories: the old good Apache HTTP client and a newer one, known as the "URL client." In order to configure which one of these HTTP clients is to be used, we need to set the property quarkus-amazon-s3_quarkus.s3.sync-client.type
which accepts one of the following values: apache
and url
, the last one being the default. In our prototype, we're using the URL HTTP client and, since this is the default, we don't need to initialize this property. However, we need to include the following dependency:
...
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>url-connection-client</artifactId>
</dependency>
...
In some cases, I have met some unexpected issues that I couldn't solve but only work around, as documented in this ticket submitted to the Quarkiverse forum. Here, injecting the S3 client resulted in NPE and the default value of the HTTP client didn't seem to be accepted. Hence, I came to some code like:
@Inject
S3Client s3client;
...
s3client = S3Client.builder().httpClient(UrlConnectionHttpClient.builder().build()).build();
...
I would have expected that injecting the S3 client would have been enough, but it wasn't. And, in order to work around the mentioned exception, I needed to initialize manually the S3 client after having injected it (initializing it manually only without injecting it won't be enough).
Unit and integration testing is another crucial point where you may expect some differences between the JVM and the native mode. As you probably have observed, our unit tests aren't so unit because they use AWS and, consequently, they are rather integration tests. However, they are annotated with @QuarkusTest
annotation, which makes them plain unit tests. The reason is that annotating them with @QuarkusIntegrationTest
(as we should have to) doesn't allow us to use CDI and injection, and this would be quite restrictive.
Using AWS in unit tests while running in JVM mode isn't a problem, but in native mode, unit tests using AWS services are done through LocalStack. This might change the result of our unit tests since instead of accessing S3 buckets and SQS queues in the AWS account, we are accessing them in a local environment, and that can change everything.
Ideally, there should be a way to execute Quarkus unit tests with AWS services on AWS and not localstack
. Waiting to find a way to do that, I disabled some of the tests that don't run as they should on localstack
.
Anyway, as you may see, there are lots of things to pay attention to when switching from running in JVM mode to the native mode. Our prototype has a dedicated native branch and, in order to play with it, proceed as follows:
$ git clone https://github.com/nicolasduminil/aws-camelk
$ cd aws-camelk
$ git checkout native
$ ./delete-all-buckets.sh
$ ./create-sqs-queue.sh
$ mvn clean install
$ ./start-ms.sh
$ ...
$ ./kill-ms.sh
Don't be surprised if the build process will be much slower than it was previously in JVM mode. That's normal because as explained, all the augmentation and other operations are done at the build time now. Be patient and, after a while, the build process should finish successfully.
Enjoy!
Opinions expressed by DZone contributors are their own.
Comments