In the last couple of years, I've been playing a bit with a generation of tools in the Java world, namely Micronaut, Quarkus and GraalVM. While I'm a Spring Boot fan since its beginning, I believe this quite an eye-opening opportunity. In this post, I'd like to see how easy, or how hard, it is to port a simple Spring Boot application to Micronaut.
Setting up the context
The JVM is an great piece of technology. Modern versions compile the running bytecode to native code, depending on the existing workload. For this reason, JVM applications are on par with - or even winning over - native executables regarding to runtime performance.
JVM applications have a warm-up time during which they don't perform well. The loading of classes at runtime doesn't help. Frameworks such as Spring and Jakarta EE have been making use of classpath scanning and reflection, which make startup time even longer. For long-running processes, such as traditional application servers, this is not an issue.
In the context of containers, it is. Because one handles containers as cattle and not pets, the platform e.g. Kubernetes kills pods and schedules new ones at regular intervals. The longer the startup time, the less relevant the JVM becomes. It becomes even worse in Serverless environments that need to auto-scale the number of pods quickly.
To hop on the bandwagon, Oracle offers SubstrateVM. A subcomponent of GraalVM, SubstrateVM, allows transforming JVM bytecode into a native executable. To do that, SubstrateVM compiles the bytecode AOT. For that reason, you need to explicitly feed it information that is available on the JVM at runtime. It's the case of reflection for example. Note that some JVM features are not ported to GraalVM. Moreover, the AOT compilation is a time-consuming process.
The result is that on one hand, we have the JVM and all its features leveraged by frameworks; on the other hand, we have native executables that require fine-tuned manual configuration and a massive amount of build time.
A new generation of frameworks has spawned that aims to find a middle ground i.e. Micronaut and Quarkus. They both aim to generate bytecode AOT. Note that this AOT is different from the one mentioned above. Instead of using reflection at runtime, which is expensive, both frameworks generate extra classes at build time. This also allows us to avoid classpath scanning at startup time. In short, the idea is about making as much code as possible available at build time.
The sample application
I want the sample application to migrate to be simple enough so I can migrate it by myself but not to the point of being trivial. It consists of the following:
- A controller layer implemented by Spring MVC
- A repository layer implemented by Spring Data JPA
- A JPA entity
- Schema generation and data insertion at startup via Spring Boot
- The Spring Boot actuator, with the
health
andbeans
endpoints enabled and accessible without authentication
The application is written in Kotlin. I'll be using H2 as the database to make the whole setup less complex.
In general, I try to approach migrations in a step-by-step way. To do that, Micronaut offers a dedicated Micronaut-Spring dependency. I must admit I didn't manage to make it work the way I wanted. Thus, I did a big-bang migration. The rest of this post will focus on different places for the migration.
Common changes
The first change is to replace the parent POM.
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<parent>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-parent</artifactId>
<version>2.1.3</version>
</parent>
Because Micronaut generates bytecode at build-time, we need to add an annotation processor during the compilation. Thus, the close second step is to configure that in the POM.
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
...
<executions>
<execution>
<id>kapt</id>
<goals>
<goal>kapt</goal>
</goals>
<configuration>
<annotationProcessorPaths>
<annotationProcessorPath>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-inject-java</artifactId> <!-- 1 -->
<version>${micronaut.version}</version>
</annotationProcessorPath>
<annotationProcessorPath>
<groupId>io.micronaut.data</groupId>
<artifactId>micronaut-data-processor</artifactId> <!-- 2 -->
<version>${micronaut.data.version}</version>
</annotationProcessorPath>
</annotationProcessorPaths>
</configuration>
</execution>
...
</executions>
...
</plugin>
- Handle dependency injection
- Handle persistence-related classes
You can check those extra classes by looking at the target/classes
folder. For example, the sample application displays the following:
$Person$Introspection$$0.class PersonRepository$Intercepted$$proxy0.class
$Person$Introspection$$1.class PersonRepository$Intercepted$$proxy1.class
$Person$Introspection$$2.class PersonRepository$Intercepted$$proxy10.clas
$Person$Introspection$$3.class PersonRepository$Intercepted$$proxy2.class
$Person$Introspection.class PersonRepository$Intercepted$$proxy3.class
$Person$IntrospectionRef.class PersonRepository$Intercepted$$proxy4.class
$PersonControllerDefinition$$exec1.class PersonRepository$Intercepted$$proxy5.class
$PersonControllerDefinition$$exec2.class PersonRepository$Intercepted$$proxy6.class
$PersonControllerDefinition.class PersonRepository$Intercepted$$proxy7.class
$PersonControllerDefinitionClass.class PersonRepository$Intercepted$$proxy8.class
$PersonRepository$InterceptedDefinition.class PersonRepository$Intercepted$$proxy9.class
$PersonRepository$InterceptedDefinitionClass.class PersonRepository$Intercepted.class
Person.class PersonRepository.class
PersonController.class SpringToMicronautApplicationKt.class
Micronaut creates classes that contain Introspection
and Intercepted
via kapt
.
To start the application, Spring Boot refers to a class.
@SpringBootApplication
class SpringToMicronautApplication
fun main(args: Array<String>) {
runApplication<SpringToMicronautApplication>(*args)
}
Micronaut allows us to just use the standard main
function.
fun main(args: Array<String>) {
build()
.args(*args)
.packages("ch.frankel.springtomicronaut")
.start()
}
The Spring Boot plugin can find the main
function "automagically". In Micronaut, the current version requires you to set it explicitly in the POM:
<properties>
...
<exec.mainClass>ch.frankel.s2m.SpringToMicronautApplicationKt</exec.mainClass>
</properties>
Migrating the web layer
Migrating to the web layer requires:
- To replace Spring Boot starters with the relevant Micronaut dependencies
- To replace Spring Boot's annotations with Micronaut's
To make an application a webapp, Micronaut mandates to add an embedded server dependency. Tomcat, Jetty, and Undertow are available. Since Spring Boot's default is Tomcat, let's use Tomcat:
<dependency>
<groupId>io.micronaut.servlet</groupId>
<artifactId>micronaut-http-server-tomcat</artifactId>
<scope>runtime</scope>
</dependency>
Spring's and Micronaut's annotations map pretty much one to one. To use Micronaut is just a matter of using the annotations of one package instead of the other. The difference is that Spring offers the ability to serialize to JSON by using a specialized Controller
annotation, @RestController
. Micronaut does not and requires to set a property on the Controller
annotation.
Spring | Micronaut |
---|---|
o.s.w.b.a.RestController |
i.m.h.a.Controller(produces = [TEXT_JSON]) |
o.s.w.b.a.GetMapping |
i.m.h.a.Get |
o.s.w.b.a.PathVariable |
i.m.h.a.PathVariable |
-
o.s.w.b.a
=org.springframework.web.bind.annotation
-
i.m.h.a
=io.micronaut.http.annotation
Migrating the data access layer
To migrate to the data access layer, one must:
- Use Micronaut's dependencies instead of Spring Boot's
- Replace Micronaut's Spring Boot's
Repository
with Micronaut's - Create the schema and load the initial data with Micronaut
To create a data source and a connection pool, Spring Boot needs a Spring Data starter and a relevant driver. Micronaut demands three different parts:
- A data access dependency
- A driver dependency
- A connection pool dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>io.micronaut.data</groupId>
<artifactId>micronaut-data-hibernate-jpa</artifactId>
<version>${micronaut.data.version}</version>
</dependency>
<dependency>
<groupId>io.micronaut.sql</groupId>
<artifactId>micronaut-jdbc-hikari</artifactId>
</dependency>
Note that if you forget the connection pool, you'll run into this error at runtime:
No backing RepositoryOperations configured for repository. Check your configuration and try again
Spring Data JPA generates repositories' implementation at runtime. Micronaut Data generates them at build time. For the developer, the main difference is that the repository interface must be annotated with Micronaut's @Repository
.
@Repository
interface PersonRepository : CrudRepository<Person, Long>
One needs to configure Micronaut to scan for repositories and entities:
jpa.default:
packages-to-scan:
- 'ch.frankel.springtomicronaut'
To create the schema, you can configure Spring Boot in two different ways: either rely on Hibernate's schema creation or provide a create.sql
file at the root of the classpath. Likewise, to insert the initial data, you can add a data.sql
.
Micronaut doesn't offer an out-of-the-box mechanism to insert data. But it provides integration with Flyway. The default location to put Flyway's migrations is db/migration
, just as for Spring Boot.
<dependency>
<groupId>io.micronaut.flyway</groupId>
<artifactId>micronaut-flyway</artifactId>
<version>2.1.1</version>
</dependency>
WARNING: I didn't use the latest version because the parent references an artifact that's not found in Maven Central.
jpa.default:
properties.hibernate:
hbm2ddl.auto: none # 1
show_sql: true # 2
flyway.datasources.default: enabled # 3
- Disable Hibernate's schema creation
- Log SQL statements
- Enable Flyway migrations
The H2 driver dependency stays the same. While Spring Boot creates a connection with default parameters, Micronaut requires to explicitly configure it:
datasources.default:
url: jdbc:h2:mem:test
driverClassName: org.h2.Driver
username: sa
dialect: H2
Migrating the actuator
Micronaut also provides management endpoints. It has mainly the same as Spring Boot's.
One needs to replace the dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-management</artifactId>
</dependency>
The biggest difference with Spring Boot is that developers need to configure endpoints on an individual basis:
endpoints:
all.path: /actuator # 1
beans:
enabled: true
sensitive: false
health:
enabled: true
sensitive: false
flyway:
enabled: true
sensitive: false
- Set the management endpoints root
Conclusion
Convention over configuration is a great benefit when everybody shares the same implicit conventions. Spring Boot defined those conventions. The hardest part of migrating to Micronaut is that it has slightly different implicitness.
Micronaut is a good alternative to Spring Boot. Migrating from the latter to the former is pretty straightforward. One just needs to be aware of the gap between the conventions of the two stacks.
The complete source code for this post can be found on Github.
To go further:
Originally published at A Java Geek on November 8th 2020