Micronaut Cache

Learn how to use Micronaut's caching annotations

Authors: Sergio del Amo

Micronaut Version: 1.2.7

1 Getting Started

In this guide, you are going to use Micronaut’s caching annotations to speed up your application.

1.1 What you will need

To complete this guide, you will need the following:

  • Some time on your hands

  • A decent text editor or IDE

  • JDK 1.8 or greater installed with JAVA_HOME configured appropriately

1.2 How to complete the guide

We recommend you to follow the instructions in the next sections and create the app step by step. However, you can go right to the completed example.

or

Then, cd into the complete folder which you will find in the root project of the downloaded/cloned project.

2 Writing the Application

Create a Kotlin Micronaut app using the Micronaut Command Line Interface.

mn create-app example.micronaut.complete --lang=kotlin

The previous command creates a micronaut app with the default package example.micronaut in a folder named complete.

Due to the --lang kotlin flag, it generates a Kotlin Micronaut app that uses the Gradle build system. However, you could use other build tools such as Maven or other programming languages such as Java or Groovy.

If you are using Java or Kotlin and IntelliJ IDEA make sure you have enabled annotation processing.

annotationprocessorsintellij

2.1 Configure the Application

In this sample application, we cache news headlines. Configure your caches in application.yml:

src/main/resources/application.yml
micronaut:
  caches:
    headlines: (1)
      charset: 'UTF-8'
1 Configure a cache called headlines.
By default Micronaut’s cache uses, by default, Caffeine.
Check the properties (maximum-size, expire-after-write and expire-after-access) to configure the size and expiration of your caches. It is important to keep the caches' size under control.

2.2 Micronaut Cache Api

Imagine a service which retrieves headlines for a given month. This operation may be expensive and you may want to cache it.

src/main/kotlin/example/micronaut/NewsService.kt
package example.micronaut

import io.micronaut.cache.annotation.CacheConfig
import io.micronaut.cache.annotation.CacheInvalidate
import io.micronaut.cache.annotation.CachePut
import io.micronaut.cache.annotation.Cacheable
import java.time.Month
import java.util.concurrent.TimeUnit
import javax.inject.Singleton

@Singleton (1)
@CacheConfig("headlines") (2)
class NewsService {
    val headlines = mutableMapOf(
            Month.NOVEMBER to listOf("Micronaut Graduates to Trial Level in Thoughtworks technology radar Vol.1",
            "Micronaut AOP: Awesome flexibility without the complexity"),
            Month.OCTOBER to listOf("Micronaut AOP: Awesome flexibility without the complexity"))

    @Cacheable (3)
    fun headlines(month: Month): List<String>? {
        return try {
            TimeUnit.SECONDS.sleep(3) (4)
            headlines[month]
        } catch (e: InterruptedException) {
            null
        }
    }

    @CachePut(parameters = ["month"]) (5)
    fun addHeadline(month: Month, headline: String): List<String>? {
        val l = headlines.getOrDefault(month, emptyList()).toMutableList()
        l.add(headline)
        headlines[month] = l
        return headlines[month]
    }

    @CacheInvalidate(parameters = ["month"]) (6)
    fun removeHeadline(month: Month, headline: String?) {
        val l = headlines.getOrDefault(month, emptyList()).toMutableList()
        l.remove(headline)
        headlines[month] = l
    }
}
1 To register a Singleton in Micronaut’s application context annotate your class with javax.inject.Singleton.
2 Specifies the cache name headlines to store cache operation values in.
3 Indicates a method is cacheable. The cache name headlines specified in @CacheConfig is used. Since the method has only one parameter, you don’t need to specify the month parameters attribute of the the annation.
4 Emulate an expensive operation by sleeping for several seconds.
5 The return value is cached with name headlines for the supplied month. The method invocation is never skipped even if the cache headlines for the supplied month already existed.
6 Method invocation causes the invalidation of the cache headlines for the supplied month.
If you don’t annotate the class with @CacheConfig, specify the cache name in the cache annotations. E.g. @Cacheable(value = "headlines", parameters = {"month"})

2.3 Controller

Create a controller which engages the previous service:

src/main/kotlin/micronaut/example/micronaut/News.kt
package example.micronaut

import io.micronaut.core.annotation.Introspected
import java.time.Month

@Introspected
data class News(val month: Month, val headlines: List<String>)
src/main/kotlin/micronaut/example/micronaut/NewsController.kt
package example.micronaut

import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import java.time.Month

@Controller
class NewsController(val newsService: NewsService) {
    @Get("/{month}")
    fun index(month: Month): News {
        return News(month, newsService.headlines(month).orEmpty())
    }
}

3 Running the app

To run the application use the ./gradlew run command which will start the application on port 8080.

First request will be slow because you will hit the multiple seconds sleep.

curl "http://localhost:8080/OCTOBER"
{"month":"OCTOBER","headlines":["Micronaut AOP: Awesome flexibility without the complexity"]}%

Second request uses the cache and the response is instantaneously.

curl "http://localhost:8080/OCTOBER"
{"month":"OCTOBER","headlines":["Micronaut AOP: Awesome flexibility without the complexity"]}%

4 Generate a Micronaut app's Native Image with GraalVM

Micronaut itself does not rely on reflection or dynamic classloading so works automatically with GraalVM native.

First, add Micronaut Graal’s annotation processor that helps to handle generating the reflection-config.json metadata that is automatically picked up by the native-image tool:

build.gradle
dependencies {
...
..
.
    kapt "io.micronaut:micronaut-graal"
}

GraalVM Native Image allows you to ahead-of-time compile Java code to a standalone executable, called a native image. This executable includes the application, the libraries, the JDK and does not run on the Java VM, but includes necessary components like memory management and thread scheduling from a different virtual machine, called “Substrate VM”. Substrate VM is the name for the runtime components (like the deoptimizer, garbage collector, thread scheduling etc.). The resulting program has faster startup time and lower runtime memory overhead compared to a Java VM.

We need to add a dependency to svm:

build.gradle
dependencies {
...
..
.
    compileOnly "org.graalvm.nativeimage:svm:19.3.0"
}

To simplify building the image you need to create a native-image.properties file. The convention is to use the folder src/main/resources/META-INF/native-image and then a folder following the maven coordinates of the application. For our example example.micronaut/micronautguide.

Add a native-image.properties inside src/main/resources/META-INF/native-image/example.micronaut/micronautguide folder.

src/main/resources/META-INF/native-image/example.micronaut/micronautguide/native-image.properties
Args = -H:IncludeResources=logback.xml|application.yml \
       -H:Name=micronautguide \
       -H:Class=example.micronaut.Application
  • The -H:IncludeResources argument allows you to tweak which static resources to include. You can use regular expressions.

  • The -H:Name argument specifies the name of the native image that will be generated.

  • The -H:Class argument specifies the entry point of your application (the class that defines a static void main method.

To generate the native image you need to generate the FAT JAR first.

$ ./gradlew assemble

Invoke native-image. It may take a minute to complete.

$ which java
/Users/sdelamo/.sdkman/candidates/java/19.3.0.r8-grl/bin/java
$ native-image --no-server -cp build/libs/complete-0.1-all.jar
[micronautguide:39362]    classlist:   3,884.97 ms
[micronautguide:39362]        (cap):   5,919.80 ms
[micronautguide:39362]        setup:   7,222.88 ms
[micronautguide:39362]   (typeflow):  14,037.78 ms
[micronautguide:39362]    (objects):  17,689.40 ms
[micronautguide:39362]   (features):   1,751.37 ms
[micronautguide:39362]     analysis:  35,421.99 ms
[micronautguide:39362]     (clinit):     978.01 ms
[micronautguide:39362]     universe:   2,114.65 ms
[micronautguide:39362]      (parse):   2,327.75 ms
[micronautguide:39362]     (inline):   2,153.46 ms
[micronautguide:39362]    (compile):  13,066.94 ms
[micronautguide:39362]      compile:  19,714.97 ms
[micronautguide:39362]        image:   3,509.84 ms
[micronautguide:39362]        write:   1,114.96 ms
[micronautguide:39362]      [total]:  73,294.82 ms

--no-server options tells to not use server-based image building.

You can invoke the generated native image micronautguide. Startup will be really fast.

 $ ./micronautguide -Xmx68m
07:42:20.792 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 21ms. Server Running: http://localhost:8080

You can invoke the generated native image micronautguide.

First request will be slow because you will hit the multiple seconds sleep.

curl "http://localhost:8080/OCTOBER"
{"month":"OCTOBER","headlines":["Micronaut AOP: Awesome flexibility without the complexity"]}%

Second request uses the cache and the response is instantaneously.

curl "http://localhost:8080/OCTOBER"
{"month":"OCTOBER","headlines":["Micronaut AOP: Awesome flexibility without the complexity"]}%

Alternatively, you can use Dockerfile to construct the native image and a script docker-build.sh to run it:

Dockerfile
FROM oracle/graalvm-ce:19.3.0-java8 as graalvm
COPY . /home/app/micronautguide
WORKDIR /home/app/micronautguide
RUN gu install native-image
RUN native-image --no-server -cp build/libs/complete-*-all.jar

FROM frolvlad/alpine-glibc
EXPOSE 8080
COPY --from=graalvm /home/app/micronautguide /app/micronautguide
ENTRYPOINT ["/app/micronautguide","-Xmx68m"]
docker-build.sh
#!/bin/sh
docker build . -t micronautguide
echo
echo
echo "To run the docker container execute:"
echo "    $ docker run -p 8080:8080 micronautguide"
$ ./gradlew assemble

$ ./docker-build.sh
Sending build context to Docker daemon  64.67MB
Step 1/9 : FROM oracle/graalvm-ce:19.3.0-java8 as graalvm
 ---> d625000893c9
Step 2/9 : COPY . /home/app/micronautguide
 ---> 4c0daf50c6f3
Step 3/9 : WORKDIR /home/app/micronautguide
 ---> Running in e07cf22b668a
Removing intermediate container e07cf22b668a
 ---> d3699ea8e284
Step 4/9 : RUN gu install native-image
 ---> Running in 71edf17b9a11
Downloading: Component catalog from www.graalvm.org
Processing Component: Native Image
Downloading: Component native-image: Native Image  from github.com
Installing new component: Native Image (org.graalvm.native-image, version 19.3.0)
Refreshed alternative links in /usr/bin/
Removing intermediate container 71edf17b9a11
 ---> ee26c71bbd1e
Step 5/9 : RUN native-image --no-server --static -cp build/libs/complete-*-all.jar
 ---> Running in d2cd451f33be
[micronautguide:28]    classlist:   8,773.36 ms
[micronautguide:28]        (cap):   1,044.04 ms
[micronautguide:28]        setup:   2,748.03 ms
[micronautguide:28]   (typeflow):  20,611.56 ms
[micronautguide:28]    (objects):  20,377.15 ms
[micronautguide:28]   (features):   2,168.30 ms
[micronautguide:28]     analysis:  45,559.16 ms
[micronautguide:28]     (clinit):   1,141.31 ms
[micronautguide:28]     universe:   2,719.10 ms
[micronautguide:28]      (parse):   2,808.15 ms
[micronautguide:28]     (inline):   3,094.66 ms
[micronautguide:28]    (compile):  25,648.31 ms
[micronautguide:28]      compile:  34,018.50 ms
[micronautguide:28]        image:   3,811.46 ms
[micronautguide:28]        write:     643.59 ms
[micronautguide:28]      [total]:  99,095.08 ms
Removing intermediate container d2cd451f33be
 ---> 429a467e2fdd
Step 6/9 : FROM frolvlad/alpine-glibc
 ---> 38dd85a430e8
Step 7/9 : EXPOSE 8080
 ---> Using cache
 ---> 643f9d29e3f8
Step 8/9 : COPY --from=graalvm /home/app/micronautguide/micronautguide /app/micronautguide
 ---> 0d41749e3ceb
Step 9/9 : ENTRYPOINT ["/app/micronautguide"]
 ---> Running in 31b4c6c21e68
Removing intermediate container 31b4c6c21e68
 ---> 3ff1de337ef4
Successfully built 3ff1de337ef4
Successfully tagged micronautguide:latest


To run the docker container execute:
    $ docker run -p 8080:8080 micronautguide

You can use docker to run the image with the app’s native-image:

 docker run -p 8080:8080 micronautguide
06:58:26.977 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 27ms. Server Running: http://f8143044b1ee:8080

5 Where To Go From Here?

Read about Micronaut’s Cache Advise. Moreover, check the Micronaut Cache project for more information.

6 Help with Micronaut

OCI sponsored the creation of this Guide. OCI offers several Micronaut services:

Free consultation

The OCI Micronaut Team includes Micronaut co-founders, Jeff Scott Brown and Graeme Rocher. Check our Micronaut courses and learn from the engineers who developed, matured and maintain Micronaut.

Micronaut OCI Team