DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Related

  • A Glimpse Into the Future for Developers and Leaders
  • Transforming Proprietary Trace Context to W3C Trace Context
  • Less Time Learning, More Time Building
  • Unleashing Serverless Computing With Azure Functions: Advantages, Best Practices and Scope

Trending

  • How Can Developers Drive Innovation by Combining IoT and AI?
  • Understanding IEEE 802.11(Wi-Fi) Encryption and Authentication: Write Your Own Custom Packet Sniffer
  • AI-Driven Root Cause Analysis in SRE: Enhancing Incident Resolution
  • How to Build Real-Time BI Systems: Architecture, Code, and Best Practices
  1. DZone
  2. Software Design and Architecture
  3. Cloud Architecture
  4. Insight Into Developing Quarkus-Based Microservices

Insight Into Developing Quarkus-Based Microservices

Through this article, we would like to share our experience deploying Quarkus framework-based microservices.

By 
Brijendra Kumar user avatar
Brijendra Kumar
·
Neeraj Kaushik user avatar
Neeraj Kaushik
·
Vaibhav Ghadage user avatar
Vaibhav Ghadage
·
Atul Saxena user avatar
Atul Saxena
·
Jan. 04, 23 · Tutorial
Likes (9)
Comment
Save
Tweet
Share
3.8K Views

Join the DZone community and get the full member experience.

Join For Free

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:

  • It helped with a quicker and more pleasant development process.
  • Optimized Serverless deployments for low memory usage and fast startup times
  • Allowed us to utilize both blocking (imperative) and non-blocking (reactive) libraries and APIs
  • Worked well with continuous testing to facilitate test-driven development
  • Allowed support to test the JUnit test cases, which we have developed using test-driven development approach

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.

  • We had to add an additional directory zip.native under src/main to add the certificates to function.zip.
  • zip.native contains cacerts and custom bootstrap.sh (shown below) 
  • cacerts is the trust store and it holds all the certificates needed. 
  • bootstrap.sh should embed this trust store within the native function.zip.
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.

Quarkus microservice Serverless computing Cloud

Opinions expressed by DZone contributors are their own.

Related

  • A Glimpse Into the Future for Developers and Leaders
  • Transforming Proprietary Trace Context to W3C Trace Context
  • Less Time Learning, More Time Building
  • Unleashing Serverless Computing With Azure Functions: Advantages, Best Practices and Scope

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!