Micronaut - Reloading using Gradle's Continuous Build

I have written about my thoughts on hot loading in general in this post.

Micronaut from version 1.2 started supporting continuous build, but you had to manually add the configuration entries to your project. With the 2.0 version, Micronaut supports 'continuous build' out of the box.

Both Gradle and Micronaut contribute to reload. If you wish to understand how Gradle's continuous build works on a standalone Java project, take a look at this post.

Let's launch the Micronaut CLI using mn command and create a Micronaut application as follows.

mn> create-app com.nareshak.demo.helloservice

Let's cd into helloservice directory and launch mn command. Now let's generate controller as follows.

mn> create-controller com.nareshak.demo.Hello
| Rendered controller to src/main/java/com/nareshak/demo/HelloController.java
| Rendered test to src/test/java/com/nareshak/demo/HelloControllerTest.java

Let's take a look at the generated controller HelloController.java.

package com.nareshak.demo;

import io.micronaut.http.annotation.*;

@Controller("/hello")
public class HelloController {

    @Get(uri="/", produces="text/plain")
    public String index() {
        return "Example Response";
    }
}

Let's run the application from the command-line making use of Gradle's continuous build feature as follows.

➜  helloservice ./gradlew run -t

> Task :run
17:05:41.147 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 718ms. Server Running: http://localhost:8080

You could also use --continuous instead of -t. Let's launch another terminal window and invoke the endpoint "/hello".

➜  ~ curl -i http://localhost:8080/hello
HTTP/1.1 200 OK
Date: Sat, 1 Aug 2020 11:37:03 GMT
content-type: text/plain
content-length: 16
connection: keep-alive

Example Response

Now, that the application is working as expected, Let's modify the message from "Example Response" to "Hello". Let's take a look at the terminal window where the application is running.

17:07:59.449 [micronaut-filewatch-thread] INFO  i.m.r.s.w.e.FileWatchRestartListener - Shutting down server following file change.
17:07:59.450 [Thread-1] INFO  io.micronaut.runtime.Micronaut - Embedded Application shutting down

BUILD SUCCESSFUL in 2m 20s
3 actionable tasks: 1 executed, 2 up-to-date

Waiting for changes to input files of tasks... (ctrl-d to exit)
modified: /Users/naresha/demo/helloservice/src/main/java/com/nareshak/demo/HelloController.java
Change detected, executing build...


> Task :compileJava
Note: Creating bean classes for 1 type elements

> Task :run
17:08:02.334 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 966ms. Server Running: http://localhost:8080

Notice how our change to file "HelloController.java" is detected and Gradle brought up the server with our code changes. Let's validate if the change we made came into effect.

➜  ~ curl -i http://localhost:8080/hello
HTTP/1.1 200 OK
Date: Sat, 1 Aug 2020 11:38:41 GMT
content-type: text/plain
content-length: 5
connection: keep-alive

Hello

Great. We are now getting the updated message "Hello".

Both Gradle and Micronaut have their mechanisms to watch for the changes to the files of interest. Upon noticing a file change, Micronaut shuts down the server (Note the log entry from FileWatchRestartListener above. The next log entry is from the shutdown hook). Once the java process terminates, Gradle re-executes the run task, and another instance of the java process is created to run the Micronaut application.

Now let's take a look at the configurations present in build.gradle which made reloading possible. I have shown only those configurations that are relevant for restart. Also, note that I am using Mac OS, which requires a few additional settings.

build.gradle

configurations {
    // for dependencies that are needed for development only
    developmentOnly
}

dependencies {
    developmentOnly("io.micronaut:micronaut-runtime-osx:$micronautVersion")
}

test.classpath += configurations.developmentOnly

tasks.withType(JavaCompile) {
    options.encoding = "UTF-8"
    options.compilerArgs.addAll([
        '-parameters',
        // enables incremental compilation
        '-Amicronaut.processing.incremental=true',
        '-Amicronaut.processing.annotations=com.nareshak.demo.*',
        "-Amicronaut.processing.group=$project.group",
        "-Amicronaut.processing.module=$project.name",
    ])
}

tasks.withType(JavaExec) {
    classpath += configurations.developmentOnly
    jvmArgs('-XX:TieredStopAtLevel=1', '-Dcom.sun.management.jmxremote')
    if (gradle.startParameter.continuous) {
        systemProperties(
            'micronaut.io.watch.restart':'true',
            'micronaut.io.watch.enabled':'true',
            "micronaut.io.watch.paths":"src/main"
        )
    }
}

For OS X, a development only dependency is added, which enables watching for file changes. OS X file watching functionality makes use of https://github.com/gmethvin/directory-watcher. Then there are configurations for enabling annotation processor in incremental mode. Finally, if we run Gradle in continuous build mode, Micronaut will enable shutting down the server upon a file change.

I have used Micronaut 2.0.1 for the code examples.