From Spring Boot to Micronaut

Nicolas Fränkel - Nov 8 '20 - - Dev Community

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 and beans 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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
  1. Handle dependency injection
  2. 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
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

Micronaut allows us to just use the standard main function.

fun main(args: Array<String>) {
  build()
    .args(*args)
    .packages("ch.frankel.springtomicronaut")
    .start()
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Migrating the web layer

Migrating to the web layer requires:

  1. To replace Spring Boot starters with the relevant Micronaut dependencies
  2. 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>
Enter fullscreen mode Exit fullscreen mode

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:

  1. Use Micronaut's dependencies instead of Spring Boot's
  2. Replace Micronaut's Spring Boot's Repository with Micronaut's
  3. 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:

  1. A data access dependency
  2. A driver dependency
  3. 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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

One needs to configure Micronaut to scan for repositories and entities:

jpa.default:
  packages-to-scan:
    - 'ch.frankel.springtomicronaut'
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  1. Disable Hibernate's schema creation
  2. Log SQL statements
  3. 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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  1. 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

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .