Intro to Micronaut: A cloud-native Java framework

Micronaut offers ahead-of-time compilation, reactive NIO, and cloud-native support for microservices and serverless development. Could it be your next Java framework?

fabric connections undulating sea of digital tranquility
Getty Images

The Spring framework has long dominated back-end Java development, but several new frameworks challenge that status quo. Micronaut is among the most compelling. Developed by the team that built Grails, Micronaut is made for modern architectures.

This article is a hands-on introduction to Micronaut. We'll start with a simple RESTful API-based application, refactor it for reactive non-blocking IO (reactive NIO), then take a quick look at Micronaut's support for cloud-native development in microservices and serverless architectures.

What's special about Micronaut

Micronaut delivers a slew of benefits gleaned from older frameworks like Spring and Grails. It is billed as "natively cloud native," meaning that it was built from the ground up for cloud environments. Its cloud-native capabilities include environment detection, service discovery, and distributed tracing.

Micronaut also delivers a new inversion-of-control (IoC) container, which uses ahead-of-time (AoT) compilation for faster startup. AoT compilation means the startup time doesn't increase with the size of the codebase. That's especially crucial for serverless and container-based deployments, where nodes are often shut down and spun up in response to demand. 

Micronaut is a polyglot JVM framework, currently supporting Java, Groovy, and Kotlin, with Scala support underway.

Finally, Micronaut supports reactive programming. Developers can use either ReactiveX or Reactor inside the framework. As of Micronaut 3, released in July 2021, Reactor is the recommended approach. (Note that no Reactive libraries are included as transitive dependencies in the new releases.) 

Get started with Micronaut

Micronaut is simple to install on any Unix-based system, including Linux and macOS, via SDKMan. If you are on Windows, download the Micronaut Binary and add it to your path.

Once the installation completes, you'll find the mn tool available on your command line.

Open a shell and find a convenient spot. Type mn create-app micronaut-idg --build maven. Micronaut supports Gradle and Maven via wrappers, so you don't have to install the build tool itself. I prefer Maven. If you like Gradle, leave off the --build maven flag in the previous command. 

If you run the server with mvnw mn:run, you can then hit http://localhost:8080/ in your browser, and it'll give you a default "not found" JSON response. 

If you explore the example project's layout, it is a standard Maven project with a main class at src/main/java/micronaut/idg/Application.java. Note that the main class runs an embedded server. When you make code changes, the Micronaut development server automatically updates the running application. 

Add a Micronaut controller

Just as in Spring MVC, you can add controller classes to map URLs to code handlers. Add a class at src/main/java/micronaut/idg/controller/SimpleController. Let's use this controller to create a text response, as shown in Listing 1. 

Listing 1. Using a Micronaut controller


package micronaut.idg.controller; 

import io.micronaut.http.MediaType; 
import io.micronaut.http.annotation.Controller; 
import io.micronaut.http.annotation.Get; 

@Controller("/simple") 
public class SimpleController { 

    @Get(produces = MediaType.TEXT_PLAIN) 
    public String index() { 
        return "A Simple Endpoint"; 
    } 
} 

Now you can see how easy it is to return a JSON-formatted response, as shown in Listing 2.

Listing 2. The JSON-formatted response


package micronaut.idg.controller; 

import io.micronaut.http.MediaType; 
import io.micronaut.http.annotation.Controller; 
import io.micronaut.http.annotation.Get; 

import java.util.Map; 
import java.util.HashMap; 

@Controller("/simple") 
public class SimpleController { 
 
    @Get(produces = MediaType.APPLICATION_JSON) 
    public Map index() { 
      Map msg = new HashMap(); 
      msg.put("message", "A simple message"); 
      return msg;   

    } 
} 

Listing 2 demonstrates Micronaut's intelligent handling of the produces argument to the @Get annotation. In this case, it issues the JSON-formatted response that we set. 

Add a Micronaut service layer 

Micronaut's IoC implementation is unique under the hood because it runs beforehand, but it's still a complete implementation of the CDI (Contexts and Dependency Injection) specification. That means you can use all the familiar DI annotations you likely know from Spring (like @Inject). 

In Listing 3, we'll wire in a service layer bean to provide a message. In a real application, this class could call out to a datastore or some other remote API via a data access bean. 

Create a src/main/java/micronaut/idg/service folder and add the two files seen in Listing 3—an interface (Simple) and its implementation (SimpleService). 

Listing 3. Create a simple service layer bean


// Simple.java 
package micronaut.idg.service; 

public interface Simple { 
  public String getMessage(); 

} 

// SimpleService.java 
package micronaut.idg.service; 

import jakarta.inject.Singleton; 

@Singleton 
public class SimpleService implements Simple { 
  public String getMessage(){ 
    return "A simple service message"; 

  } 
} 

Now you can use your new service layer by injecting the service into the SimpleController you created in Listing 1. Listing 4 shows the Constructor injection. 

Listing 4. Inject the service bean into the controller


@Controller("/simple") 
public class SimpleController { 

  @Inject 
  private final Simple simpleService; 

  public SimpleController(@Named("simpleService") Simple simple) {  // (1) 
    this.simpleService = simple; 

  } 

  @Get(produces = MediaType.APPLICATION_JSON) 
  public Map index() { 
    Map msg = new HashMap(); 
    msg.put("message", simpleService.getMessage()); 
    return msg; 
  } 
} 

The critical work is done in the line commented "(1)", where the service bean is wired in by name. Now, if you visit http://localhost:8080/simple, you'll see the response from the service layer: {"message":"A simple service message"}

Reactive NIO with Micronaut

Next, let's explore using Micronaut with Reactor. In this case, we'll just refactor our current application to use Reactor and non-blocking IO. The application will perform the same task, but using a non-blocking stack—Reactor and Netty—under the hood.

As I mentioned earlier, Micronaut 3 doesn't include a reactive library by default, so begin by adding the Reactor core to your Maven POM, as shown in Listing 5. 

Listing 5. Add Reactor to the pom.xml 


<dependency> 
    <groupId>io.projectreactor</groupId> 
    <artifactId>reactor-core</artifactId> 
    <version>3.4.11</version> 
</dependency> 

Now you can return to the SimpleController and modify it as shown in Listing 6.

Listing 6. Make the controller non-blocking


import reactor.core.publisher.Mono; 

//... 

@Get 
  public Mono<map> index() { 
    Map msg = new HashMap(); 
    msg.put("message", simpleService.getMessage()); 
    return Mono.just(msg); 
  } 
} 

As you can see, we are just wrapping the same return type (a map of string/string) with the Reactor Mono class. 

Similar support exists for consuming remote services in a reactive manner, so you can run an application entirely on non-blocking IO. 

Using Micronaut's CLI to create new components

You can use Micronaut's command-line tool to stub out components. For instance, if you wanted to add a new controller, you could use the command mn add-controller MyController. This outputs a new controller and the tests for it, as shown in Listing 7. 

Listing 7. Create a new controller with the Micronaut command line


mn create-controller MyController 
| Rendered controller to src/main/java/micronaut/idg/MyControllerController.java 
| Rendered test to src/test/java/micronaut/idg/MyControllerControllerTest.java 

Cloud-native development with Micronaut

I mentioned earlier that Micronaut was built from the ground up for cloud-native microservices and serverless development. One cloud-native concept that Micronaut supports is the federation. The idea of a federation is that several smaller applications share the same settings and can be deployed in tandem. If that sounds an awful lot like a microservices architecture, you are right. The purpose is to make microservice development simpler and keep it manageable. See Micronaut's documentation for more about federated services.

Micronaut also makes it easy to target cloud environments for deployment. As an example, you could target Google Cloud Platform's Docker registry, as shown in Listing 8. 

Listing 8. Deploy a Micronaut application using GCP's Docker registry


./mvnw deploy \ 
     -Dpackaging=docker \ 
     -Djib.to.image=gcr.io/my-org/my-project:latest 

In this case, the project would be pushed to the GCP Docker registry as a Docker image. Note that we've used the Jib Maven plugin, which turns a Java project into a Docker image without requiring you to create an actual Docker file. 

Also notice that we've identified Docker as the packaging tool with -Dpackaging=docker. Once the packaging is complete, you can deploy your project with the GCP command-line tool, as shown in Listing 9. 

Listing 9. Run the Docker image from command line


gcloud run deploy \ 
    --image=gcr.io/my-org/my-project:latest \ 
    --platform managed \ 
    --allow-unauthenticated

Tracing is another cloud-native feature that Micronaut supports. For example, Micronaut makes it fairly straightforward to enable Jaeger distributed tracing via annotations

Listing 10 configures Jaeger to trace all the requests in a microservices application's application.xml file.

Listing 10. Jaeger configuration in application.xml


tracing: 
  jaeger: 
    enabled: true 
    sampler: 
      probability: 1 

Conclusion

Micronaut delivers a collection of features that are wonderful for cloud-native and microservice development. At the same time, the framework makes more traditional API-based development straightforward and simple. And it integrates well with Reactor and Netty for reactive NIO.

Micronaut sits alongside Quarkus, Dropwizard, and other cloud-native Java frameworks. It is a refreshing alternative to existing solutions. 

Copyright © 2022 IDG Communications, Inc.

How to choose a low-code development platform