Set Sail with Micronaut

PUBLISHED ON SEP 7, 2018 — CLOUD, CONTAINER, JAVA

In May 2018, Micronaut was announced with the promise to one-up Spring and JEE for modern applications.

Until now, Spring and JEE are the most widespread application frameworks for Java. Both are powerful, high-quality frameworks that make developers on the JRE highly productive.

But both these frameworks were designed with classic monolithic applications and classic operations in mind, and both frameworks drag along baggage that no longer serves much purpose.

The Problem with Startup Times

Since ever, startup times directly translate into less developer productivity, because they significantly to development round-trip times.

For traditional applications, startup times were less of an issue in production, as application uptimes were measured in months and years. However, in a cloud environment, containers get created and destroyed all the time for a host of reasons. So consistently low startup times are now an important operations KPI.

Spring and JEE typically have startup times that increase linearly with the size of the code base (also memory consumption, but memory usage tends to be dominated by other factors) That is because these frameworks do dependency injection at run time. They have to:

  • Read the byte code of every annotated class
  • Synthesize new annotations to support meta annotations or stereotypes
  • Build (and cache) reflective metadata

Enter Micronaut

The core idea behind Micronaut is to use compile-time annotation processing to do dependency injection and AOP without resorting to reflection. This has two big advantages:

  1. Startup is dramatically faster than with Spring / JEE
  2. Dependency injection and AOP errors are raised during compile time

Micronaut has a number of cool features, cloud-native and otherwise. There are a few aspects that really shine:

  • Polyglot programming: Micronaut officially supports writing applications in Java, Groovy and Kotlin.
  • Cloud Native Features:
    • Service Discovery with Kubernetes, Consul and Eureka
    • Client-side Load Balancing with discovered or pre-configured service instances or Netflix Ribbon
    • Configuration Sharing with Consul
  • Built-in support for Serverless / Functions as a Service
  • Declarative HTTP Clients: You can create managed HTTP clients by simply specifying and annotated interface.
  • Built-in HTTP Server: Micronaut includes it’s own fully reactive and non-blocking HTTP server. No more messing around with Servlet containers.

Declarative HTTP Clients

Micronaut provides declarative annotation-base HTTP clients. The following example shows a declarative client for the Open Policy Agent API:

@Client("http://${opa.host:localhost}:${opa.port:8181}")
interface OpaDataClient {

    @Post(value = "/v1/data/foo/bar/allow")
    Single<OpaDataApiResponse<Boolean>> isFooBarAllowed(Map<String, Object> input);
}

The client code as well as the serializers / deserializers for request and response will be created at compile time.

Even better: If a service Id instead of an URL is used for the @Client annotation, Micronaut supports client-side load balancing. Consider:

@Client("service-id")

You can then use service instances discovered by Consul, Eureka or other Service Discovery servers, or you can use a list of manually provided instances. Micronaut will even health-check these instances for you and suspend them from load balancing if they are unhealthy:

micronaut:
    http:
        services:
            service-id:
                urls:
                    - http://instance-1
                    - http://instance-2
                health-check: true
                health-check-interval: 15s
                health-check-uri: /health

Micronaut HTTP clients also support Retries, Circuit Breaking and Fallbacks. Also, there’s a lower-level API for HTTP clients that allow much more detailed configuration.

HTTP Server

Micronaut does not provide or build on a Servlet container. Instead, it includes its own fully reactive and non-blocking HTTP server built on Netty, supporting RxJava 2.x and Reactor.

As an application developer, you won’t see much of the server itself except for the Spring-Boot-like init boilerplate:

public class Application {

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

Demo Application

But how does it feel to develop an application with Micronaut? To get some hands-on experience, I’ve built a showcase application combining Micronaut and OPA (Open Policy Agent), which is available here: github.com/az82/micronaut-opa-demo.

The application servers Chuck Norris facts. There is a protected endpoint that will reveal Chuck Norris real name, but only to authenticated users. Authentication is offloaded to OPA.

1. Creating the App

Micronaut provides a CLI that makes common tasks like creating an app easy. Of course, the CLI must be installed beforehand.

The following will create an application skeleton with Spock unit tests, declarative HTTP clients and management endpoints enabled:

mn create-app micronaut-opa-demo -f spock,http-client,management

The app still needs to be configured:

micronaut:
    application:
        name: micronaut-opa-demo
    server:
        port: 8080
    endpoints:
        health:
            enabled: true # Enable the health endpoint
            sensitive: false # Do not protect the health endpoint as it will not be exposed

2. Implementing The Controller

Controllers can also be added with the CLI:

mn create-controller DemoController

The nice thing about this is that the Micronaut CLI automatically generates a Spock specification skeleton.

The controller is backed by a static repository of Chuck Norris Quotes. This repository is declared as a singleton using the javax.inject.Singleton annotation

@Singleton
public class ChuckNorrisFacts {

    public String getRandom() { /* ... */ }

}

The repository is wired to the controller using constructor injection. Add two endpoints and we’re done:

@Controller(value = "/", produces = TEXT_PLAIN)
public class DemoController {

    private final ChuckNorrisFacts chuckNorrisFacts;

    @Inject
    public DemoController(ChuckNorrisFacts chuckNorrisFacts) {
        this.chuckNorrisFacts = chuckNorrisFacts;
    }

    @Get("/free")
    public String free() { /* ... */ }

    @Get("/protected")
    public String protectedd() { /* ... */ }

}

For convenience, an index redirect can be added:

return HttpResponse.permanentRedirect(URI.create("/free"));

3. Adding Authentication

First, we need a HTTP client for OPA. OPA’s data API accepts an arbitrary JSON structure as input and always wraps it’s response in a { result: {} } structure.

@Client("http://${demo.opa.host:localhost}:${demo.opa.port:8181}")
interface OpaDataClient {

    @Post(value = "/v1/data/mn/demo/allow")
    Single<OpaDataResponse<Boolean>> isMnDemoAllowed(Map<String, Object> input);

}

Then, we need a Filter that makes the authentication decision. The filter is so complex that it warrants some discussion.

First, while this remarkably looks like a Servlet filter, it is not. It implements a Micronaut-proprietary API. This is important because when using non-blocking IO, the Servlet API would hold some nasty pitfalls.

Second, the command chain in doFilter shows a simple example of reactive programming with ReactJava and Micronaut. OPA is called the result interpreted and if it’s OK, then the request is allowed to proceed. If not, an HTTP 401 Unauthorized status code is returned.

Even this small sample impressively shows the complexity trade-off involved with non-blocking and reactive. The corresponding blocking solution would have had only three lines.

@Filter("/protected/**") // Only filter paths starting with /protected/
public class OpaFilter implements HttpServerFilter {

    /* ... */

    public Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request, ServerFilterChain chain) {
        // Input for OPA to make it's authentication decision on
        Map<String, Object> input = new HashMap<>();
        input.put("headers", request.getHeaders().asMap());
        input.put("method", request.getMethod().toString());
        input.put("path", request.getPath());

        // Call OPA and act on it's decision
        return opaClient.isMnDemoAllowed(input)
                .doOnError(e -> LOGGER.error("Unable to communicate with OPA", e))
                .map(OpaDataResponse::getResult)
                .doOnSuccess(r -> LOGGER.info("On {}, OPA says {}", request.getUri(), r))
                .onErrorReturnItem(false)
                .flatMapPublisher(allowed -> allowed
                        ? chain.proceed(request)
                        : Flowable.fromArray(HttpResponse.unauthorized()
                                .contentType(TEXT_PLAIN)
                                .body("You are not authorized. Use /free, you filthy peasant!")));
    }

}

4. Unit Testing

In our showcase, behavior-driven unit tests are implemented with Spock. For instance, the following example tests that the free endpoint works:

def 'free facts work'() {
    when:
    HttpResponse response = client.toBlocking().exchange('/free', String)

    then:
    response.status == OK
    response.getBody(String).get() =~ /Chuck Norris/
}

Test setup needs to be quite complicated as we want to support running the same tests both against the application backed by a real OPA instance as well as a Mock created with WireMock.

void setupSpec() {
    def opaPort = getenv('demo.opa.port')
    def opaHost = getenv('demo.opa.host')

    // Only start the OPA mock if no host / port for OPA has been pre-set
    if (opaPort == null && opaHost == null) {
        LOGGER.info("Using an OPA Mock on a dynamically assigned port")
        opaMock = createOpaMock(options().dynamicPort())
        //noinspection GroovyAssignabilityCheck
        embeddedServer = ApplicationContext.run(EmbeddedServer, ['demo.opa.port': opaMock.port().toString()])
    } else {
        LOGGER.info("Using an OPA instance at {}:{}", opaHost ?: 'localhost', opaPort ?: '8181')
        embeddedServer = ApplicationContext.run(EmbeddedServer)
    }

    client = embeddedServer.applicationContext.createBean(RxHttpClient, embeddedServer.getURL())
}

5. Performance testing

Performance testing can be done with Gatling just like any other web service. The following scenario was used to evaluate the limits of a low-memory Micronaut setup:

setUp(
scenario("Load test")
    .randomSwitch(
    90.0 -> exec(http("/free")
        .get("/free")
        .check(status is 200)),
    8.0 -> exec(http("/protected (authorized)")
        .get("/protected")
        .header("Authorization", s"Bearer $token")
        .check(status is 200)),
    2.0 -> exec(http("/protected (unauthorized)")
        .get("/protected")
        .check(status is 401)))
    .inject(
    rampUsers(users) over rampDuration,
    constantUsersPerSec(users) during constantDuration))
.protocols(
    http
    .baseURL(s"http://$appHost:$appPort")
    .acceptHeader("text/plain"))
.assertions(
    global.failedRequests.count is 0,
    global.responseTime.percentile3 lte 20,
    global.responseTime.percentile4 lte 100)

6. Containerization & Deployment

The app is containerized using Google JIB, which I present in a prior blog post “Building Containers with Google Jib”.

It can then be deployed to Kubernetes.

OPA needs a policy file, which is provided in a ConfigMap. This policy will evaluate whether the Authorization header contains a Bearer token with the subject micronaut-opa-demo.

apiVersion: v1
kind: ConfigMap
metadata:
  name: micronaut-opa-demo-policies
data:
  example.rego: |
    package mn.demo

    default allow = false

    authorization = {"method": parts[0], "token": parts[1]} { split(input.headers["Authorization"][_], " ", parts) }
    token = {"payload": payload } { io.jwt.decode(authorization.token, [_, payload, _]) }

    allow {
        authorization.method = "Bearer"
        token.payload.sub = "micronaut-opa-demo"
    }

The resource limits are notably low. With this configuration, the app is able to serve 100 requests per instance with acceptable response times.

resources:
    requests:
        cpu: 100m
        memory: 48Mi

Summary

Developing a Micronaut application feels very similar to Spring Boot. But Micronaut brings crucial improvements: Fast startup times, no more Servlet containers, built-in cloud-native. And even in it’s early state Micronaut feels streamlined, efficient and well designed and built. Thankfully, it’s authors do not throw conventions and lessons learned from other frameworks overboard, but built on them. Within this year, Micronaut has promised to provide a GA release. For me, it’s my new default option for a JRE Microservice and I expect to see rapid adoption of this new framework.

See Also