DEV Community ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป

Cover image for Migrating a Spring Boot application to Quarkus
Pierre Guimon
Pierre Guimon

Posted on

Migrating a Spring Boot application to Quarkus

Introduction

Java applications running in traditional Java Enterprise Edition environments are not well suited for cloud environments.

The application server start-up time are quite high, usually above one minute, and the memory footprint required is high. Often, they require complex cluster configuration.

This is not compatible with scale-up and scale-down concepts introduced in the cloud.

A myriad of java frameworks are available on the market.

Quarkus is a Red-Hat java framework that does not require an application server, and whose goal is to support Kubernetes and Java Native Compilation using GraalVM.

Quarkus allows to reuse many existing java libraries, offering specific extensions for native compilation.

Applications built with Quarkus can start in few seconds and if native compiled, they have a very limited memory and disk footprint.

If you have no idea what is Quarkus, I encourage you to read my Quarkus fundamentals post (15mins read) on the subject.

With Quarkus it is not possible to replace 100% of the features provided by Java Enterprise Edition application servers: EJB, JSP and other similar technologies will not be available to applications written for Quarkus.

Migrating to Quarkus from a Spring boot application is not an immediate task, especially if targeting native compilation: many adaptations could be required.

Although there are great guides out there to explain you how to migrate a Spring boot application to Quarkus, those guides do not really emphasize on the approach for migrating a multi service code base from Spring to Quarkus.
Here are some examples:

In this article I will detail the approach for migrating a substantial Spring boot code base application (in other terms, a monolith ๐Ÿ˜†) to Quarkus.

I will also highlight some pitfalls that we have ran into while migrating one of my company service code base to Quarkus.

Approach for migrating to Quarkus

I will explain in this section how we have been progressing on the migration of one of my company service to Quarkus.

First of all, whatever the approach is, I would recommend anyone to get to know Quarkus by following the post highlighted in the introduction.

Once done, you should also play with the Quarkus Get Started guide on the official website, so that you can get familiar with the packaging and build your first application with Quarkus in no more than an hour.

The hothead approach

The first approach, that I like to call the hothead approach consists in:

  1. Adding the quarkus universe bom dependency to your service pom.xml file, following the guide: https://quarkus.io/guides/maven-tooling#build-tool-maven
  2. Build the service with: mvn quarkus:dev command line and light a candle hoping that everything will work at first try!

The build will of course generate tons of errors, most of them being related to dependency injection issues.

[ERROR] Failed to execute goal io.quarkus:quarkus-maven-plugin:2.2.3.Final:build (default) on project webapp: Failed to build quarkus application: io.quarkus.builder.BuildException: Build failure: Build failed due to errors
[ERROR] [error]: Build step io.quarkus.arc.deployment.ArcProcessor#validate threw an exception: javax.enterprise.inject.spi.DeploymentException: Found 107 deployment problems:
[ERROR] [1] Unsatisfied dependency for type org.springframework.web.client.RestTemplate and qualifiers [@Default]
[ERROR] - java member: com.myapp.server_impl.ServerImpl#<init>()
[ERROR] - declared on CLASS bean [types=[com.myapp.server_impl.ServerImpl, java.lang.Object], qualifiers=[@Named(value = "serverImpl"), @Default, @Any], target=com.myapp.server_impl.ServerImpl]
[ERROR] [2] Unsatisfied dependency for type javax.ws.rs.ext.Provider and qualifiers [@Default]
[ERROR] - java member: com.myapp.server_impl.ServerImpl#<init>()
[ERROR] - declared on CLASS bean [types=[com.myapp.server_impl.ServerImpl, java.lang.Object], qualifiers=[@Named(value = "serverImpl"), @Default, @Any], target=com.myapp.server_impl.ServerImpl]
[ERROR] [3] Unsatisfied dependency for type java.util.concurrent.ExecutorService and qualifiers [@Default]
[ERROR] - java member: com.myapp.server_impl.ServerImpl#<init>()
[ERROR] - declared on CLASS bean [types=[com.myapp.server_impl.ServerImpl, java.lang.Object], qualifiers=[@Named(value = "serverImpl"), @Default, @Any], target=com.myapp.server_impl.ServerImpl]
...
...
hundreds of errors later
...
...
[ERROR] at io.quarkus.arc.processor.BeanDeployment.processErrors(BeanDeployment.java:1108)
[ERROR] at io.quarkus.arc.processor.BeanDeployment.init(BeanDeployment.java:265)
[ERROR] at io.quarkus.arc.processor.BeanProcessor.initialize(BeanProcessor.java:129)
[ERROR] at io.quarkus.arc.deployment.ArcProcessor.validate(ArcProcessor.java:418)
Enter fullscreen mode Exit fullscreen mode

Did you really think it would have work like that ๐Ÿ˜‡ ?!
Okay, let's take a step back and explain the basics.

The use-my-brain approach

Dependency injection

Quarkus is designed to work with the most widely used Java standards, frameworks and libraries, such as Eclipse MicroProfile, Apache Kafka, RESTEasy (JAX-RS), Hibernate ORM (JPA) and many more.

Quarkus programming model is based on another standard: the Contexts and Dependency Injection for Java 2.0 specification.

If you are completely new to dependency injection, I encourage you to read Quarkus introduction to contexts and dependency injection.

The first thing to know about Quarkus bean discovery and injection is that it won't scan classes from external modules.

If you have a multi maven modules project, like we do for the service we have been migrating, you will find out that Quarkus won't find by default classes in other modules.

You have various ways to make Quarkus find your beans. They are listed here: https://quarkus.io/guides/cdi-reference#bean_discovery.

Here is an excerpt from the referenced link:

"The bean archive is synthesized from:

  1. The application classes,

  2. Dependencies that contain a beans.xml descriptor (content is ignored),

  3. Dependencies that contain a Jandex index META-INF/jandex.idx,

  4. Dependencies referenced by quarkus.index-dependency in application.properties configuration file,

  5. And Quarkus integration code."

If you want, an external module or a third-party library on which you do not have the hand (meaning that you can't modify it), to be scanned by Quarkus you should add the dependency in the application.properties configuration file.

If you have control on the module/project, you can directly add an empty beans.xml file in the META-INF folder.

That said, you might want to clean your dependencies before getting your hands dirty, the less code base you will need to migrate the better.

We will come back to that point later.

Spring

Let's focus now on Spring. In the service we have been migrating, developers have been using Spring intensively.

The service grew over the years and Spring dependencies have been added to the project. Spring dependency injection has been used here and there, instead of standard CDI specifications.

As an example, @Component Spring DI annotations might have been used instead of @Singleton CDI annotation.

Another example would be the use of @Bean Spring DI annotation instead of @Produces CDI annotation.

There are more examples, and you can find a conversion table (Spring DI annotation versus CDI) on the Quarkus website: https://quarkus.io/guides/spring-di#conversion-table.

So that the migration to Quarkus is not cumbersome, the Quarkus team came-up with a set of extensions that will help you migrating Spring projects to Quarkus: spring-di, spring-web, spring-data-jpa, spring-data, spring-security, spring-cache, spring-scheduled, spring-boot-properties, spring-cloud-config-client.

For instance, if you decide to use spring-di Quarkus extension, a spring DI processor will map Spring DI annotations to CDI annotations.

That said, it is recommended to migrate all your Spring beans to CDI specifications.

Migrating to Quarkus

Following the above explanation on dependency injection and Spring we came-up with the following workplan that can be implemented for any migration of Spring boot based service to Quarkus.

1. Dependencies analysis

First of all, we want to analyze the dependencies that are required to build and run the service to be migrated to Quarkus framework.

  • Why ? This step is fundamental to identify all the required external dependencies as well as internal dependencies.

  • How ? We have used Class Dependency Analyzer (CDA) tool to meet this goal. You can find out how to use it on this page.

You can use built-in IDE dependency analyzer as well, but I found CDA really convenient to use and you can also use it as a library in your project if you want to improve the tool possibilities.

2. Maven modules cleaning

Secondly, we want to clean all unwanted internal dependencies.

  • Why ? As said previously we are migrating a monolith and it is based on Maven software management tool.

The code base consists of multiple Maven modules that are used in different services.

Some code that is not used, by the service we want to migrate, is part of Maven modules that the service depends on.

By cleaning all unwanted internal dependencies through moves to new/others maven modules, we will eventually reduce the scope of code base to be migrated to Quarkus.

Following this principle, we have performed major cleaning in the code base to remove irrelevant and unwanted internal dependencies for our service.

I highly encourage you to perform such cleaning prior to this migration. This preliminary step will eventually allow you to save time in the later steps.

  • How ? The output of Class Dependency Analyzer tool allows you to check all the classes that your service depends on, and eventually remove/move all unwanted classes.

Concretely, this has been performed by moving some classes that were not needed by the service, to new maven modules or existing maven modules on which the service doesn't have a dependency on.

We have also refactored some pieces of code: by splitting some classes for instance, by creating new classes to specialized their usage to the service we have been migrating to Quarkus framework.

3. Mocking

The next step is to mock all the external dependencies that we have highlighted in step 1 to progress on the migration to Quarkus of OUR code base first.

  • Why ? By mocking all the external dependencies, we make sure to progress on our code base migration first and that we are not blocked by external dependencies.

  • How ? Simply by implementing interfaces with mocked behavior.

For instance, in the service that we have been migrating to Quarkus, we use an external dependency interface to have access to a context.

So that our application is building with Quarkus, we had to mock temporarily the context interface with a static mocked implementation.

We implemented the interface and made sure that the bean follows the CDI specifications.

4. Internal/external dependencies teams support

The previous steps should have highlighted you all the dependencies that are handled inside and outside of your organization.

Now you can ask support to the teams inside/outside your organization owning dependencies, to unlock your progression.

  • How ? Either you ask for the support of the teams inside/outside your organization or you contribute directly to the migration to Quarkus of your dependencies.

This is done in an iterative approach, meaning that you or an external team is making a dependency Quarkus ready, then they deliver it, you integrate it in your code base, you remove the mock associated to this external dependency and you go on and on until there is no more external dependency to be migrated to Quarkus.

5. Spring-DI Quarkus extension

So that the migration to Quarkus is not cumbersome, the Quarkus team came-up with a set of extensions that will help you migrating Spring projects to Quarkus.

  • Why ? To comply with CDI specifications, we would need to migrate all our non-CDI compliant beans to CDI compliant beans, meaning we would need to migrate all Spring beans to CDI beans. This might be a fastidious work.

  • How ? To avoid doing this non-neglectable task, we have been using the Quarkus spring-di extension that do the job for you for the nominal cases.

You can find a conversion table (Spring DI annotation versus CDI) here: https://quarkus.io/guides/spring-di#conversion-table.

Using this extension is done simply by adding the following dependency to your service pom.xml file:

<dependencies>
    <!-- Spring DI extension -->
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-spring-di</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>
Enter fullscreen mode Exit fullscreen mode

6. Multi Maven modules handling

Let's be honest, no monolith has only one maven module.

This step is about making Quarkus scan beans in all required maven modules that the service you are migrating to Quarkus depends on.

  • Why ? As explained in the dependency injection section, Quarkus won't scan classes from external modules.

If you want, an external module or a third party library on which you do not have the hand (meaning that you can't modify it), to be scanned by Quarkus you should add the dependency in the application.properties configuration file.

If you have control on the module/project, you can directly add an empty beans.xml file in the META-INF folder.

This will ensure that Quarkus scans your beans.

  • How ? Simply create an empty beans.xml file in the META-INF folder of each maven module that you own, or create a dependency in the application.properties configuration file for modules that we do not own.

You can find more details in here: https://quarkus.io/guides/cdi-reference#bean_discovery.

image
image

7. Migrate your code base

This step focuses on migrating uncompliant base code to Quarkus framework compliancy.

  • Why ? So far, we mocked external dependencies to progress on the migration of our code base, we used spring-di Quarkus extension to ease our migration, but some pieces of code must be migrated to comply with Quarkus standards to build your service.

Indeed, some pieces of software cannot be handled by the extensions provided by the Quarkus team and must be migrated; and some other pieces of code are not following the CDI standard specifications and must be migrated as well.

This step really depends on your software.

We will review later on the recurrent errors we have been facing while migrating the service to Quarkus.

  • How ? Comply with CDI specifications and migrate some Spring dependencies that cannot be handled by Quarkus extensions.

  • Example.

In the service code base we have migrated we were using ThreadPoolTaskExecutor which is a java bean that allows for configuring a java standard ThreadPoolExecutor in bean style (through its corePoolSize, maxPoolSize, keepAliveSeconds, queueCapacity properties).

This class is also well suited for management and monitoring (e.g. through JMX), providing several useful attributes: corePoolSize, maxPoolSize, keepAliveSeconds (all supporting updates at runtime); poolSize, activeCount (for introspection only).

This class is part of the spring-context library.

Having a quick look at spring-di quarkus extension pom.xml you can find-out quite easily that spring-context dependency is excluded so that it is not accessible to us, as end-user.

Spring-context is excluded from the dependencies to filter spring-context classes and only keep the ones that are necessary in quarkus-spring-context-api dependency.

This means that we can't use at the same time, the spring-di quarkus extension (which is definitely a must have to migrate a monolith since we do not want to migrate all our beans to CDI standard specifications in the first place) and the Spring ThreadPoolTaskExecutor.

To mitigate this issue we have migrated our Spring ThreadPoolTaskExecutor to the ManagedExecutor class of org.microprofile library which is a standard supported by Quarkus.

This is an example that is particular to the code of this service, since not everyone uses Spring ThreadPoolTaskExecutor.

In the recurrent errors and tips section, we will go through more commons errors that you will for sure face while migrating to Quarkus framework.

8. Optimize software to embrace GraalVM idiomatics

This step is an optimization step that you should perform to embrace GraalVM idiomatics: boot faster, deliver smaller packages.

  • Why ? GraalVM idiomatics require to change the way frameworks work, not really at runtime but at startup time. Most of the dynamicity that a framework brings actually comes at the startup time and this is what is being shifted to the build time with Quarkus.

Quarkus could be also highlighted to be a framework to makes frameworks start at build time.

At startup time a framework (like Hibernate or Spring for instance) does usually the following:

  • Parse config files (e.g.: persistance.xml file)
  • Classpath and classes scanning for annotations (e.g.: @Entity, @Bean, etc...), getters or others metadata
  • Build metamodel objects from all those above information on which the framework will run at runtime. For instance Hibernate doesn't keep .xml files in memory but builds an internal model that is represented at runtime and it is this model that is used at runtime to save entities, etc...
  • Prepare reflection (will get the reference to method object and field to be able to perform invoke) and build proxies
  • Start and open IO, threads, etc... (e.g.: database connection, etc...)

Conceptually, when you look at those steps, there could be easily done at build time instead of at startup time.

Everything that is prior to the last step and even some parts of the start can be done at build time.

This is what is done by Quarkus, it takes a framework like Hibernate and makes it work so that the maximum of steps can be performed at build time.

On the following schema, you can see a typical Java framework at the top where most of the work is performed at runtime (configuration load, classpath scanning, model creation, starts the management), whereas at the bottom you can see a Quarkus framework where most of the work is performed at build time.

image

That said, you might want to endorse this approach and make sure that all possible actions that could be performed at build time are taken out from the runtime and deported to the build time.

Let's explain those concepts with a concrete example that we faced while migrating our service to Quarkus.

Once we were able to package our quarkus application exposing our service, we started it and launched a first message towards it.

The first query was taking a long time to be processed, whereas the second query was much more fast (x10 times faster ๐Ÿ˜ฎ).

We had to investigate why the first query was so long to be processed.

Using the Async Profiler we were able to build flamegraphs for the first and second queries to picture the differences in path length of the two transactions execution.

In the first flamegraph we saw that we spend most of the transaction time in initializing a JAXB context responsible for marsharling/unmarshalling a context from the input query.

This operation could be transferred at build time instead of doing it at startup time, since all the information required are present at build time.

This is just one example but, I'm positive, that in your code base, you have some operations that could be transferred from runtime to build time too !

Recurrent errors and tips

In this section we will highlight some common errors that you might encounter while migrating a service to Quarkus framework, and tips to solve them.

Package-private

You will see from time to time the following info message while building your Quarkus application:

[INFO] [io.quarkus.arc.processor.BeanProcessor] Found unrecommended usage of private members (use package-private instead) in application beans:
    - @Inject field com.myapp.service.MyService#someBean
Enter fullscreen mode Exit fullscreen mode

If a property is package-private, Quarkus can inject it directly without requiring any reflection to come into play.

That is why Quarkus recommends package-private members for injection as it tries to avoid reflection as much as possible (the reason for this being that less reflection means better performance which is something Quarkus strives to achieve).

Quarkus is using GraalVM to build a native executable. One of the limitations of GraalVM is the usage of reflection. Reflective operations are supported but all relevant members must be registered for reflection explicitly. Those registrations result in a bigger native executable.

And if Quarkus DI needs to access a private member it has to use reflection. Thatโ€™s why Quarkus users are encouraged not to use private members in their beans. This involves injection fields, constructors and initializers, observer methods, producer methods and fields, disposers and interceptor methods.

Bean list injection

Bean injection list is working perfectly well with Spring:

@Inject List<PaymentProcessor> paymentProcessor;
Enter fullscreen mode Exit fullscreen mode

but is not part of the CDI standard specifications.

In certain situations, injection is not the most convenient way to obtain a contextual reference. For example, it may not be used when:

  • the bean type or qualifiers vary dynamically at runtime, or

  • depending upon the deployment, there may be no bean which satisfies the type and qualifiers, or

  • we would like to iterate over all beans of a certain type.

In these situations, an instance of the javax.enterprise.inject.Instance interface may be injected:

@Inject Instance<PaymentProcessor> paymentProcessor;
Enter fullscreen mode Exit fullscreen mode

For more details you can checkout the CDI specifications for the instance interface.

That is to say, that you will have to migrate your Spring list beans injection to a CDI specifications compliant solution.

Usually you will use a producer pattern to produce those beans.

Unused beans

This particular point echoes the Quarkus documentation: https://quarkus.io/guides/cdi-reference#remove_unused_beans

Some of our beans were being removed at build time because they were considered as unused by Quarkus.

For example, the following Spring bean:

@Component
public class PaymentMapper extends Mapper<Payment> {
...
}
Enter fullscreen mode Exit fullscreen mode

which extends from:

public abstract class Mapper<T> {

  @Inject
  private MapperFactory mapperFactory;

  @PostConstruct
  private void register() {
    mapperFactory.register(type_of_the_class, this);
  }
}
Enter fullscreen mode Exit fullscreen mode

was registered in:

@Named
public class MapperFactory {

  private static final Map<Class, Mapper> mappers = new HashMap<>();

  public void register(Class type, Mapper mapper) {
    mappers.put(type, mapper);
  }
}
Enter fullscreen mode Exit fullscreen mode

The bean PaymentMapper was considered as unused because it was not referenced anywhere else in the code apart from its definition.

Unfortunately, the issue is that it was actually used via registering in a @PostConstruct method call in the MapperFactory.

The static mappers map ended up being always empty, because the Mapper beans were marked as unused.

For this case, we had to change the code so that the MapperFactory registers a list of beans implementing the same interface, which makes anyway way more sense.

Legal bean type

It is clearly written in the CDI specifications that: A parameterized type that contains a wildcard type parameter is not a legal bean type.

I have made a small reproducer on github to show you the failure of a CDI parameterized bean, that contains a wildcard type parameter, injection:

Since those beans are not considered as legal, they are not considered by Quarkus.

Provider no-arg constructor

In our code base we are using @Provider classes to decode inputs or to encode outputs.

These providers implement ReaderInterceptor/WriterInterceptor interfaces.

When quarkus-resteasy library comes into play it tells us at compilation time:

WARN  [io.qua.res.com.dep.ResteasyCommonProcessor] (build-9) Classes annotated with @Provider should have a single, no-argument constructor, otherwise dependency injection won't work properly. Offending class is com.myapp.interceptor.BaseReaderInterceptor
Enter fullscreen mode Exit fullscreen mode

The rule is the following: Classes annotated with @Provider should have a single, no-argument constructor and classes must be public.

For instance, this piece of code is not compiling:

@Provider
class BaseReaderInterceptor implements ReaderInterceptor {

  private StatsCollector statsCollector;

  @Inject
  public BaseReaderInterceptor(StatsCollector statsCollector) {
    this.statsCollector = statsCollector;
  }
  ...
}
Enter fullscreen mode Exit fullscreen mode

Whereas this one is compiling:

@Provider
public class BaseReaderInterceptor implements ReaderInterceptor {

  // Injection by constructor makes REST-EASY unable to initialize the ReaderInterceptor...!!!!
  // Hence we inject at member level
  @Inject
  private StatsCollector statsCollector;

  ...
}
Enter fullscreen mode Exit fullscreen mode

References

  • Emmanuel Bernard talk on Quarkus DEVOXX video: Quarkus why, how and what
  • Oleg Selaje & Thomas Wuerthinger talk on GraalVM DEVOXX video: Everything you need to know about GraalVM
  • For FR speakers: Emmanuel Bernard & Clรฉment Escoffier talk on using Quarkus with GraalVM DEVOXX video: Quarkus: Comment faire une appli Java Cloud Native avec Graal VM

Top comments (0)

Take Your Github Repository To The Next Level

>> Check out this classic DEV post <<