Real world Quarkus with native-image

In our first blog post about Quarkus and GraalVM’s native-image we tested startup times and memory consumption in a synthetic test consisting of REST and JPA extensions. As it turned out we had a chance to bring this to the real world.

In one of our projects we needed to migrate part of the platform to a resource constrained environment due to data locality policies, also denoted as demilitarized zone (DMZ). Since that component was already written as a JEE application we looked for seamless migration paths. The goal was simple:

  1. Rewrite the app from Thorntail to Quarkus
  2. Build the Quarkus app to native image using GraalVM
  3. Run it on ARM-based device

You may ask why Quarkus and not Micronaut or even Spring Boot. Well the reason was mainly that Quarkus seemed a better fit to solve the first goal as it’s closer to traditional JEE implementation.

We consider the application to be middle-sized with well understood architecture and integrations. It uses a relational database via Hibernate and messaging using MQTT protocol. It also exposes tens of REST endpoints via a JAX-RS implementation.

The migration

The code migration was in part very straightforward. All components (standard JPA repositories, business services with transaction boundaries and REST resources) worked out of the box with the exception of having to change the access modifier of @Injected dependencies to get rid of the compiler warnings and few changes to configuration and scheduler annotations.

The only part where we had to roll up our sleeves was the security stuff. Since the original application used JEE security which is not supported by Quarkus we had to rewrite the security components. None of the provided implementations suited our needs so we had to hook up into the Quarkus security with custom implementation. And this was not the most pleasant experience as the documentation is scarce in this area and little to no resources to guide the way.

In summary to have a cookie based encrypted token with authenticated principal we had to

  1. Implement HttpAuthenticationMechanism which uses PersistentLoginManager to extract the user from the cookie and build TrustedAuthenticationRequest
  2. Create TrustedIdentityProvider implementing  IdentityProvider<TrustedAuthenticationRequest> which checks the validity of the user account

It’s not clear how to differentiate and process failed authentications, e.g. account locked vs bad credentials since the IdentityProvider is expected to throw AuthenticationFailedException which is final.

Also don’t forget to add io.quarkus:quarkus-security extension, otherwise the whole security request flow will not be triggered.

Native-image build

The journey was far from straightforward. In general, reflection is your enemy and must be eradicated. Otherwise you’ll get NullPointerExceptions on places you would never guess. This may not be an easy job given how the java ecosystem relies on reflection and other runtime magic (dynamic proxies, custom class-loading etc.).

For further reading see https://github.com/oracle/graal/blob/master/substratevm/LIMITATIONS.md

We’ll present the issues encountered in random notes:

  • We use Gradle as our build tool. With the Gradle plugin and Quarkus 1.5 there’s zero diagnostics when something goes wrong with native-image build. There’s an improvement in Quarkus 1.6, which however did not work for us. We decided to migrate to Maven since the build configuration is fairly simple. Running the native image build with Maven provides more details from native-image process.
  • The build requires at least 8GB of memory. Otherwise if fails without reason. See quarkus.native.native-image-xmx=8g property
  • We use JPA metamodel generator to safely reference Hibernate’s entities and fields in JPA queries. The problem is that although the metamodel classes are generated on compile time using annotation processor, the attribute values are generated in runtime using reflection. So you can’t even add @RegisterForReflection to these generated classes. An option would be to list all the metamodel classes in reflect-config.json but that would require more effort so we decided to just go with generated constant string attribute names in the metamodel. This had some negative impact on the JPA queries where additional type parameters need to be stated explicitly.
  • We use Jasypt library for password hashing due to backward compatibility, which initializes some classes lazily. This again does not work in native image since it uses reflection. We solved it by explicitly calling the required code in static block, in this case org.jasypt.normalization.Normalizer#normalizeToNfc(java.lang.String) and adding the class with the static block to --initialize-at-build-time property. This way, the classes are initialized during build and ready when the process is started.
  • What’s still puzzling is that Jackson deserialization should work, based on this documentation, only if the DTO classes are in the REST method signatures. We do however use javax.ws.rs.core.Response as a return type. Still the de/serialization works. The suspect may be the usage of JAXB annotations like @XmlRootElement which Quarkus can automatically register for reflection?
  • Another library using reflection, although only in loading messages is Paho MQTT client. To solve this we used another Quarkus feature, using @RegisterForReflection(targets = { ResourceBundle.class, ResourceBundleCatalog.class }) on a dummy class. This demonstrates how one can support reflection for classes in other libraries.

GraalVM team suggests using native image tracing agent (-agentlib:native-image-agent) which can record all usages of reflection, resources, proxying etc. in config files which can be directly fed into the native-image build. Usability of the tool is questionable in a Quarkus app as it produces a lot of cases which are handled by Quarkus custom build. It’s not evident what’s left to be handled manually. 

Tips:

Finally some numbers for our project:

  • Build time: 10 minutes, 8 minutes for native-image (8GB Heap, 8xCPU i7-8750H)
  • Startup time: 1s
  • Memory consumption: 90MB

Conclusion

With the rise of cloud friendly native images and the way how GraalVM uses AOT compiler to optimize startup time and resource consumption we see a clear shift from using dynamic aspects in java runtime to code generators and annotation processors basically embracing meta-programming techniques. Given that perspective there’s a great advantage in frameworks like Micronaut which does not use reflection when considering building java applications as native images.

The biggest pain point remains native compilation speed. Waiting 10 minutes for simple application build takes us back to the 90’s. Luckily Quarkus developer experience in the form of hot reload and quick debug attaching when building and testing the application on JVM is super awesome.

We did not cover ARM build in this post. Since GraalVM doesn’t support cross-compilation we aim to build it on an ARM-based VM.

Stay tuned.

Michal K.

Leave a Reply

Your email address will not be published. Required fields are marked *