Quarkus 1.0.0.Final has been released, so I think it’s time to go beyond the “Get started” guides and to give it a try.
In this article, I’m going to illustrate how to run an existing Jakarta EE/MicroProfile application on Quarkus, both in JVM and native modes, with minimum changes on the code and the configuration.
I’ve used my demo application made at Oracle Code One. As you can see on my GitHub repository, this application is made of 3 services. In this article, we’re going to focus on one of them named EasyPay (a very simplified demo of a payment acceptance system). The original version was a traditional Jakarta EE/MicroProfile application packaged as a war file.
The technical context of this article has been:
- Quarkus 1.0.1.Final
- GraalVM Community Edition 19.2.1
- AdoptOpenJDK 11.0.5
- Use of JAX-RS, JPA, CDI, MicroProfile Health, Fault Tolerance, Rest Client, Fault Tolerance
- CDI discovery mode set to annotated
- Use of lombok to avoid boilerplate code.
Although this example is very simple, and not fully representative of a production-ready application, it provides useful information about the process to follow.
First off, the Maven configuration has to be changed. The pom.xml file of a Quarkus project is significantly different from a traditional Jakarta EE/MicroProfile project.
A traditional JakartaEE/MicroProfile project:
- Generates a thin war (or ear)
- Uses “generic” dependencies: Jakarta EE and MicroProfile
- Uses Arquillian to run tests.
In contrast, a Quarkus project:
- Generates a jar file (or a binary file with the native Profile)
- Has dependencies on individual extensions
- Does not use Arquillian
- Uses a specific Quarkus maven plugin.
Replacing generic dependencies by individual extensions is a relatively tedious task. Fortunately, error messages are accurate enough when an extension is missing, and the command mvn quarks:list-extensions allows to identify them quickly.
To provide you with an idea of the change, the initial pom.xml was 91 lines long, the final one with Quarkus is a bit more verbose with 146 lines long.
Database and JDBC technology
HSQLDB was used by my initial application. HSQLDB is an easy to use and lightweight SQL database that fits the needs for demos. As of this date (Dec. 2019), it’s not currently supported by Quarkus (no extension for its driver). According to the Quarkus Datasources documentation, the following databases are currently supported: H2, Derby, MariaDB, MySQL, PostgreSQL and Microsoft SQL Server.
Hence, I had to choose another database technology. I’ve decided to switch to H2 (using quarkus-jdbc-h2 extension) which is another viable option for demos.
The @DataSourceDefinition annotation was used by the original application to define and configure the DataSource:
name = “java:app/jdbc/easypay”, className = “org.hsqldb.jdbcDriver”,
url = “jdbc:hsqldb:hsql://localhost/oco”, user = “sa”, password = “xxx”,
maxPoolSize = 8, minPoolSize = 2
As a reminder, @DataSourceDefinition is part of JSR 250 (Common annotations) and is used to define a container DataSource to be registered with JNDI. This annotation is not supported by Quarkus.
With Quarkus, DataSources must be defined in application.properties. The original DataSource definition has been replaced by (including the change from HSQDLDB to H2):
A described in the documentation, there are about 20 datasource properties.
Quarkus supports multiple Datasources. However, as described below, it seems that only one of them is usable by JPA.
An interesting feature provided by Quarkus is that MicroProfile Health Check is automatically enabled on Datasources (see Datasource Health Check documentation). This can be disabled by setting quarkus.datasource.health.enabled property to false.
JPA and Hibernate configuration
My objective was to keep using JPA in standard way:
- By using a persistence.xml file (vs application.properties)
- Without using Panache.
There are two exclusive options to configure JPA persistence units with Quarkus:
- Using a traditional persistence.xml (which is supported but not recommended)
- Using hibernate-orm.* properties (in application.properies).
As mentioned in Using Hibernate ORM and JPA , you cannot mix them : “If you have a persistence.xml, then you cannot use the quarkus.hibernate-orm.* properties and only persistence units defined in persistence.xml will be taken into account.”
The original persistence.xml was made of a single Persistent Unit definition. I had to remove the jta-data-source element from it. As a refresher, this element defines the JNDI name of the underlying datasource and JNDI is intentionally not supported by Quarkus. I’ve not found anyway to name a datasource in persistent.xml. It seems that, as of this day, only the default Datasource one is usable by JPA.
However, Quarkus supports multiple JPA Persistent Units and you can inject a Persistent Unit by name:
private EntityManager em;
By using persistent.xml, it seems that Quarkus is not able to detect the SQL dialect and I had to explicitly define it in persistence.xml:
<property name=”hibernate.dialect” value=”org.hibernate.dialect.H2Dialect”/>
Without that, a Hibernate ServiceException is raised at startup.
I also had to add semi-colons at the end of lines in load.sql file (this was not required by Eclipse JPA).
The original application was using a JAX-RS application definition with a @ApplicationPath annotation (on a class extending javax.ws.rs.core.Application) to define the base URI of the REST application endpoints:
Using this annotation is supported by Quarkus, but is not recommended. The base URI of the initial application was /easypay and my objective was to keep the exact same URIs (prefixed by /easypay/api for the REST application endpoints and without any prefix for metrics, health …). I’ve tried to mix quarkus.resteasy.path with @ApplicationPath, but this didn’t work. According to my tests, these options are exclusive: @ApplicationPath, if defined, has a higher priority and quarkus.resteasy.path is just ignored.
To keep the same URIs, I have removed the Application class (and the @ApplicationPath annotation) and define the following property:
CDI injection on private fields
Quarkus is not happy with CDI injection on private fields and generates warning messages in such cases. To minimize the impact on my existing code, I’ve decided to keep my code as is, it works but it is not technically optimal. As detailed in Context and Dependency Injection, Quarkus needs to access private member with reflection. Reflection is supported by GraalVM but results in bigger executable.
Removal of Java 11 code
My initial application was using some elements of Java 11. This is supported in JVM mode but not in native mode: GraalVM 19.2.1 implements Java 8. Hence I had to downgrade part of my code to Java 8. As detailed by Emmanuel Bernard in his recent article Delay In GraalVM 19.3 support, JDK 11 support is expected early 2020.
Testing with Quarkus is just magic! Nothing to deploy, just annotate your test classes with @QuarkusTest and run it. All I had to do was to remove the Shrinkwrap deployment code.
There is however a subtlety when testing in native mode:
- Native mode testing is made using @NativeImageTest (instead of @QuarkusTest)
- Native mode testing only applies to REST endpoints
- CDI beans that implement the business logic cannot be tested with @NativeImageTest.
See Building native images for more information.
In reality, this is not a problem: both JVM and native tests are run when packaging a native image, which ensures a good test coverage in all circumstances. You can easily dissociate internal tests (that can only be run in JVM mode) and external tests (which can be run in both mode). Due to some technical differences between the JVM and the native modes, it is highly recommended to test REST endpoints both in JVM and native mode (see “Don’t forget @RegisterForReflection” below).
I’ve kept the original microprofile-config.properties file without any issue. Quarkus does not require to transfer its content into application.properties.
JVM vs native in development
The magic of Quarkus is that you can develop in JVM mode and run in native mode. In theory, you get the same functional behavior with a faster startup time and a smaller memory footprint. Reality is more nuanced …
Building a native image
It takes about 4 minutes (and all resources) on my laptop (MacBook Pro) to build a native image. That is a rather heavy process when developing. I imagine it is much longer with a real application. To be used with moderation …
Don’t forget @RegisterForReflection on some classes!
As mentioned in Context and Dependency Injection, GraalVM supports reflective operations, but all relevant members must be registered for reflection explicitly (by the way, these registrations result in a bigger executable). Quarkus tries to determine which classes are subject to reflection, but sometimes you have to help him by using the @RegisterForReflection annotation on some classes. And if you forget that, the effects can be very surprising as illustrated in the 2 examples below!
My first example is related to an application class, named PaymentResponse, embedded in a generic JAX-RS Response:
PaymentResponse response = paymentContext.generateResponse();
// Other code here …
As mentioned in Writing JSON REST Services, in that context, Quarkus is not able to detect that PaymentResponse needs some reflection to be JSON serialized. It is necessary to set @RegisterForReflection on the class. If you omit it, the net effect is surprising: everything seems to work well (no error or warning message) but the response body of the REST response message is just empty! That is why it is highly recommended to test REST endpoints in both JVM and native modes.
My second example is related to a CDI bean using the @Fallback Fault Tolerance annotation. Everything was OK in JVM mode, no error or warning message was generated when packaging the native image. But the native image was unable to start due to a runtime exception: “FaultToleranceDefinitionException: Fallback method xxx with same parameters as yyy not found“!
As detailed in Writing native applications, an alternative to @RegisterForRegistration (which is intrusive into the code) is to use a configuration file to register classes for reflection. With this option, the original code is kept “as is.” I’ve tested it: it works!
Enabling swagger-ui in native mode
To enable swagger-ui in native mode, I had to set quarkus.swagger-ui.always-include to true even with an image built with the dev mode (with the mvn package -Pquarkus.profile=dev command). I do not know whether it is a bug or an expected behavior but at least the documentation is inaccurate. I’ve opened an issue for that.
JVM vs native runtime
Here are some figures to illustrate the runtime differences between a traditional Java/Jakarta EE/Microprofile solution, Quarkus in JVM mode and Quarkus in native mode.
- Traditional Jakarta EE/Microprofile/JVM: 4.266 sec
- Quarkus JVM mode: 1.778 sec
- Quarkus native mode: 0.101 sec.
- In JVM mode, Quarkus startup time is 58% faster than a traditional stack
- The native mode is about 18 times faster than the JVM mode.
Memory footprint (Resident Set Size)
- Traditional Jakarta EE/Microprofile/JVM: 248.388 bytes
- Quarkus JVM mode: 177.512 bytes
- Quarkus Native mode: 39.560 bytes
- In JVM mode, Quarkus memory footprint is 28% smaller than a traditional stack
- The native mode needs about 4.5 times less memory than the JVM mode.
Response time of the 2 first requests
- Traditional Jakarta EE/Microprofile/JVM: 172 msec, 9 msec
- JVM mode: 199 msec, 6 msec
- Native mode: 23 msec, 2 msec
- In JVM mode, the response time of the 2 first requests are similar to traditional stack
- The response time of the first request in native mode is about 9 times faster than the JVM mode. The response time of the following is 3 times faster.
Don’t misinterpret these figures. It doesn’t mean that Quarkus native is always better than the JVM. First off, keep in mind that this is not a real benchmark, just some simple measures. Furthermore, as explained in Quarkus runtime performance, the JVM remains a good option in some conditions. It depends on how you want to scale: “Quarkus gives you the option to scale up in JVM mode if you need a single instance with a larger heap, or scale out in Native mode if you need more, lighter-weight instances“. A single JVM instance scales better than a single native instance.
In this article, I’ve detailed how to run an existing Jakarta EE/MicroProfile application on Quarkus with minimal changes. I’ve tried to keep the code and the configuration as close as possible to the standards.
In the end, the whole process is not very complex, but there are several subtleties to be taken into account, in particular to ensure the consistency between the JVM and native modes.
It is clear that the native mode is a good option on cloud platforms, where fast startup, small system footprint and lightweight instances are expected. This is at the price of some limitations:
- The whole Java ecosystem is not supported: specific Quarkus extensions are needed to support JDBC drivers and middleware
- Quarkus doesn’t aim at being a full Jakarta EE/MicroProfile implementation. For instance: CDI is partially supported, JNDI and EJB are not supported. It clearly target green-field cloud-native applications using a subset of the standards
- In native mode, GraalVM also has technical limitations. It currently doesn’t support Java>8 (Java 11 should be supported early 2020). SubstrateVM isn’t currently as powerful as a full-blown JVM regarding thread management, garbage collecting, scalability …
Despite these limitations, Quarkus opens a new era for Java by supporting a scaling model that better fits with modern cloud-platforms.
Leave a Reply