Recently, I discovered a new framework in Java land called Micronaut. Being a Java developer for last fourteen years, I have seen many Java frameworks come and go. Apart from Spring, there are very few frameworks that made a mark. After a long time, I discovered a framework that is challenging the status quo of web development in Java.
Micronaut is an open-source modern JVM framework for building modular, easily testable microservice and serverless applications. It is a truly polyglot framework. You can build applications in Java, Kotlin, and Groovy.
Why you should learn Micronaut?
There have to be valid reasons why should learn a new framework. The reason why I decided to learn Micronaut are:
Reason 1: Fast startup time & reduced memory footprint
You might be wondering if it really matters to have fast startup time. In the old times, most JVMs ran on servers for a long time. So for most applications, it didn’t matter if application took few more seconds to start. Nowadays, we build applications that are composed of many small services. These services run in dynamic cloud environments orchestrated by container orchestration solution like ECS or Kubernetes. This requires your applications to be able to come up and down in no time.
To make it concrete, let’s compare start up times of Micronaut and Spring Boot. We will assume that we have two hello-world applications – one created with Micronaut and other with Spring Boot. You can generate a new Spring Boot application with just web dependency from https://start.spring.io/. For Micronaut, you can use its command-line. In a later section, I will show you how to create the application in a step by step manner. For now just assume that you can create similar hello-world applications with Spring Boot and Micronaut. Both of these framework allows you to create executable jars. Let’s assume that for Spring Boot our jar is hello-world-boot.jar and for Micronaut it is hello-world-micronaut.jar.
To run the hello-world Spring Boot jar, you need at least 16M of heap size.
java -Xmx16M -jar hello-world-boot.jar
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.1.1.RELEASE) 2018-12-08 13:25:41.570 INFO 90020 --- [ main] com.example.demo.DemoApplication : Starting DemoApplication on shekhargulati.local with PID 90020 2018-12-08 13:25:41.574 INFO 90020 --- [ main] com.example.demo.DemoApplication : No active profile set, falling back to default profiles: default 2018-12-08 13:25:42.835 INFO 90020 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http) 2018-12-08 13:25:42.982 INFO 90020 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 2018-12-08 13:25:42.983 INFO 90020 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/9.0.13 2018-12-08 13:25:43.037 INFO 90020 --- [ main] o.a.catalina.core.AprLifecycleListener : The APR based Apache Tomcat Native library which allows optimal performance in production environments was not found on the java.library.path: 2018-12-08 13:25:43.989 INFO 90020 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 2018-12-08 13:25:43.989 INFO 90020 --- [ main] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 2373 ms 2018-12-08 13:25:46.059 INFO 90020 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor' 2018-12-08 13:25:52.085 INFO 90020 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' 2018-12-08 13:25:52.151 INFO 90020 --- [ main] com.example.demo.HelloWorldBootApplication : Started HelloWorldBootApplication in 10.944 seconds
As you can see it took close to 11 seconds to start the Spring Boot application with 16M of heap memory. If you give Spring Boot application 1G of memory then application can start under 2 seconds. This is the best Spring Boot application can do.
On the other hand, Micronaut application can start in 1 second with only 7M of heap memory.
$ java -Xmx7M -jar hello-world-micronaut.jar
13:27:47.487 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 1015ms. Server Running: http://localhost:8080
To understand why Micronaut has faster startup compared to Spring Boot we have to understand what JVM does at startup. As mentioned in this post by PurelyFunctional people, JVM does three things at startup:
- Allocate memory for the initial heap
- Load classes that application needs to run
- Initialise classes loaded by it step 2
Assuming we give same initial heap size to both Micronaut and Spring Boot applications, the points to focus are 2 and 3.
Load classes that application needs to run
To find number of classes loaded by Spring Boot, we can run the following command.
$ java -verbose:class -jar hello-world-boot.jar | grep Loaded
Spring Boot loads close to 6640 classes.
To find number of classes loaded by Micronaut, we can run the following command.
$ java -verbose:class -jar hello-world-micronaut.jar | grep Loaded
Micronaut loads close to 5900 classes.
Overall, there is not much difference between number of classes loaded by Spring Boot and Micronaut.
Initialise loaded classes
This is the process that takes time in Spring Boot. At server boot up, Spring dependency injection container creates beans. Spring dependency injection is based on runtime reflection and proxies. Spring caches the metadata so that it does not have to use reflection again but you pay the cost at startup.
Micronaut like Spring and Grails also supports dependency injection. Dependency injection changed the way Java developers build applications. But unlike Spring and Grails, Micronaut does dependency injection at compile time. Micronaut uses annotation processors to precompile the necessary metadata required for DI.
Micronaut borrows the idea of compile time dependency injection from Dagger. Dagger is a fully static, compile-time dependency injection framework for both Java and Android. It is an adaptation of an earlier version created by Square and now maintained by Google.
Micronaut supports the following types of dependency injection:
- Constructor injection (must be one public constructor or a single contructor annotated with
@Inject
) - Field injection
- JavaBean property injection
- Method parameter injection
You can read more about Micronaut DI support in its documentation.
Reason 2: Future ready
Micronaut is written from the ground up for building modern Java applications. The low memory footprint and faster startup times discussed above makes it suitable to running in scenarios such as Serverless functions or low memory footprint reactive Microservices.
Reason 3: Micronaut == Spring Boot + Spring Cloud
Micronaut API is similar to Spring Boot. Most of the Java developers are friendly with Spring Boot. So, it is easy for them to start working with Micronaut as well. Micronaut also supports cloud native features like service discovery, client side load balancing, distributed tracing. This gives you all the toolset required to build modern Java web applications.
Installing Micronaut
Micronaut comes with a command-line client that you can use to scaffold applications. The Micronaut CLI can be installed using SDKMan. To install SDKMAN, you can refer to install instructions mentioned on their website. If you are Mac or Unix platform then you can install SDKMAN using following command:
$ curl -s "https://get.sdkman.io" | bash
Next, open a new terminal or enter:
$ source "$HOME/.sdkman/bin/sdkman-init.sh"
Once SDKMan is installed, you can install Micronaut CLI using the following command
$ sdk install micronaut
Once installed, you can use the Micronaut CLI using the mn
command as shown below.
$ mn
| Starting interactive mode... | Enter a command name to run. Use TAB for completion: mn>
Step 1: Creating a new Micronaut app
To create a new Micronaut app, you can run the following command
mn> create-app todo-service --lang=java --build=gradle --features=junit
As you can see from the above command, we created a new Java application that uses Gradle as the build tool. We told Micronaut that we want to use JUnit as our testing framework.
The above command is equivalent to the following command as Java, Gradle, and JUnit are default values.
mn> create-app todo-service
Micronaut supports three languages – Java, Kotlin, and Groovy so you pick them as well. Java is the default choice.
Similarly, Micronaut supports both Maven and Gradle but Gradle is the default choice.
Micronaut supports JUnit and Spock as the two testing frameworks. JUnit is the default choice.
Once application is created, you can open the app in your IDE. If you use IntelliJ then you can open the app using command-line. Please exit from the Micronaut shell and change directory to todo-service
.
$ cd todo-service $ idea .
This will open the application in IntelliJ Idea.
Step 2: Running the application
To run the application, you can either do that via IDE or Gradle. To run the application using Gradle, execute the following command in root folder of your application.
$ ./gradlew run
This should start the application in under 1 second. This the main selling point of Micronaut. Micronaut does compile time dependency injection so it avoid creating proxies at run time.
On MacOS, you might notice that the application takes more than 10 second to start. This is due because on macOS Sierra and above, java.net.InetAddress:getLocalHost
is slow. The solution for this is to modify your /etc/hosts
file and add following two lines.
127.0.0.1 localhost mac.local ::1 localhost mac.local
Replace mac.local
with your hostname. You can find host name of you machine by running hostname
command.
You can read more about this issue in the post by Antonio Troina. Thanks Antonio.
After you make the change, run you app again and this time you will see application started in less than a second.
$ ./gradlew run > Task :run 18:30:59.539 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 726ms. Server Running: http://localhost:8080
As you can see above, application started in 726ms.
Another point in favour of Micronaut is the size of the executable JAR. If you build the application then you can create the executable JAR.
$ ./gradlew clean build
The executable JAR is created inside build/libs
directory.
$ ll build/libs -rw-r--r-- 1 shekhargulati staff 11M Dec 5 18:31 todo-service-0.1-all.jar
As you can see, application JAR is 11M only.
Step 3: Creating a REST controller
We can use Micronaut CLI to create a new controller for us. As you will notice, Micronaut uses Spring terminology. You can also create the class using your IDE.
mn> create-controller todo.service.controllers.TodoController
This will create TodoController
and TodoControllerTest
classes. The code for TodoController is shown below.
package todo.service.controllers; import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Get; import io.micronaut.http.HttpStatus; @Controller("/todo") public class TodoController { @Get("/") public HttpStatus index() { return HttpStatus.OK; } }
The above exposes a GET REST endpoint at /todo
. When you will make a GET call to /todo
then HTTP status code 200 will be returned.
The generated test case for the above code is shown below
package todo.service.controllers; import io.micronaut.context.ApplicationContext; import io.micronaut.http.HttpStatus; import io.micronaut.http.client.RxHttpClient; import io.micronaut.runtime.server.EmbeddedServer; import org.junit.Test; import static org.junit.Assert.assertEquals; public class TodoControllerTest { @Test public void testIndex() throws Exception { try(EmbeddedServer server = ApplicationContext.run(EmbeddedServer.class)) { try(RxHttpClient client = server.getApplicationContext().createBean(RxHttpClient.class, server.getURL())) { assertEquals(HttpStatus.OK, client.toBlocking().exchange("/todo").status()); } } } }
The code shown above creates a Reactive HTTP Client. Micronaut uses RxJava to provide reactive HTTP client.
If you will run this code using your IDE then you might see that test case will fail because of Page not found
error. The error happens because we have not enabled annotation processor in IntelliJ.
Once enabled, you should be able to run the test and it will be green.
Conclusion
In this post, we learnt about a new JVM framework — Micronaut. We started with listing reasons why Java developers should learn Micronaut. Then, we created a hello world Micronaut application using its CLI tool. In the next post, we will build a fully feature Micronaut application.