1. Introduction
1.绪论
In this article, we’ll explain how Spring WebFlux interacts with @Cacheable annotation. First, we’ll cover some common problems and how to avoid them. Next, we’ll cover the available workarounds. Finally, as always, we’ll provide code examples.
在这篇文章中,我们将解释Spring WebFlux如何与@Cacheable注解进行交互。首先,我们将介绍一些常见问题以及如何避免这些问题。接下来,我们将介绍可用的变通方法。最后,像往常一样,我们将提供代码示例。
2. @Cacheable and Reactive Types
2.@Cacheable和Reactive类型
This topic is still relatively new. At the time of writing this article, there was no fluent integration between @Cacheable and reactive frameworks. The primary issue is that there are no non-blocking cache implementations (JSR-107 cache API is blocking). Only Redis is providing a reactive driver.
这个话题还比较新。在写这篇文章的时候,@Cacheable和反应式框架之间还没有流畅的整合。主要问题是没有非阻塞的缓存实现(JSR-107缓存API是阻塞的)。只有Redis正在提供一个反应式驱动程序。
Despite the issue we mentioned in the previous paragraph, we can still use @Cacheable on our service methods. This will result in caching of our wrapper objects (Mono or Flux) but won’t cache the actual result of our method.
尽管我们在上一段提到了这个问题,我们仍然可以在我们的服务方法上使用@Cacheable。这将导致缓存我们的封装对象(Mono或Flux),但不会缓存我们方法的实际结果。
2.1. Project Setup
2.1.项目设置
Let us illustrate this with a test. Before the test, we need to set up our project. We’ll create a simple Spring WebFlux project with a reactive MongoDB driver. Instead of running MongoDB as a separate process, we’ll use Testcontainers.
让我们用一个测试来说明这一点。在进行测试之前,我们需要设置我们的项目。我们将创建一个简单的Spring WebFlux项目,并配备一个反应式MongoDB驱动程序。我们将使用Testcontainers,而不是将MongoDB作为一个单独的进程运行。
Our test class will be annotated with @SpringBootTest and will contain:
我们的测试类将被注解为@SpringBootTest,并将包含。
final static MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:4.0.10"));
@DynamicPropertySource
static void mongoDbProperties(DynamicPropertyRegistry registry) {
mongoDBContainer.start();
registry.add("spring.data.mongodb.uri", mongoDBContainer::getReplicaSetUrl);
}
These lines will start a MongoDB instance and pass the URI to SpringBoot to auto-configure Mongo repositories.
这几行将启动一个MongoDB实例,并将URI传给SpringBoot以自动配置Mongo存储库。
For this test, we’ll create ItemService class with save and getItem methods:
对于这个测试,我们将创建带有save和getItem方法的ItemService类。
@Service
public class ItemService {
private final ItemRepository repository;
public ItemService(ItemRepository repository) {
this.repository = repository;
}
@Cacheable("items")
public Mono<Item> getItem(String id){
return repository.findById(id);
}
public Mono<Item> save(Item item){
return repository.save(item);
}
}
In application.properties, we set loggers for cache and repository so we can monitor what is happening in our test:
在application.properties中,我们为cache和repository设置了记录器,这样我们就可以监控测试中发生的情况。
logging.level.org.springframework.data.mongodb.core.ReactiveMongoTemplate=DEBUG
logging.level.org.springframework.cache=TRACE
2.2. Initial Test
2.2.初始测试
After the setup, we can run our test and analyze the result:
设置完成后,我们可以运行我们的测试并分析结果。
@Test
public void givenItem_whenGetItemIsCalled_thenMonoIsCached() {
Mono<Item> glass = itemService.save(new Item("glass", 1.00));
String id = glass.block().get_id();
Mono<Item> mono = itemService.getItem(id);
Item item = mono.block();
assertThat(item).isNotNull();
assertThat(item.getName()).isEqualTo("glass");
assertThat(item.getPrice()).isEqualTo(1.00);
Mono<Item> mono2 = itemService.getItem(id);
Item item2 = mono2.block();
assertThat(item2).isNotNull();
assertThat(item2.getName()).isEqualTo("glass");
assertThat(item2.getPrice()).isEqualTo(1.00);
}
In the console, we can see this output (only essential parts are shown for brevity):
在控制台中,我们可以看到这样的输出(为简洁起见,只显示基本部分)。
Inserting Document containing fields: [name, price, _class] in collection: item...
Computed cache key '618817a52bffe4526c60f6c0' for operation Builder[public reactor.core.publisher.Mono...
No cache entry for key '618817a52bffe4526c60f6c0' in cache(s) [items]
Computed cache key '618817a52bffe4526c60f6c0' for operation Builder[public reactor.core.publisher.Mono...
findOne using query: { "_id" : "618817a52bffe4526c60f6c0"} fields: Document{{}} for class: class com.baeldung.caching.Item in collection: item...
findOne using query: { "_id" : { "$oid" : "618817a52bffe4526c60f6c0"}} fields: {} in db.collection: test.item
Computed cache key '618817a52bffe4526c60f6c0' for operation Builder[public reactor.core.publisher.Mono...
Cache entry for key '618817a52bffe4526c60f6c0' found in cache 'items'
findOne using query: { "_id" : { "$oid" : "618817a52bffe4526c60f6c0"}} fields: {} in db.collection: test.item
On the first line, we see our insert method. After that, when getItem is called, Spring checks the cache for this item, but it’s not found, and MongoDB is visited to fetch this record. On the second getItem call, Spring again checks cache and finds an entry for that key but still goes to MongoDB to fetch this record.
在第一行,我们看到了我们的插入方法。之后,当getItem被调用时,Spring检查了缓存中的这个项目,但没有找到,于是访问MongoDB来获取这个记录。在第二次调用getItem时,Spring再次检查缓存并找到该键的条目,但仍然去MongoDB获取该记录。
This happens because Spring caches the result of the getItem method, which is the Mono wrapper object. However, for the result itself, it still needs to fetch the record from the database.
这是因为Spring缓存了getItem方法的结果,也就是Mono包装对象。然而,对于结果本身,它仍然需要从数据库中获取记录。
In the following sections, we’ll provide workarounds for this issue.
在下面的章节中,我们将提供解决这个问题的方法。
3. Caching the Result of Mono/Flux
3.缓存Mono/Flux的结果
Mono and Flux have a built-in caching mechanism that we can use in this situation as a workaround. As we previously said, @Cacheable caches the wrapper object, and with a built-in cache, we can create a reference to the actual result of our service method:
Mono和Flux有一个内置的缓存机制,我们可以在这种情况下作为一种变通方法使用。正如我们之前所说, @Cacheable 缓存了包装对象,通过内置的缓存,我们可以创建一个对我们服务方法实际结果的引用。
@Cacheable("items")
public Mono<Item> getItem_withCache(String id) {
return repository.findById(id).cache();
}
Let’s run the test from the last chapter with this new service method. The output will look like following:
让我们用这个新的服务方法运行上一章的测试。其输出结果将如下。
Inserting Document containing fields: [name, price, _class] in collection: item
Computed cache key '6189242609a72e0bacae1787' for operation Builder[public reactor.core.publisher.Mono...
No cache entry for key '6189242609a72e0bacae1787' in cache(s) [items]
Computed cache key '6189242609a72e0bacae1787' for operation Builder[public reactor.core.publisher.Mono...
findOne using query: { "_id" : "6189242609a72e0bacae1787"} fields: Document{{}} for class: class com.baeldung.caching.Item in collection: item
findOne using query: { "_id" : { "$oid" : "6189242609a72e0bacae1787"}} fields: {} in db.collection: test.item
Computed cache key '6189242609a72e0bacae1787' for operation Builder[public reactor.core.publisher.Mono...
Cache entry for key '6189242609a72e0bacae1787' found in cache 'items'
We can see almost similar output. Only this time, there is no additional database lookup when an item is found in the cache. With this solution, there is a potential problem when our cache expires. Since we are using a cache of a cache, we need to set appropriate expiry times on both caches. The rule of thumb is that Flux cache TTL should be longer than @Cacheable.
我们可以看到几乎类似的输出。只是这一次,当在缓存中找到一个项目时,没有额外的数据库查询。有了这个解决方案,当我们的缓存过期时就会有一个潜在的问题。 由于我们使用的是一个缓存的缓存,我们需要在两个缓存上设置适当的过期时间。经验法则是,Flux缓存的TTL应该长于@Cacheable./strong>。
4. Using Reactor Addon
4.使用Reactor Addon
Reactor 3 addon allows us to use different cache implementations in a fluent way with CacheMono and CacheFlux classes. For this example, we’ll configure the Caffeine cache:
Reactor 3插件允许我们通过CacheMono和CacheFlux类以流畅的方式使用不同的缓存实现。在这个例子中,我们将配置Caffeine>缓存。
public ItemService(ItemRepository repository) {
this.repository = repository;
this.cache = Caffeine.newBuilder().build(this::getItem_withAddons);
}
In the ItemService constructor, we initialize the Caffeine cache with minimum configuration, and in the new service method, we use that cache:
在ItemService构造函数中,我们以最小的配置初始化Caffeine缓存,而在新服务方法中,我们使用该缓存。
@Cacheable("items")
public Mono<Item> getItem_withAddons(String id) {
return CacheMono.lookup(cache.asMap(), id)
.onCacheMissResume(() -> repository.findById(id).cast(Object.class)).cast(Item.class);
}
Because CacheMono internally works with the Signal class, we need to do some casting to return appropriate objects.
因为CacheMono在内部与Signal类一起工作,我们需要做一些转换来返回适当的对象。
When we re-run the test from before, we’ll get similar output as in the previous example.
当我们重新运行之前的测试时,我们会得到与之前的例子类似的输出。
5. Conclusion
5.总结
In this article, we covered how Spring WebFlux interacts with @Cacheable. In addition, we described how they could be used and some common problems. As always, code from this article can be found over on GitHub.
在这篇文章中,我们介绍了Spring WebFlux如何与@Cacheable进行交互。此外,我们还介绍了如何使用它们以及一些常见的问题。一如既往,本文的代码可以在GitHub上找到over。