pyronaut create example.micronaut.micronautguide
Micronaut Cache
Learn how to use Micronaut caching annotations
1. Getting Started
In this guide, you will use Micronaut caching annotations to speed up your application.
2. What you will need
To complete this guide, you will need the following:
-
Some time on your hands
-
GraalPy installed and the Pyronaut CLI available locally
3. Solution
We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.
-
Download and unzip the source
4. Writing the Application
Create an application using the Pyronaut CLI (Command Line Interface) or Pyronaut Launch
4.1. Configure the Application
In this sample application, you cache news headlines. Add the Micronaut Caffeine Cache dependency, which adds support for caching with Caffeine.
Configure your caches in application.toml:
(1)
[micronaut.caches.headlines]
charset = "UTF-8"
| 1 | Configure a cache called headlines. |
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.
|
4.2. Micronaut Cache API
Imagine a service that retrieves headlines for a given month. This operation may be expensive, and you may want to cache it.
from time import sleep
from jakarta.inject import Singleton
from micronaut.cache.annotation import CacheConfig, CacheInvalidate, CachePut, Cacheable
@Singleton (1)
@CacheConfig("headlines") (2)
class NewsService:
def __init__(self):
self._headlines = {
"NOVEMBER": [
"Micronaut Graduates to Trial Level in Thoughtworks technology radar Vol.1",
"Micronaut AOP: Awesome flexibility without the complexity",
],
"OCTOBER": [
"Micronaut AOP: Awesome flexibility without the complexity",
],
}
@Cacheable (3)
def headlines(self, month: str) -> list[str]:
sleep(3) (4)
return self._headlines[month]
@CachePut(parameters=["month"]) (5)
def add_headline(self, month: str, headline: str) -> list[str]:
headlines = [*self._headlines.get(month, []), headline]
self._headlines[month] = headlines
return headlines
@CacheInvalidate(parameters=["month"]) (6)
def remove_headline(self, month: str, headline: str) -> None:
if month in self._headlines:
self._headlines[month] = [
existing for existing in self._headlines[month] if existing != headline
]
| 1 | Use jakarta.inject.Singleton to designate a class as a 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 annotation. |
| 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 exists. |
| 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"})
|
4.3. Test the Cache
You can verify that the cache works as expected:
from time import monotonic
import pytest
from pyronaut.test import MicronautTest, micronaut_test_fixture
@pytest.fixture
def my_context(request):
fixture = micronaut_test_fixture(
request,
MicronautTest(transactional=False), (2)
)
yield fixture
fixture.stop()
@pytest.fixture
def news_service(my_context):
return my_context["example.micronaut.NewsService"] (3)
def assert_duration_under(seconds: float, operation):
start = monotonic()
result = operation()
duration = monotonic() - start
assert duration < seconds
return result
def test_cacheable_method_uses_cache(news_service):
headlines = news_service.headlines("NOVEMBER")
assert len(headlines) == 2
headlines = assert_duration_under(1, lambda: news_service.headlines("NOVEMBER"))
assert len(headlines) == 2
def test_cache_put_updates_cache(news_service):
news_service.headlines("NOVEMBER")
headlines = news_service.add_headline(
"NOVEMBER",
"Micronaut 1.3 Milestone 1 Released",
)
assert len(headlines) == 3
headlines = assert_duration_under(1, lambda: news_service.headlines("NOVEMBER"))
assert len(headlines) == 3
def test_cache_invalidate_removes_cached_value(news_service):
news_service.headlines("NOVEMBER")
news_service.remove_headline("NOVEMBER", "Micronaut AOP: Awesome flexibility without the complexity")
headlines = news_service.headlines("NOVEMBER")
assert len(headlines) == 1
| 1 | Annotate the class with @MicronautTest so the Micronaut framework will initialize the application context and the embedded server. More info. |
| 2 | Inject NewsService bean. |
4.4. Controller
Create a route that uses the previous service:
from dataclasses import dataclass
from micronaut.serde.annotation import Serdeable
@Serdeable (1)
@dataclass
class News:
month: str
headlines: list[str]
| 1 | Declare the @Serdeable annotation at the type level in your source code to allow the type to be serialized or deserialized. |
from typing import Annotated
from jakarta.inject import Inject
from micronaut.http.annotation import Get
from .news import News
from .news_service import NewsService
news_service: Annotated[NewsService, Inject]
@Get("/{month}") (1)
def index(month: str) -> News:
return News(month, news_service.headlines(month))
| 1 | Maps GET requests to /{month}. |
Add a test:
import pytest
import requests
from pyronaut.test import MicronautTest, micronaut_test_fixture
@pytest.fixture
def my_context(request):
fixture = micronaut_test_fixture(
request,
MicronautTest(transactional=False),
)
yield fixture
fixture.stop()
@pytest.fixture
def client(my_context):
return requests.with_context(my_context)
def test_fetching_october_headlines_uses_cache(client): (1)
expected = "Micronaut AOP: Awesome flexibility without the complexity"
response = client.get("/OCTOBER")
assert response.status_code == 200
assert response.json()["headlines"] == [expected]
response = client.get("/OCTOBER")
assert response.status_code == 200
assert response.json()["headlines"] == [expected]
| 1 | Call the endpoint twice and verify that the cached response is reused. |
5. Running the Application
To run the application, use the pyronaut dev command, which starts the application on port 8080.
You should be able to execute this curl request and see results:
curl localhost:8080/NOVEMBER
6. Next Steps
Read about Micronaut Cache Advice. Also, check the Micronaut Cache project for more information.
7. License
| All guides are released with an Apache License 2.0 for the code and a Creative Commons Attribution 4.0 license for the writing and media (images). |