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.
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:
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:
Micronaut has a number of cool features, cloud-native and otherwise. There are a few aspects that really shine:
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.
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);
}
}
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.
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
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"));
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!")));
}
}
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())
}
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)
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
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.