Bungee-Jumping into Quarkus: Blindfolded but Happy
A year ago, I started a new project using Quarkus. Here are some of the challenges I found using this new Java framework.
Join the DZone community and get the full member experience.
Join For FreeA year ago, I started with a couple of friends a new project based on Quarkus to create a visual editor for integrations called Kaoto.
As responsible of the backend side, I obviously chose Java to do it. Coming from the Java 8 world with shy traces of Java 11, I decided to jump directly to Quarkus on Java 17 (unstable at the time) with Reactive and explore the serverless possibilities while, at the same time, keep the over-engineering and the over-fanciness of new features as reasonable as possible.
On this article I will discuss the good and the bad of this experience. I am not a Quarkus developer, I am a developer that used Quarkus. And as any average developer that starts with a new technology, I obviously skipped the documentation and just bungee jumped into the framework, blindfolded and without safe nets.
Supersonic Subatomic Java
The first thing I noticed is that Quarkus is incredibly fast. Running over GraalVM was already a great improvement over normal Java, but when compiled to native code, it was amazing. We could deploy our full application, with multiple endpoints, and warm up a cache of transformed data in just milliseconds.
The dependency management was also a very nice surprise. Those of you coming from the Spring world will find familiar the use of dependencies that are released officially with the framework. If you want to add authentication, you add the Spring authentication dependency. If you want to add database access, you add the Spring database dependencies. So your life is constrained but easy to manage.
Quarkus does something similar but goes one step forward. It is still an opinionated set of libraries, but not restricted just to Quarkus libraries. You can choose different options on how you want to support your features. Do you want Hibernate? Use it. Do you want direct JDBC access? Also valid. All of them warranted to play nice in Quarkus.
Reactive
Reactive support is also native to Quarkus. You can still use non reactive style of programming, but adding the proper libraries, Quarkus can gently push you and teach you how to do proper reactive code.
Reactive is nice but sometimes you need something that is not Reactive. For example, I needed an endpoint that would stream logs from a deployed integration. And Quarkus didn't like it:
WARNING [io.ver.cor.imp.BlockedThreadChecker] (vertx-blocked-thread-checker)
Thread Thread[vert.x-eventloop-thread-10,5,main]=Thread[vert.x-eventloop-thread-10,5,main] has been blocked for 4827 ms,
time limit is 2000 ms: io.vertx.core.VertxException: Thread blocked
In reactive mode, there is one main thread that handles most of the code. The moment you need to do a blocking operation or you want to run a long process, you need to explicitly tell Quarkus this is a blocking
operation with the proper annotation:
@NoCache
@Path("/{name}/logs")
@Produces(MediaType.TEXT_PLAIN)
@GET
@Operation(summary = "Get logs",
description = "Get the resource's log.")
@Blocking
public Multi<String> logs(
final @Parameter(description = "Name of the resource "
+ "of which logs should be retrieved.")
@PathParam("name") String name,
final @Parameter(description = "Namespace of the cluster "
+ "where the resource is running.")
@QueryParam("namespace") String namespace,
final @Parameter(description = "Number of last N lines to be "
+ "retrieved.")
@QueryParam("lines") int lines) {
return clusterService.streamlogs(namespace, name, lines);
}
Developer Joy
There are many joys developing Quarkus, but the most gratifying feature is the live coding. You can keep your code running while coding and it will automatically refresh the running code so you can hot-test your new code on the fly. You can also run tests hot while you are developing. While this may seem like a nice but optional feature, once you get used to it you only can wonder how did life happened before it.
Quarkus also pushes you softly and gently into using very great design patterns. They are not enforced, but if you decide not to use them, that's usually a bad engineering decision.
While running on dev mode, you also have the dev console dashboard.
This dashboard contains very useful information. Each dependency you add can choose to have their own slot here to show relevant running data. It also allows you to edit and change the configuration on the fly.
Let's Start Then!
Starting with Quarkus is too easy. You go to https://code.quarkus.io/, select the dependencies you are going to use, download a zip, and voilà, you have your maven project ready to use.
Once my hello world was running I took things into my dirty hands and split my maven project into a multi maven project. Copied-pasted the folder several times, change name, generate parent pom.xml,... And it worked! Nice. Looks good.
Multi-Module Projects
Now I tried the compilation in native mode and... my dependencies were not found. I was confused for a while, why did it fail on native mode but worked on Java mode? Why were my dependencies not working?
Well... there is a very simple explanation. Native mode was run on all the maven modules. In native mode, Quarkus do a lot of cleanup of the source code assuming you are in a closed world. That means you will know everything you need to know at build time. If a class is not explicitly called, it gets removed. If a part of the code is never reached, it gets removed. This makes the application smaller and faster, but it forbids dynamic things like Reflection
.
<profiles>
<profile>
<id>native</id>
<activation>
<property>
<name>native</name>
</property>
</activation>
<properties>
<quarkus.package.type>native</quarkus.package.type>
</properties>
</profile>
</profiles>
I learned that native mode only goes on the last project of the chain. That's the one that is going to remove useless classes, optimize code, and forget about unused functions. If you add the native mode profile on all your dependencies, the classes will be removed before they reach the final project.
Where Are My Beans?
But then again, I was missing beans only in native mode. If I defined a bean in a module and tried to inject it in another module, the second module had no idea my bean existed. Even if it was properly injected somewhere when in its original module.
There is a long explanation on why this happens. If you are only interested in the solution, just know you need to generate a Jandex index in each of your modules. These indexes list all the beans available. You can generate the indexes with a maven plugin you must add to all your projects (or your parent pom.xml):
<plugin>
<groupid>org.jboss.jandex</groupid>
<artifactid>jandex-maven-plugin</artifactid>
<version>1.2.1</version>
<executions>
<execution>
<id>make-index</id>
<goals>
<goal>jandex</goal>
</goals>
</execution>
</executions>
</plugin>
Context Dependency Injections and Bean Discovery in Quarkus
Wait, we are talking about bean injection. I forgot to mention that Quarkus does have a built-in CDI.
And I have good news for you: Quarkus is following the Jakarta Context and Dependency Injection (version 2.0). If you decide later to move to another framework that uses the same standard, the annotations will be already valid and you won't have to modify them.
Quarkus also has classic functionality like @PostConstruct
to run a function right after bean creation, @PreDestroy
to cleanup resources before delete the bean, or @Counted
that runs every time a bean is invoked.
@ApplicationScoped
public class ClusterService {
@Inject
MyOtherBean beanybeany;
@Counted
String translate(String sentence) {
System.out.println("Interceptor binding annotation from MicroProfile");
}
@PostConstruct
void init() {
System.out.println("Hello there!");
}
@PreDestroy
void init() {
System.out.println("My revenge will be terrible.");
}
}
There's also different types of beans: @ApplicationScoped
, @Singleton
, @RequestScoped
, @Dependent
, and @SessionScoped
. These annotations allow you to have more control over when are beans created and destroyed.
The @Startup
annotation mark the bean to be initialized at startup, even if no one is asking for it yet. That was very helpful for us to warm up the caches of data. As Quarkus analyzes the code thoroughly on build time, you can also simplify all the setters of beans in one single function:
@Startup
@ApplicationScoped
public class StepCatalog extends AbstractCatalog {
private Instance<StepCatalogParser> stepCatalogParsers;
@Inject
public void setStepCatalogParsers(
final Instance<StepCatalogParser> stepCatalogParsers) {
this.stepCatalogParsers = stepCatalogParsers;
}
@Inject
void multipleBeans(OneBean one, AnotherBean another) {
this.one = one;
this.another = another;
}
}
And as shown above, you can inject a list of beans using the Instance
class, which extends Iterable
.
All the injections are resolved on build time, which means exactly one bean must be assignable to an injection point. Otherwise, the build fails, because there is nothing on runtime that can change which bean to assign where.
Not All Quarkus Is Rainbows and Sunshine
Quarkus is a fancy State of the Art framework. This is good, because you are using the latest knowledge available to humanity to build your apps — until you need to do something that is not yet implemented.
Keep Your Quarkus Updated
After every release Quarkus is more and more extensive and mature, and I would guess that 90% of use cases are already covered. But sometimes things are not ready yet.
That happened to me. I was using a nested class inside my configuration object. This was no big deal on normal Java, but on native mode, Quarkus was discarding the class during build time.
The good thing when this happens (on any FOSS project) is that you can talk directly to developers and work together solving the issue. If you contribute actively in the resolution of the issue, you may even shape that feature so it does exactly what you need, which is always nice for your future self.
But again, don't be me and remember to keep your Quarkus updated. Don't help fix something on February and forget to upgrade your dependencies by July.
Because in the frontier of Development, you sometimes forget you already fixed something.
And upgrading Quarkus is shamefully easy, you usually just need to update a number on your pom.xml file.
<!-- Quarkus version -->
<quarkus.platform.artifact-id>quarkus-universe-bom</quarkus.platform.artifact-id>
<quarkus.platform.group-id>io.quarkus</quarkus.platform.group-id>
<quarkus.platform.version>2.10.2.Final</quarkus.platform.version>
<quarkus-plugin.version>${quarkus.platform.version}</quarkus-plugin.version>
Polymorphic Unmarshalling
Kaoto is very dynamic and needs to adapt to multiple DSL. This required several endpoints to be able to work with interfaces and deserialize objects not knowing beforehand which object is coming from the endpoint request. This is called polymorphic deserialization: a weird feature of Jackson that fascinates me.
In theory, you make the endpoint receive an interface instead of a class. And then add classes to your source code with a special annotation marking that class as a subtype of the interface. Jackson will know which class to deserialize automatically when the request enters the endpoint.
But again I reached Quarkus State of the Art and fell into the other side. This worked on Java mode, but not on native mode. So I had to forget about adding decoupled classes to my source code and did explicitly tell Jackson where and when to deserialize what:
@JsonSubTypes({
@JsonSubTypes.Type(value = Patata.class, name = "patata"),
@JsonSubTypes.Type(value = Poteito.class, name = "potato")})
//This is a workaround utility class until Quarkus supports fully polymorphism
@JsonbTypeDeserializer(MyObjectDeserializer.class)
public abstract class MyObject implements Cloneable {
...
}
And I had to add an annotation @RegisterForReflection
to my deserializer because as it was not explicitly called anywhere in the code, Quarkus just assumed it was unused code.
@RegisterForReflection
public class MyObjectDeserializer implements JsonbDeserializer {
private static final Jsonb JSONB = JsonbBuilder.create();
@Override
public MyObject deserialize(final JsonParser parser,
final DeserializationContext context,
final Type rtType) {
String type = parser.getObject().toString().getString("type");
switch (type) {
case "patata":
...
}
}
Anyway, that worked, so even when I fell to the other side of the State of the Art and found myself in unimplemented features, I was able to work around it. Because even if there are unimplemented features, they are weird and work-aroundeables. You can probably live without them.
Quarkus Tests Joy
When you are implementing services with the rest-easy Quarkus dependency, tests are quick and nice. With the @TestHTTPEndpoint
annotation you can point to the resource you want to test and it will automatically generate the proper base path on the calls.
@QuarkusTest
@TestHTTPEndpoint(IntegrationResource.class)
class IntegrationResourceTest {
@Test
void thereAndBackAgain() throws URISyntaxException, IOException {
var res = given()
.when()
.contentType("application/json")
.body(Collections.emptyList())
.post("/dsls")
.then()
.statusCode(Response.Status.OK.getStatusCode());
List<String> dsls =
mapper.readValue(res.extract().body().asString(), List.class);
assertFalse(dsls.isEmpty());
}
}
If we later move the path of that endpoint resource, the test will be ready for it, no changes needed.
Don't forget to run tests on native mode too, just in case!
Smart TestContainers
If you are familiar with test containers, you are gonna love this. Quarkus have an automatic provisioning of unconfigured services. Databases, Messaging,... you just add a configuration on a properties file and the test containers are configured and run out of the box.
And if you need a Kubernetes cluster, you can use the fabric8 dependency and the @WithKubernetesTestServer
annotation to create a cluster only for tests.
@WithKubernetesTestServer
@QuarkusTest
class ClusterServiceTest {
@Inject
public void setKubernetesClient(final KubernetesClient kubernetesClient){
this.kubernetesClient = kubernetesClient;
}
private KubernetesClient kubernetesClient;
}
Final Word
I like the experience of working with Quarkus and I don't intend to move away any time soon. If you are doubting if you should or shouldn't use Quarkus, just try it a bit. I am sure you will be greatly surprised — in a good way.
This article was originally a talk I delivered on JBCNConf 2022.
Published at DZone with permission of María Arias de Reyna. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments