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 an app using the Micronaut Command Line Interface.

mn create-app example.micronaut.complete

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

By default, create-app creates a Java Micronaut app that uses the Gradle build system. However, you could use other build tools such as Maven or other programming languages such as Groovy or Kotlin.

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/java/example/micronaut/NewsService.java
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 javax.inject.Singleton;
import java.time.Month;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@Singleton (1)
@CacheConfig("headlines") (2)
public class NewsService {

    Map<Month, List<String>> headlines = new HashMap<Month, List<String>>() {{
        put(Month.NOVEMBER, Arrays.asList("Micronaut Graduates to Trial Level in Thoughtworks technology radar Vol.1",
                "Micronaut AOP: Awesome flexibility without the complexity"));
        put(Month.OCTOBER, Collections.singletonList("Micronaut AOP: Awesome flexibility without the complexity"));
    }};

    @Cacheable (3)
    public List<String> headlines(Month month) {
        try {
            TimeUnit.SECONDS.sleep(3); (4)
            return headlines.get(month);
        } catch (InterruptedException e) {
            return null;
        }
    }

    @CachePut(parameters = {"month"}) (5)
    public List<String> addHeadline(Month month, String headline) {
        if (headlines.containsKey(month)) {
            List<String> l = new ArrayList<>(headlines.get(month));
            l.add(headline);
            headlines.put(month, l);
        } else {
            headlines.put(month, Arrays.asList(headline));
        }
        return headlines.get(month);
    }

    @CacheInvalidate(parameters = {"month"}) (6)
    public void removeHeadline(Month month, String headline) {
        if (headlines.containsKey(month)) {
            List<String> l = new ArrayList<>(headlines.get(month));
            l.remove(headline);
            headlines.put(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 Test the Cache

We can verify that the cache works as expected:

src/test/java/micronaut/example/micronaut/NewsServiceTest.java
package example.micronaut;

import io.micronaut.test.annotation.MicronautTest;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.Timeout;

import javax.inject.Inject;
import java.time.Month;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;

@TestMethodOrder(MethodOrderer.OrderAnnotation.class) (1)
@MicronautTest (2)
class NewsServiceTest {

    @Inject (3)
    NewsService newsService;

    @Timeout(4) (4)
    @Test
    @Order(1) (5)
    public void firstInvocationOfNovemberDoesNotHitCache() {
        List<String> headlines = newsService.headlines(Month.NOVEMBER);
        assertEquals(2, headlines.size());
    }

    @Timeout(1) (4)
    @Test
    @Order(2) (5)
    public void secondInvocationOfNovemberHitsCache() {
        List<String> headlines = newsService.headlines(Month.NOVEMBER);
        assertEquals(2, headlines.size());
    }

    @Timeout(4) (4)
    @Test
    @Order(3) (5)
    public void firstInvocationOfOctoberDoesNotHitCache() {
        List<String> headlines = newsService.headlines(Month.OCTOBER);
        assertEquals(1, headlines.size());
    }

    @Timeout(1) (4)
    @Test
    @Order(4) (5)
    public void secondInvocationOfOctoberHitsCache() {
        List<String> headlines = newsService.headlines(Month.OCTOBER);
        assertEquals(1, headlines.size());
    }

    @Timeout(1) (4)
    @Test
    @Order(5) (5)
    public void addingAHeadlineToNovemberUpdatesCache() {
        List<String> headlines = newsService.addHeadline(Month.NOVEMBER, "Micronaut 1.3 Milestone 1 Released");
        assertEquals(3, headlines.size());
    }

    @Timeout(1) (4)
    @Test
    @Order(6) (5)
    public void novemberCacheWasUpdatedByCachePutAndThusTheValueIsRetrievedFromTheCache() {
        List<String> headlines = newsService.headlines(Month.NOVEMBER);
        assertEquals(3, headlines.size());
    }

    @Timeout(1) (4)
    @Test
    @Order(7) (5)
    public void invalidateNovemberCacheWithCacheInvalidate() {
        assertDoesNotThrow(() -> {
            newsService.removeHeadline(Month.NOVEMBER, "Micronaut 1.3 Milestone 1 Released");
        });
    }

    @Timeout(1) (4)
    @Test
    @Order(8) (5)
    public void octoberCacheIsStillValid() {
        List<String> headlines = newsService.headlines(Month.OCTOBER);
        assertEquals(1, headlines.size());
    }

    @Timeout(4) (4)
    @Test
    @Order(9) (5)
    public void novemberCacheWasInvalidated() {
        List<String> headlines = newsService.headlines(Month.NOVEMBER);
        assertEquals(2, headlines.size());
    }
}
}
1 Used to configure the test method execution order for the annotated test class.
2 Annotation used to define a Micronaut test
3 Inject NewsService bean.
4 Timeout annotation fails a test if its execution exceeds a given duration. It helps us verify that we are leaveraging the cache.
5 Used to configure the order in which the test method should executed relative to other tests in the class.

2.4 Controller

Create a controller which engages the previous service:

src/main/java/micronaut/example/micronaut/News.java
package example.micronaut;

import io.micronaut.core.annotation.Introspected;

import java.time.Month;
import java.util.List;

@Introspected
public class News {
    private Month month;

    private List<String> headlines;

    public News() {

    }

    public News(Month month, List<String> headlines) {
        this.month = month;
        this.headlines = headlines;
    }

    public Month getMonth() {
        return month;
    }

    public void setMonth(Month month) {
        this.month = month;
    }

    public List<String> getHeadlines() {
        return headlines;
    }

    public void setHeadlines(List<String> headlines) {
        this.headlines = headlines;
    }
}
src/main/java/micronaut/example/micronaut/NewsController.java
package example.micronaut;

import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;

import java.time.Month;

@Controller
public class NewsController {

    private final NewsService newsService;

    public NewsController(NewsService newsService) {
        this.newsService = newsService;
    }

    @Get("/{month}")
    public News index(Month month) {
        return new News(month, newsService.headlines(month));
    }
}

Add a test:

src/test/java/micronaut/example/micronaut/NewsControllerTest.java
package example.micronaut;

import io.micronaut.http.HttpRequest;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.http.uri.UriBuilder;
import io.micronaut.runtime.server.EmbeddedServer;
import io.micronaut.test.annotation.MicronautTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;

import javax.inject.Inject;
import java.time.Month;
import java.util.Arrays;

import static org.junit.jupiter.api.Assertions.assertEquals;

@MicronautTest
public class NewsControllerTest {

    @Inject
    EmbeddedServer server;

    @Inject
    @Client("/")
    HttpClient client;

    @Timeout(4) (1)
    @Test
    void fetchingOctoberHeadlinesUsesCache() {
        HttpRequest request = HttpRequest.GET(UriBuilder.of("/").path(Month.OCTOBER.toString()).build());
        News news = client.toBlocking().retrieve(request, News.class);
        String expected = "Micronaut AOP: Awesome flexibility without the complexity";
        assertEquals(Arrays.asList(expected), news.getHeadlines());

        news = client.toBlocking().retrieve(request, News.class);
        assertEquals(Arrays.asList(expected), news.getHeadlines());
    }
}
1 We call the endpoint twice and verify with the @Timeout annotation that the cache is being used.

3 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 {
...
..
.
    annotationProcessor "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

4 Where To Go From Here?

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

5 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