Key Takeaways
- The Micronaut framework provides a solid foundation for building Cloud Native Java microservices based on a compilation-time approach.
- Tight integration with GraalVM Native Image Ahead-of-Time Compilation (AOT) makes it easy to convert applications to native executables, which has massive benefits, particularly for serverless and microservice workloads.
- Reducing the use of Java reflection, runtime proxy generation, and dynamic classloading has resulted in performance, memory & startup improvements and makes it easier to debug and test Micronaut applications.
- Active compilation-time checking increases type safety and improves developer productivity by surfacing errors at build time rather than runtime.
- A large ecosystem of modules and integrations, such as Micronaut Data for database access, has helped provide further innovations within the Micronaut framework.
This article is part of the article series "Native Compilation Boosts Java". You can subscribe to receive notifications about new articles in this series via RSS. Java dominates enterprise applications. But in the cloud, Java is more expensive than some competitors. Native compilation with GraalVM makes Java in the cloud cheaper: It creates applications that start much faster and use less memory. So native compilation raises many questions for all Java users: How does native Java change development? When should we switch to native Java? When should we not? And what framework should we use for native Java? This series will provide answers to these questions. |
Changing the Perception of Server Side Java
In 2017 the Java Server Side landscape had a perception problem. With the shift to microservices and lighter weight, often containerized runtimes, developers began to notice the relative bloat of traditional Java applications, packaged and deployed to a shared Java Virtual Machine (JVM) on a servlet container. The emergence of Serverless further accelerated this perception.
It was during this time that a team at Object Computing began rethinking how Java frameworks are designed from the ground up. The result is the Micronaut framework, a Java framework that takes a wholly different approach by shifting the computation of how the framework is wired together into the compilation phase with Java annotations. That completely eliminates the need for reflection, runtime generated proxies, and complex dynamic classloading, which are present in traditional Java frameworks.
The first public release of the Micronaut framework in April 2018 triggered a sea change in thinking in the Java space and changed the perception of Java being slow and bloated. Many newer initiatives have taken a similar approach: Move more logic into the build and compilation phase of the application to optimize application startup and eliminate reflection.
The benefits of a build-time approach are clear: By computing more during compilation, the framework is already primed for execution in the most optimal way. And eliminating reflection, dynamic class loading, and runtime generation of proxies gives us further downstream optimization opportunities for both the JIT and, critically, GraalVM’s Native Image tooling. Thanks to this approach, Native Image requires no additional configuration to perform a closed-world static analysis of a Micronaut framework application.
Due to this synergy between the Micronaut framework and GraalVM, the Micronaut framework co-founder Graeme Rocher joined Oracle Labs. Oracle Labs does not only own GraalVM but also, in addition to Object Computing, contributes significantly to the Micronaut framework's ongoing development.
What Is the Micronaut Framework?
A common misconception about the Micronaut framework is that it is solely designed for microservices. In fact, the Micronaut framework features an extremely modular architecture for a range of application types!
At its base, the Micronaut framework implements the JSR-330 dependency injection specification. The framework provides a number of additional built-in features on top that make it a great choice as a general-purpose framework backed by an annotation-based programming model, including:
- Configuration Injection
- Aspect-Oriented Programming concepts such as Interceptors
- Built-in support for many fundamental Cloud Native application concepts such as validation, caching, retry for resiliency, job scheduling, and more.
Micronaut has an HTTP server and HTTP client built on the Netty I/O toolkit.
Users have adopted the Micronaut framework to build serverless applications, command-line applications, and even JavaFX applications.
The Micronaut framework’s solid core foundation provides the basis for an extensive ecosystem of modules that allow Micronaut to solve a range of problems. This flexibility is responsible for the Micronaut framework’s dramatic growth in popularity amongst developers. The following architecture diagram describes how the framework is structured:
The foundational layer is based on Java Annotation Processing (APT), implements compile-time dependency injection, and supports the construction of various modules, including the Netty-based HTTP server. But it also covers other areas, such as Data Access, Security, and JSON Serialization.
Why Should I Use the Micronaut Framework?
The goal of the Micronaut framework is to provide a lightweight alternative to traditional Java frameworks by eliminating completely the dynamic parts of these frameworks that use features such as Java reflection, dynamic class loading, and the runtime generation of proxies and byte code.
The elimination of these aspects of traditional frameworks has a profound impact on improving performance, memory consumption, security, robustness, ease of debugging, and testing. And unlike other solutions, a Micronaut framework application starts up quickly in the JVM, too!
The improvements to startup time often completely eliminate the need to split code between integration and unit testing, greatly improving the code to test cycle time. Too often in the past, we wrote fewer integration tests because the application started up too slowly. The Micronaut framework eliminates this concern and hence doesn’t include extensive mocking facilities for the HTTP layer. Many frameworks do in order to avoid the cost of having to start the application.
Eliminating reflection also helps debugging with a reduction in the size of stack traces which can often be enormous in traditional frameworks.
The Micronaut framework also provides mechanisms and APIs to shift your own code to build-time approaches. That’s why, by integrating directly with the Java compiler, the Micronaut framework can and does produce compilation errors when annotations are used incorrectly, improving the type safety of code and overall developer experience.
Getting Started with the Micronaut Framework
This section will cover how you can get started with the Micronaut framework to build Cloud Native Java microservices.
There are several different ways to get going with the Micronaut framework. At a minimum, you need a JDK for Java SE 8 or later. To use the Native Image feature, you need GraalVM JDK for Java 11 or later.
To create a Micronaut application, you can use one of the wizards integrated into your favorite IDE, for example, IntelliJ IDEA Ultimate or the GraalVM Tools for Micronaut Extension for VSCode.
Alternatively, it is super easy to create a new Micronaut application via the web with Micronaut Launch. That is a project creation wizard that lets you select the kind of application you want to build and the features you want to include. It then generates a ZIP file with the application you can download or lets you push the code to a Github repository of your choosing.
If you feel more at home with the command line, you can also create an application by installing the Micronaut CLI via common methods, including SDKMAN!, Homebrew, etc. Once installed, creating a new application is as simple as:
mn create-app demo –build gradle
However, if you are not keen on installing an additional CLI application, no problem! You can use the Micronaut Launch API via curl directly:
curl https://start.micronaut.io/demo.zip\?build\=gradle -o demo.zip && unzip demo.zip && cd demo
The above commands create applications with the Gradle build tool. You can replace the word “gradle” with “maven” to use Maven instead.
In terms of the project structure, a Micronaut framework project is structure in the same way as any other Java project:
- A Gradle or Maven build (although any build tools can be configured, such as Bazel, for example)
- Configuration is provided through
src/main/resources/application.yml
by default. However, if you are not a YAML fan, you can use Java properties, JSON, HOCON, or TOML as alternatives. - Logging by default is based on the SLF4J + Logback combination via
src/main/resources/logback.xml
. You can swap the SLF4J adapter for other logging systems. - Testing is based on JUnit 5, but support for other test frameworks, including Spock and Kotest for Kotlin, exist as alternatives.
A newly created project features a couple of Java sources to help you get going. The first is an Application.java
class located within src/main/java
that includes the main entry point for a Micronaut application:
package demo;
import io.micronaut.runtime.Micronaut;
public class Application {
public static void main(String[] args) {
Micronaut.run(Application.class, args);
}
}
The call to Micronaut.run(..)
triggers the startup sequence of the framework.
The second generated class is in the src/test/java
directory and tests that the application can start successfully without any error:
package demo;
import io.micronaut.runtime.EmbeddedApplication;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Assertions;
import jakarta.inject.Inject;
@MicronautTest
class DemoTest {
@Inject
EmbeddedApplication<?> application;
@Test
void testItWorks() {
Assertions.assertTrue(application.isRunning());
}
}
This JUnit 5 test is annotated with @MicronautTest
, a JUnit 5 extension that allows a JUnit 5 test to inject any component into the test itself. In this case, it’s the EmbeddedApplication
type that models the running application.
Setting up an IDE for Micronaut Development
Generally speaking, one of the advantages of the Micronaut framework being based on Java Annotation Processing (APT) is that no special build tooling whatsoever is needed to work with the framework. All popular IDEs support annotation processing, although some, such as Eclipse, require you to enable annotation processing explicitly.
Having said that, the increase in popularity of the Micronaut framework has seen IDE vendors develop specific support for the framework. JetBrain’s IntelliJ Ultimate includes excellent tooling for users of the framework, including a project wizard, code completion for configuration, Micronaut Data support, and more.
In addition, there is awesome support in Visual Studio Code via the free GraalVM Extension Pack, which is based on the NetBeans IDE. It includes a Micronaut project creation wizard, code completion for configuration, and integrated native image features for Micronaut applications.
Once you have either of these options installed, simply opening the Gradle or Maven project in the IDE will set everything up, and you are ready to go.
Writing REST APIs
The Micronaut framework supports a wide range of server-side workloads, including REST, gRPC, GraphQL, and Message-Driven microservices with messaging technologies such as Kafka, RabbitMQ, JMS, and MQTT. This introduction will focus on building REST applications with the default Netty-based HTTP server.
Each HTTP route in a Micronaut application is defined by a Java class annotated with the @Controller
annotation. The name of the annotation originates from the Model View Controller MVC pattern. A type annotated with @Controller
can specify one or more methods that map to a particular HTTP verb and URI.
The canonical “Hello World” example can be implemented with a Micronaut controller as follows:
package demo;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
@Controller("/hello")
public class HelloController {
@Get(uri="/{name}", produces=MediaType.TEXT_PLAIN)
String hello(String name) {
return "Hello " + name;
}
}
The controller is mapped to the URI /hello
. A single method annotated with @Get handles an HTTP GET and uses a RFC 5741 URI Template to bind the name
parameter of the method. You start the server by running the main
method of the Application
class from an IDE or use ./gradlew run
or ./mvnw mn:run
. Now you can test the endpoint manually by sending a curl request to the default 8080 port used by the Micronaut HTTP server:
curl -i http://localhost:8080/hello/John
HTTP/1.1 200 OK
date: Mon, 28 Mar 2022 13:08:54 GMT
Content-Type: text/plain
content-length: 10
connection: keep-alive
Hello John
With the Micronaut framework’s strong focus on testing, however, what better way to try out the API than testing? The following is a simple JUnit 5 test for the HelloController example from above:
package demo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
@MicronautTest
public class HelloControllerTest {
@Test
void testHello(@Client("/") HttpClient client) {
var message = client
.toBlocking()
.retrieve("/hello/John");
assertEquals("Hello John", message);
}
}
The above test injects Micronaut’s HTTP Client and sends a GET request to the /hello/John
URI, asserting the result is correct.
One of the huge benefits of the Micronaut framework is executing a test such as the one above is extremely fast and rivals regular unit tests. Even though the @MicronautTest annotation starts the Micronaut server and the test executes a fully HTTP request/response cycle, the test execution speed doesn’t suffer. There is no need to learn an extensive mocking API for the HTTP server! This encourages developers to write more integration tests that provide significant long-term maintainability and code quality benefits.
Accessing a Database
Accessing a database is such a common activity in server-side applications that many frameworks provide simplifications to improve developer productivity in this area. The Micronaut framework is no different.
Micronaut Data is a database access toolkit with a twist, however: Leveraging integration with the Micronaut compiler, Micronaut Data adds compilation-time checking and build-time computation of database queries to improve runtime efficiency.
Much like Spring Data JPA, Micronaut Data allows you to specify Java interfaces using the repository pattern that automatically implement database queries for you at compilation time.
Micronaut Data performs a compilation-time analysis on the method signatures of a repository interface and implements the interface if it can. Otherwise, a compilation error will occur.
Micronaut Data supports several different databases and query formats, including:
- Hibernate and JPA - You can use JPA and Hibernate, and Micronaut Data JPA computes JPA queries during compilation (as described above).
- JDBC and SQL - For those who prefer raw SQL and a simple data mapper rather than Object Relational Mapping (ORM), Micronaut Data JDBC provides a simple solution to read and write Java 17+ records and POJOs from and to a relational database.
- MongoDB - As the most recent addition, Micronaut Data MongoDB integrates directly with the MongoDB driver to encode objects to and from BSON in a completely reflection-free manner using Micronaut Serialization.
- R2DBC - Based on Netty, the Micronaut framework features a reactive, non-blocking core. You can write SQL applications that avoid blocking end-to-end by combining the Micronaut Netty server with the Reactive Database Connectivity (R2DBC) specification and a supported database.
- Oracle Coherence - A distributed data grid for massive scale, Coherence features dedicated integration with Micronaut Data to easily implement repositories backed by a Coherence cluster.
Covering all of the different database access options in the Micronaut framework would be an article series by itself. Luckily, there are some excellent guides on the subject: Check out “Accessing a Database with Micronaut Data JDBC” or “Accessing a Database with Micronaut Data Hibernate/JPA” for help with getting started.
A personal favorite of mine is Micronaut Data JDBC, which is a simple data mapper for JDBC. It is based on compilation-time bean introspections, which eliminate reflection altogether from the persistence layer.
Once you have configured your Gradle or Maven build to include Micronaut Data JDBC, you can create Java 17 records that map to database tables, views, or query results. That is different from JPA, which encourages having a one-to-one mapping between a Java class and a table and completely modeling the schema through the use of associations. These associations introduce concepts such as lazy loading that often lead to performance challenges (such as the infamous N+1 selects problem). The following is an example record definition:
package demo;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.data.annotation.GeneratedValue;
import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.MappedEntity;
@MappedEntity
public record Person(
@Id
@GeneratedValue
@Nullable Long id,
String firstName,
String lastName,
int age
) {}
You can then define repository logic in interfaces that implement the majority of the common logic required to build an application. For example:
package demo;
import java.util.List;
import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.repository.CrudRepository;
@JdbcRepository(dialect=Dialect.H2)
public interface PersonRepository extends CrudRepository<Person, Long> {
Person findByAgeGreaterThan(int age);
List<Person> findByLastNameStartsWith(String str);
}
The above example provides a full set of create, read, update, and delete operations (CRUD) via the CrudRepository super interface. It also defines custom queries using the supported query expressions.
If you have any custom requirements that need more advanced use cases, you can write custom queries, criteria queries, or write JDBC logic directly to bind the results. Micronaut Data JDBC makes it a breeze while being completely free of reflection and runtime-generated proxies. There is no notion of state and session synchronization like in JPA, which helps keep your application super lightweight and performs excellently with GraalVM Native Image.
In addition, every repository interface is checked at compilation time. This prevents repository methods that query non-existing properties or use an unsupported return type, maintaining the type safety of Java while enabling such an awesome dynamic feature.
Building a Native Executable
The Micronaut framework was first released before GraalVM was publicly available. Still, a natural synergy developed between these two great technologies primarily because of the ease with which the Native Image component of GraalVM can turn a Micronaut application into a native executable.
GraalVM Native Image has great support for reflection, runtime proxies, and dynamic class loading in Java. However, the developer does need to provide Native Image with the configuration necessary to declare when and where these usages occur. But with the Micronaut framework, there is no need to declare these usages since a Micronaut application doesn’t use any of these techniques at the framework level! That makes the closed-world analysis during the Ahead-of-Time Compilation (AOT) much simpler for GraalVM Native Image.
Of course, if you engage a third-party library that does rely on reflection, then you would need to declare the usage. But it helps a great deal that the majority of cases within the framework you are using are reflection-free.
The Micronaut Gradle and Micronaut Maven plugins leverage the excellent GraalVM Native Build Tools project by Oracle Labs to help simplify building native executables. So building a native executable with Gradle is as simple as:
./gradlew nativeCompile
The equivalent for Maven is:
./mvnw package -Dpackaging=native-image
Either command will produce a native executable for the platform they run on within the build directory of each tool.
Running the native executable will demonstrate the first huge benefit of going native:
./demo
__ __ _ _
| \/ (_) ___ _ __ ___ _ __ __ _ _ _| |_
| |\/| | |/ __| '__/ _ \| '_ \ / _` | | | | __|
| | | | | (__| | | (_) | | | | (_| | |_| | |_
|_| |_|_|\___|_| \___/|_| |_|\__,_|\__,_|\__|
Micronaut (v3.4.0)
[main] INFO io.micronaut.runtime.Micronaut - Startup completed in 23ms. Server Running: http://localhost:8080
Startup time is reduced to mere milliseconds (23 ms in the example above), and memory consumption drops significantly. With such a huge reduction, it is possible to deploy Micronaut applications to environments with more limited memory constraints or in cases where startup time is critical (serverless workloads, for example).
Note that further research is ongoing to provide even more significant improvements through the Micronaut AOT project, developed at Oracle Labs. It runs an additional static analysis step on the byte code prior to building the native executable to optimize and eliminate dead code paths and perform tasks such as converting YAML to Java to avoid needing a YAML parser at runtime.
Building for the Cloud
Beyond Native Image, the Micronaut framework supports a number of different packaging formats and deployment targets, including:
- A traditional runnable JAR with
./gradlew assemble
or./mvnw
package. - Docker images with
./gradlew dockerBuild or ./mvnw package -Dpackaging=docker
- Docker images containing a native executable from GraalVM Native Image with
./gradlew dockerBuildNative
or./mvnw package -Dpackaging=docker-native
- Custom AWS Lambda runtimes can be built to deploy a Micronaut application to the serverless platform.
- Extensive integration with Kubernetes exists for simplifying deployments to Kubernetes.
Overall, the Micronaut framework delivers a feature set that makes it a great choice for building Cloud Native Java applications, from distributed configuration support to integrated service discovery to modules that provide implementations of common abstractions for cloud providers such as AWS, Google Cloud, Azure, and Oracle Cloud. These abstractions ensure your applications remain portable among cloud providers.
Summary
The Micronaut framework introduced a breath of fresh air into server-side Java workloads. It provides an innovative compilation-time approach and feature set that make it a great candidate for building modern Cloud Native Java applications.
Close integration with GraalVM Native Image and a working relationship with the GraalVM team at Oracle Labs means significant innovations have continued to appear with projects such as Micronaut AOT and Micronaut Serialization (a reflection-free alternative to Jackson Databind).
A vibrant community has emerged around the Micronaut framework with numerous modules materializing that improve developer productivity, including Micronaut Data which includes key integrations with database technologies.
Community feedback continues to drive the development of the framework. Hence if you have any feedback, please don’t hesitate to share ideas for new features and improvements via the Micronaut Community.
This article is part of the article series "Native Compilations Boosts Java". You can subscribe to receive notifications about new articles in this series via RSS. Java dominates enterprise applications. But in the cloud, Java is more expensive than some competitors. Native compilation with GraalVM makes Java in the cloud cheaper: It creates applications that start much faster and use less memory. So native compilation raises many questions for all Java users: How does native Java change development? When should we switch to native Java? When should we not? And what framework should we use for native Java? This series will provide answers to these questions. |