Ad Astra: The Micronaut Framework

Part 1 - Liftoff

Michael Hunger
12 min readNov 5, 2018

--

Originally published in German in JavaSpektrum 4/2018 (Summer). Now that Micronaut 1.0 went GA and the grace-period after the print publication is over, I wanted to make my observations available to a wider audience.
Please excuse if any oddly formulated sentences made it through the translation :)

It’s not often that a new application framework is introduced in the Java world. Much less is spoken of milliseconds in the same breath.

Especially for microservices, Serverless (FaaS), mobile applications (Android), and stream processing, fast start-up and efficient operations are important because latency requirements are high. So far, most Java frameworks have not been famous for that.

The developers of OCI around Graeme Rocher have developed Micronaut, a complete (full-stack and cloud-native aka. peak buzzword) framework that combines the convenience of Spring with the speed of handwritten code.

The OCI team brings 10 years of hands-on experience with the development of the Grails-Framework (Groovy) to the table, which itself is based on the Spring stack.

Micronaut is licensed Apache 2.0 and supports application development in Java, Kotlin and Groovy. Since it is not based on reflection, the framework can also be used on Android.

The Idea behind Micronaut

With Micronaut, the well-known and popular features of full-stack frameworks should be preserved, such as

  • dependency injection
  • simple configuration with reasonable standards
  • asynchronous APIs
  • little boilerplate code
  • service discovery
  • monitoring
  • scalable (HTTP) clients
  • asynchronous HTTP servers with routing, middleware, security etc.

To help developers get started, many of the approaches, APIs, and annotations are based on concepts from Spring and Grails. Beans, controllers, jobs, and services are equivalent to their Spring equivalents.

At the same time, the problems with

  • start time
  • memory requirements
  • Proxies / Reflections
  • Integration tests of the full stack

are handled in a better way.

How can one reconcile these two goals — comfort and speed?

Answer: By shifting the configuration, dependency injection, profile activation, route, and finder method resolution from runtime to compile time.

It has long been possible in Groovy, Kotlin, and Scala to use compiler plug-ins to generate bytecode based on configuration, conventions, and annotations, which is then executed very effectively during runtime of the application. In Java itself, this is accomplished with annotation processors (APT), as known from Lombok or Hibernate.

Incidentally, microservice and FaaS projects are much smaller than classic, monolithic applications with their thousands of classes. Thus, both the compile and generation overhead, as well as the setup of the application at startup are much less expensive.

What makes Micronaut special?

Besides the long list of useful features and integrations (more in part 2), the compile-time code generation and weaving is the biggest difference.

Dependency injection is supported like in Spring by an IoC (Inversion of Control) container, whose Micronaut implementation relies on code generation and uses reflection and proxies only in few, exceptional cases.

For all injection and bean provisioning, ASM generated code implements the provisioning and configuration of instances (prototypes, singletons, and other scopes). This removes the classpath and reflection scan during the launch of the application, as well as the caches for all reflection information (annotations, fields, methods, constructors). On the other hand, the JVM can optimize and inline the generated code as usual in the JIT process. Since in complex setups chains of beans must often be instantiated, the performance increase is cumulative.

The code generation takes place in annotation processors (Java), AST transformations (Groovy) or compiler plugins (Kotlin, kapt). The collected annotation information will be made available at runtime using BeanDefinitions.

Constructors, fields or setters can be annotated as usual with @Inject. Beans can optionally be marked as @Singleton or created from Factory @Bean annotated methods. There is also an optionalBeanContext like in Spring, which you rarely need to use.

As elsewhere, there are qualifications for beans with various annotations and also scopes like @Context , @Infrastructure or @ThreadLocal or own variants. @Refreshable is a scope of the beans newly generated by external trigger (eg via API or RefreshEvent ). With @Requires very flexible conditions based on configuration, environment, etc. can be linked to the availability of beans.

Beans in Micronaut, unlike Spring, have no names, just their types and possibly qualifiers, causing fewer ambiguity issues. For this purpose, with @Replaces a replacement bean can be defined in a certain context, if the conditions of the first bean are not met or in a test scenario. Whole groups of beans within a package can be configured with a @Configuration annotation in package-info.java.

All annotated beans in the classpath are discovered and linked during the build process. Dependencies on libraries for persistence, orchestration, etc. are resolved at the same time and their services are made available as beans.

For all APIs, Micronaut consistently supports reactive types to allow efficient use of system resources.

Here is an example service for Meetup groups

 @Singleton
class GroupService {
@Inject GroupRepository repo;
public Single<Group> group (Single<String> id) {
return id.map (value -> repo.findById (value))
}
}
import io.micronaut.context. *
...
GroupService service = BeanContext.run()
.getBean (GroupService.class)
service.group(Single.just('graphdb-berlin')).forEach (...)

Configuration

As in Spring Boot or Grails, configuration information from property-, JSON-, YAML-, or Groovy-files is used directly but can be overridden by environment variables or system properties. If these values ​​are to be type-safe, they can also be defined in classes.

The configuration values ​​are then used in places annotated with @Value (via generated bytecode), the use of such expressions is also helpful within other annotations, e.g.@Controller("${api.version}/list"). Application contexts can be specified with multiple environments, which then form the basis for specific configurations or conditional bean selection. Certain environments (eg tests, cloud) are automatically detected or read from MICRONAUT_ENVIRONMENTS or micronaut.environments .

AOP at Compile-time

For the realization of cross-cutting concerns such as logging, transactions and monitoring, the concepts of AOP are still applicable. Originally, the AOP camp relied on dedicated compilers like aspectj, but in the last 5-10 years most frameworks switched to load time weaving or runtime resolution, for instance fo the @Transactional handling in Spring.

Micronaut now goes back to applying AOP around-advices and adding new class components and behavior at compile time. The original classes are complemented by compiled generated proxies.

For around-advices you just implement a method MethodInterceptor.intercept(MethodInvocationContext) which is called instead of the original (annotated) method which it then can delegate to it, if necessary.

Here is an example around advice for caching:

@Singleton
class CacheInterceptor implements MethodInterceptor<Object,Object> {
@Inject Cache<MethodCall,Object> cache;
public Object intercept(
MethodInvocationContext<Object, Object> context) {
return cache.computeIfAbsent(methodCall(context),
call -> context.proceed);
}
}

And here the @Memoized annotation for our CacheInterceptor

@Around
@Type(CacheInterceptor.class)
@Target(ElementType.METHOD)
public @interface Memoized {}

And finally the application of the Memoized annotation on an expensive method.

... 
@Memoized
BigInteger factorial(BigInteger input) { ... }
...

Since the compile order of classes is not always deterministic, it makes sense to keep your own AOP advices in a separate module (jar) so that they are built first and then available in the project.

Introduction-Advices are used for persistence frameworks and also in Micronauts Http-Client both of which are interface-based.

Micronaut uses these AOP mechanisms for its own purposes

  • Validation, JSR-303 using Hibernate Validator
  • Caching (synchronous and asynchronous), (eg using Caffeine or Redis) @Cacheable
  • Retry with @Retryable also on asynchronous methods
  • Retry for beans (eg if services are (still) not available)
  • Circuit Breaker with @CircuitBreaker
  • @Scheduled execution using @Scheduled
  • @Transactional also for Springs variant via an alias

The Micronaut Tooling currently uses Maven and Gradle as build systems, in IntelliJ the Annotation Processing compiler option has to be activated.

Micronaut applications are assembled into an executable Jar or Docker container and can then be deployed on the respective cloud infrastructure. You can also start them with ./gradlew run.

The included examples are simple Hello-World’s for Java, Kotlin and Groovy, but there are also guides available with executable example-code for specific topics, such as authentication, tracing, error-handling etc.

The sample repository also contains a complete application, a petstore, that has been implemented as a federated microservice architecture. The Microservices are orchestrated by Consul and use Neo4j, MongoDB or Redis as databases, in part they work with GORM and integrate the Twitter API as an example. The frontend is a simple React application that accesses a facade (Storefront) which encapsulates the individual services. The services communicate asynchronously over HTTP, partly also via streaming, the APIs use mostly reactive approaches.

In the following example (WIP), I will consume the meetup.com cities and RSVP APIs (stream.meetup.com/2/rsvps) and then use various microservices to process, store, aggregate these events.

Getting started with the Micronaut CLI

Micronaut can be downloaded as binary releases from the website (repository), but it is easier with SDKman. This will install Micronaut and the command-line mn. According to the documentation, Micronaut works with Java 8 and later (with minimal customization eg. for javax annotations).

sdk install micronautmn --version
| Micronaut Version: 1.0.0
| JVM Version: 1.8.0_172

The most efficient way to manage Micronaut projects is through the mn command line tool, either directly or through an interactive mode that also provides command completion. It can be used to create projects, controllers, http clients, jobs, serverless functions, services and much more. The cli calls are controlled by flags and can be assigned to profiles with templates for code generation. The so-called "features" can currently only be activated automatically when creating the project. To change settings and dependencies them later you have to follow the documentation and make the changes manually. But one can imagine very well to implement such an automation with something like Atomist.

Let’s generate our service

create-app micro-meet-city
| Application created ... / micro-meet-city
cd micro-meet-city

This generates a project with a Gradle Build configuration, the class Application in the package micro.meet.city and src/main/resources/application.yml for configuration.

public class Application {
public static void main (String [] args) {
Micronaut.run (Application.class);
}
}

The port ( server.port ) is randomly selected or fetched from environment variables, but you can also fix it in the configuration.

micronaut:
application:
name: micro-meet-city
server:
port: 8888

With ./gradlew run the application can be started, but it does not do anything without controllers, jobs or other services.

Http Server

The asynchronous Http server in Micronaut is based on Netty and is optimized to provide server-side messaging between services rather than serving primarily as a browser endpoint. Therefore, MIME types for endpoints are defined by default as application/json, and the delivery of static resources must be explicitly enabled. Headers, path and query elements, JSON or form data can be mapped to POJOs or to controller method parameters. Incremental file uploads (MediaType.MULTIPART_FORM_DATA) are also supported, eg via Publisher<PartData> or StreamingFileUpload parameters.

Asynchronous methods are the default choice in Micronaut. Controller methods with reactive result types (eg Observable, Publisher, CompletableFuture etc.) are executed asynchronously in Netty, all others in a dedicated I/O thread pool. This may also be necessary when calling remote, synchronous services. Then you can also annotate those methods with @Blocking.

Server sent events (SSE, text/event-stream) can be sent to the consumer via a Publisher<Event<DataType>>. The full HTTP response with status, header fields, and body is can be managed via HttpResponse instances.

The controller is generated by command line.

mn create-controller City

The generated method is modified to return the path parameter.

@Controller ("/city")
public class CityController {
@Get ("/echo/{text}")
public Single<String> echo (String text) {
return Single.just(">" + text);
}
}

The startup time for the application is really nice, below 1 second:

./gradlew assemble
java -jar build/libs/micro-meet-city-0.1-all.jar
14:24:31.753 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 989ms.
Server Running: http://localhost: 8888

Also, the Apache Bench test is not bad, for my MacBook with a single CPU.

 ab -n5000 -c2 http://localhost:8888/city/echo/test
This is ApacheBench, Version 2.3 <$ Revision: 1807734 $>
Concurrency Level: 2
Time taken for tests: 0.944 seconds
Complete requests: 5000
Failed requests: 0
Total transferred: 475000 bytes
HTML transferred: 30000 bytes
Requests per second: 5295.38 [# / sec] (mean)
Time per request: 0.378 [ms] (mean)
Time per request: 0.189 [ms] (mean, across all concurrent requests)
Transfer rate: 491.27 [Kbytes / sec] received
Percentage of requests within a certain time (ms)
50% 0
66% 0
75% 0
80% 0
90% 0
95% 1
98% 1
99% 1
100% 7 (longest request)

Our controller can now use other services, such as repositories that are simply injected.

But we can also accept data from another API and pass it after processing to another endpoint.

If the controller methods do not call blocking operations, they are still executed in the netty event loop after the parameter information has been read, even if no reactive types were used.

Micronaut controllers, like Spring, also support the RFC-6570 URI template for placeholders in the URI string with a wide range of possibilities, which are explained in detail in the documentation. In addition to variables from the URI, Header , Cookie and Body can be bound to controller method parameters. Besides the usual HTTP verbs, also@Patch, @Trace, @Options are supported.

Http filters (modification of the request or response or for tracing, security) are also asynchronous in Micronaut. They are provided by implementing HttpServerFilter.doFilter and bound to URL patterns via a @Filter annotation.

Micronaut is stateless by default, but can support in-memory or Redis-based sessions as needed, similar to Spring Session.

For the error handling Micronaut goes an interesting way: Methods (optionally annotated with @Error) can be declared in the controller (or globally), which expect a certain exception type as the last parameter, which is then processed in this error handling method.

Http Client

Micronaut supports a declarative Http client defined by @Client annotation on an interface or an abstract class. The implementation of the client is done in Micronaut-AOP through an Introduction-Advice. There is also a low-level HTTP client, eg for tests and reactive streams.

Let’s get information from the Meetup API for Cities: https://api.meetup.com/2/cities?page=10

mn create-client City

We first create two minimal POJOs for the API result and the city.

public class City {
public long id;
public string city;
public string country;
public double lon, lat;
}
class CityResult {
public List <City> results;
}

And adapt the generated client.

@Client("https://api.meetup.com/2")
public interface CityClient {
@Get("/cities{?page}")
public CityResult cities(int page);
}

And use it in our controller:

@Controller("/city")
public class CityController {
@Inject CityClient client; @Get("/list/{count:5}")
public Stream<City> cities(int count) {
return client.cities(5).results.stream();
}
}
curl http://localhost:8888/city/list/1
[{ "Id": 1007712, "city", "Dresden", "country", "de", "lon": 13.739999771118164, "lat": 51.04999923706055}]

Http clients are also very flexible in terms of parameter binding, similar to controller methods you can bind parameters to URI’s, query parameters, headers, cookies or the payload using annotations. The built-in mechanisms for resilience such as @Retryable and @CircuitBreaker can also be applied to Http clients.

Testing

Due to the short startup time of Micronaut applications, they can be started directly in unit and integration tests. Beans can be mocked by @Replaces and @Primary or @Qualifier in the test class path. It is also easy to use test-specific, declarative HTTP clients for your own controllers. Persistance integration testing is often done with database setups that either run directly in the process, or are managed by the test (e.g. with testcontainers).

For application tests you can use the EmbeddedServer .

public class CityControllerTest {
private EmbeddedServer server;
private CityControllerClient client;
@Before
public void setup() {
this.server = ApplicationContext.run(EmbeddedServer.class);
this.client = server.getApplicationContext()
.getBean(CityControllerClient.class);
}
@Test
public void shouldReturnHello() {
String response = client.cities(1).blockingGet();
assertEquals(true, response.contains("\"country\":"));
}
@After
public void cleanup() {
this.server.stop();
}
}

Recurring Jobs

Of course, we do not always want to access the Meetup API, but cache the information in our database (or cache).

For the regular fetching, you create a job in which the Http client is used and which stores the data using a repository.

Micronaut supports jobs with regular calls to @Scheduled methods, which can also be controlled using CRON syntax or configuration parameters.

mn create-job City
mn create-bean CityRepository

Minimalistic CityRepository

@Singleton
public class CityRepository {
private final Map<Long,City> data=new ConcurrentHashMap<>();

public void save(City c) {
data.putIfAbsent(c.id, c);
}
public Stream<City> findByName(String name) {
return data.values().stream()
.filter(c -> c.city.contains(name));
}
}

City job

@Singleton
public class CityJob {
@Inject CityClient client;
@Inject CityRepository repo;
@Scheduled(fixedRate = "5m")
public void process () {
client.cities(5).forEach(repo::save);
}
}

Now we can also use our repository in the controller.

@Inject CityRepository repo;    @Get("/named/{name}")
public Stream<City> cities(String name) {
return repo.findByName(name);
}
curl http://localhost:8888/city/named/Ch[{"id":7724,"city":"Chemnitz","country":"de","lon":12.9,"lat":50.8},
{"id":7,"city":"Cheská Lípa","country":"cz","lon":14.5,"lat":50.7}]

Persistance in Micronaut

Of course, a ConcurrentHashMap is not a substitute for a database.

Micronaut currently supports Redis, relational databases using Hibernate, MongoDB and Neo4j. Except for Redis, object mapping is also offered using GORM (with Groovy). Examples of these database integrations can be found in the Petstore.

You can either specify the respective persistence feature (usually one database per microservice) when creating the project or add the dependencies and configuration manually; the details are explained in the documentation.

mn create-app <name> -feature bolt-neo4j

build.gradle for bolt-neo4j

compile "io.micronaut.configuration: neo4j-bolt"

Top-level entry in application.yml (or via environment variables neo4j.uri )

neo4j:
uri: bolt://localhost

Then you can get a Neo4j driver injected into the repository and use it.

@Inject Driver driver;

public void save(City city) {
try (Session s = driver.session()) {
String statement =
"MERGE (c:City {id:$city.id}) ON CREATE SET c+=$city";
s.writeTransaction(tx ->
tx.run(statement, singletonMap("city", city.asMap())));
}
}

public Stream<City> findByName(String name) {
try (Session s = driver.session()) {
String statement =
"MATCH (c:City) WHERE c.city contains $name RETURN c";
return s.readTransaction(tx ->
tx.run(statement, singletonMap("name",name)))
.list(record ->
new City(record.get("c").asMap())).stream();
}
}

Conclusion

In addition to speed and compactness, Micronaut impresses with its functionality, examples, and documentation. In the part 2, I want to highlight Micronaut’s cloud-native capabilities, Function as a Service (FaaS) support, and the use of reactive approaches. Until then, I’m looking forward to new releases and features and would like to encourage anyone to try the framework too.

References

--

--

Michael Hunger

A software developer passionate about teaching and learning. Currently working with Neo4j, GraphQL, Kotlin, ML/AI, Micronaut, Spring, Kafka, and more.