Insight Into Developing Quarkus-Based Microservices

Quarkus

Quarkus is an open-source CDI-based framework introduced by Red Hat. It supports the development of fully reactive microservices, provides a fast startup, and has a small memory footprint. Below was our overall experience using Quarkus:

Quarkus Supports Native Builds

Quarkus supports native builds for an application deployment which contains the application code, required libraries, Java APIs, and a reduced version of a VM. The smaller VM base improves the startup time of the application.

To generate a native build using a Java Maven project, one can leverage Docker or podman with GraalVM:

 
mvn clean install -Dnative -Dquarkus.native.container-build=true -Dmaven.test.skip=true


The native executable is lightweight and performance optimized.

Common Build and Runtime Errors

Quarkus, being fairly new, lacks sufficient support in the community and documentation. Below were some of the errors/issues we encountered during development and their resolution.

Build Errors

UnresolvedElementException

 
com.oracle.graal.pointsto.constraints.UnresolvedElementException: Discovered unresolved type during parsing: xxx


This error is caused by missing classes at the image build time. Since the native image runtime does not include the facilities to load new classes, all code needs to be available and compiled at build time. So any class that is referenced but missing is a potential problem at run time. 

Solution

The best practice is to provide all dependencies to the build process. If you are absolutely sure that the class is 100% optional and will not be used at run time, then you can override the default behavior of failing the build process by finding a missing class with the — allow-incomplete-classpath option to native-image.

Runtime Errors

Random/SplittableRandom

 
com.oracle.graal.pointsto.constraints.UnsupportedFeatureException: Detected an instance of Random/SplittableRandom class in the image heap


These errors are caused when you try to initialize these classes in a static block. Embedding instances of Random and SplittableRandom in native images cause these errors. These classes are meant to provide random values and are typically expected to get a fresh seed in each run. Embedding them in a native image results in the seed value that was generated at build-time to be cached in the native image, thus breaking that expectation.

Solution

We were able to resolve these by using below different ways:

  1. By avoiding build time initialization of classes holding static fields that reference (directly or transitively) instances of Random or SplittableRandomclasses. The simplest way to achieve this is to pass — initialize-at-run-time=<ClassName>to native-image and see if it works. Note that even if this works, it might impact the performance of the resulting native image since it might prevent other classes from being build-time initialized as well.
  2. Register classes holding static fields that directly reference instances of Random or SplittableRandom classes to be reinitialized at run-time. This way, the referenced instance will be re-created at run-time, solving the issue.
  3. Reset the value of fields (static or not) referencing (directly or transitively) instances of Random or SplittableRandom to null in the native-image heap.

ClassNotFoundException/InstantiationException/IllegalArgumentException

These errors can occur when a native image builder is not informed about some reflective calls or a resource to be loaded at run time. Or if there is a third-party/custom library that includes some, ahead-of-time incompatible code.

Solution

In order to resolve these exceptions, add the complaining class in reflect-config.json

JSON
 
{

  {

    "name": "com.foo.bar.Person",

    "allDeclaredMethods": true,

    "allDeclaredConstructors": true

  }

}


Reflection Issues With Native Builds

When building a native executable, GraalVM operates with a closed-world assumption. Native builds with GraaVM analyzes the call tree and remove all the classes/methods/fields that are not used directly. The elements used via reflection are not part of the call tree, so they are dead code eliminated. In order to include these elements in the native executable, we’ll need to register them for reflection explicitly. JSON libraries typically use reflection to serialize the objects to JSON, and not registering these classes for reflection causes errors like the below:

 
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class org.acme.jsonb.Person and no properties discovered to create BeanSerializer (to avoid an exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)


We resolved these by adding below annotation:

Java
 
@RegisterForReflection
public class MyClass {
  ...
}


If the class is in a third-party jar, you can do it by using an empty class that will host the @RegisterForReflection for it:

Java
 
@RegisterForReflection(targets={ MyClassRequiringReflection.class, MySecondClassRequiringReflection.class})
public class MyReflectionConfiguration {
  ...
}


Note that MyClassRequiringReflection and MySecondClassRequiringReflection will be registered for reflection but not MyReflectionConfiguration. This feature is handy when using third-party libraries using object mapping features (such as Jackson or GSON):

Java
 
@RegisterForReflection(targets = {User.class, UserImpl.class})
public class MyReflectionConfiguration {
...
}


We can use a configuration file to register classes for reflection. As an example, in order to register all methods of class com.test.MyClass for reflection, we create reflection-config.json (the most common location is within src/main/resources).

JSON
 
[
  {
    "name" : "com.test.MyClass",
    "allDeclaredConstructors" : true,
    "allPublicConstructors" : true,
    "allDeclaredMethods" : true,
    "allPublicMethods" : true,
    "allDeclaredFields" : true,
    "allPublicFields" : true
  }
]


Integration With DynamoDB-Enhanced Client

Another aspect of using Serverless architecture was the use of DynamoDB. Although there are ways to connect simple DynamoDB clients to do all operations, it does require a lot of code writing which brings verbosity and a lot of boilerplate code to the project. We considered using DynamoDBMapper but figured we couldn't use it with Quarkus since it doesn't support Java SDK1. Enhanced DynamoDB Client in Java SDK2 is the substitute Java SDK1 DynamoDBMapper, which worked well with Quarkus, although there were a few issues setting it up for classes when using native images. 

Annotated Java beans for creating TableSchema apparently didn’t work with native images. Mappings got lost in translation due to reflection during the native build. To resolve this, we used static table schema mappings using builder pattern, which actually is faster compared to bean annotations since it doesn't require costly bean introspection:

Java
 
TableSchema<Customer> customerTableSchema =

  TableSchema.builder(Customer.class)

    .newItemSupplier(Customer::new)

    .addAttribute(String.class, a -> a.name("id")

                                      .getter(Customer::getId)

                                      .setter(Customer::setId)

                                      .tags(primaryPartitionKey()))

    .addAttribute(Integer.class, a -> a.name("email")

                                       .getter(Customer::getEmail)

                                       .setter(Customer::setEmail)

                                       .tags(primarySortKey()))

    .addAttribute(String.class, a -> a.name("name")

                    .getter(Customer::getCustName)

                                      .setter(Customer::setCustName)

    .addAttribute(Instant.class, a -> a.name("registrationDate")

    .build();


Quarkus has extensions for commonly used libraries which simplifies the use of that library in an application by providing some extra features which help in development, testing, and configuration for a native build.

Recently, Quarkus released an extension for an enhanced client.

This extension will resolve the above-mentioned issue related to native build and annotated Java beans for creating TableSchema caused by the use of reflection in AWS SDK. To use this extension in the Quarkus project, add the following dependency in the pom file:

XML
 
<dependency>
  <groupId>io.quarkiverse.amazonservices</groupId>
  <artifactId>quarkus-amazon-dynamodb</artifactId>
</dependency>
<dependency>
  <groupId>io.quarkiverse.amazonservices</groupId>
  <artifactId>quarkus-amazon-dynamodb-enhanced</artifactId>
</dependency>


We have three options to select an HTTP client 2 for a sync DynamoDB client and 1 for an async DynamoDB client; the default is a URL HTTP client, and for that need to import the following dependency.

XML
 
<dependency>
    <groupId>software.amazon.awssdk</groupId>
    <artifactId>url-connection-client</artifactId>
</dependency>


If we want an Apache HTTP client instead of a URL client, we can configure it by using the following property and dependencies:

Properties files
 
quarkus.dynamodb.sync-client.type=apache
XML
 
<dependency>
    <groupId>software.amazon.awssdk</groupId>
    <artifactId>apache-client</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-apache-httpclient</artifactId>
</dependency>


For an async client, the following dependency can be used:

XML
 
<dependency>
    <groupId>software.amazon.awssdk</groupId>
    <artifactId>netty-nio-client</artifactId>
</dependency>


Dev and test services for DynamoDB are enabled by default which uses docker to start and stop those dev services; these services help in dev and test by running a local DynamoDB instance; we can configure the following property to stop them if we don’t want to use them or don’t have Docker to run them:

Properties files
 
quarkus.dynamodb.devservices.enabled=false


We can directly inject enhanced clients into our application, annotate the model with corresponding partitions, sort, and secondary partitions, and sort keys if required.

Java
 
@Inject  
DynamoDbEnhancedClient client;

@Produces  
@ApplicationScoped  
public DynamoDbTable<Fruit> mappedTable() {    
  return client.table("Fruit", TableSchema.fromClass(Fruit.class))   
}


Java
 
@DynamoDbBean
public class Fruit {

    private String name;
    private String description;

    @DynamoDbPartitionKey
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }
}



Achieving Security With Native Builds — SSL connections

Developing microservices architecture will, at some point, require you to call a microservice from another microservice. Quarkus provides its own Rest client configuration to efficiently do so but calling those services securely (using SSL) becomes complicated when using native images. By default, Quarkus will embed the default trust store ($GRAALVM_HOME/Contents/Home/lib/security/cacerts) in the native Docker image. The function.zip generated with the native build won’t embed the default trust store, which causes issues when the function is deployed to AWS.

Shell
 
# bootstrap.sh
#!/usr/bin/env bash

./runner -Djava.library.path=./ 
-Djavax.net.ssl.trustStore=./cacerts 
-Djavax.net.ssl.trustStorePassword=changeit


Health Checks and Fault Tolerance with Quarkus

Health Checks and Fault Tolerance are crucial for microservices since these help in enabling Failover strategy in applications. We leveraged the quarkus-smallrye-health extension, which provides support to build health checks out of the box. We had overridden the HealthCheck class and added dependency health checks for dependent AWS components like DynamoDB to check for health. Below is one of the sample responses from the health checks with HTTP status 200:

JSON
 
{
    "status": "UP",
    "checks": [
        {
            "name": "Database connections health check",
            "status": "UP"
        }
    ]
}


Along with these health checks, we used fault tolerance for microservice-to-microservice calls. In a case called microservice is down or not responding, max retry and timeouts were configured using quarks-small rye-fault-tolerance. After max retries, if the dependent service still doesn't respond, we used method fallbacks to generate static responses.

Java
 
import org.eclipse.microprofile.faulttolerance.Retry;
import org.eclipse.microprofile.faulttolerance.Timeout;
import org.eclipse.microprofile.faulttolerance.Fallback;
...

public class FooResource {
    ...
    @GET
    @Retry(maxRetries = 3)
    @Timeout(1000)
    public List<Foo> getFoo() {
        ...
    }

    @Fallback(fallbackMethod = "fallbackRecommendations")
    public List<Foo> fallbackforGetFoo() {
        ...
    }

    public List<Foo> fallbackRecommendations() {
        ...
        return Collections.singletonList(fooRepository.getFooById(1));
    }
}


Conclusion

Overall, Quarkus is an excellent growing framework that offers a lot of options for developing serverless microservices using native images. It optimizes Java and makes it efficient for containers, cloud, and serverless environments with memory consumption optimization and a fast first response time.

 

 

 

 

Top