'Spring @Cacheable and @Async annotation
I have the need to cache some the results of some asynchronous computations. In detail, to overcome this issue, I am trying to use Spring 4.3 cache and asynchronous computation features.
As an example, let's take the following code:
@Service
class AsyncService {
@Async
@Cacheable("users")
CompletableFuture<User> findById(String usedId) {
// Some code that retrieves the user relative to id userId
return CompletableFuture.completedFuture(user);
}
}
Is it possible? I mean, will the caching abstraction of Spring handle correctly the objects of type CompletableFuture<User>? I know that Caffeine Cache has something like that, but I can't understand if Spring uses it if properly configured.
EDIT: I am not interested in the User object itself, but in the CompletableFuture that represents the computation.
Solution 1:[1]
As per SPR-12967, ListenableFuture (CompletableFuture) are not supported.
Solution 2:[2]
The community asks me to do some experiments, so I made them. I found that the answer to my question is simple: @Cacheable and @Async do not work together if they are placed above the same method.
To be clear, I was not asking for a way to directly make the cache return the object owned by a CompletableFuture. This is impossible, and if it isn't so, it will break the contract of asynchronous computation of the CompletableFuture class.
As I said, the two annotations do not work together on the same method. If you think about it, it is obvious. Marking with @Async is also @Cacheable means to delegate the whole cache management to different asynchronous threads. If the computation of the value of the CompletableFuture will take a long time to complete, the value in the cache will be placed after that time by Spring Proxy.
Obviously, there is a workaround. The workaround uses the fact the CompletableFuture is a promise. Let's have a look at the code below.
@Component
public class CachedService {
/* Dependecies resolution code */
private final AsyncService service;
@Cacheable(cacheNames = "ints")
public CompletableFuture<Integer> randomIntUsingSpringAsync() throws InterruptedException {
final CompletableFuture<Integer> promise = new CompletableFuture<>();
// Letting an asynchronous method to complete the promise in the future
service.performTask(promise);
// Returning the promise immediately
return promise;
}
}
@Component
public class AsyncService {
@Async
void performTask(CompletableFuture<Integer> promise) throws InterruptedException {
Thread.sleep(2000);
// Completing the promise asynchronously
promise.complete(random.nextInt(1000));
}
}
The trick is to create an incomplete promise and return it immediately from the method marked with the @Cacheable annotation. The promise will be completed asynchronously by another bean that owns the method marked with the @Async annotation.
As a bonus, I also implemented a solution that does not use the Spring @Async annotation, but it uses the factory methods available in the CompletableFuture class directly.
@Cacheable(cacheNames = "ints1")
public CompletableFuture<Integer> randomIntNativelyAsync() throws
InterruptedException {
return CompletableFuture.supplyAsync(this::getAsyncInteger, executor);
}
private Integer getAsyncInteger() {
logger.info("Entering performTask");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return random.nextInt(1000);
}
Anyway, I shared the complete solution to my GitHub problem, spring-cacheable-async.
Finally, the above is a long description of what the Jira SPR-12967 refers to.
I hope it helps. Cheers.
Solution 3:[3]
Add @Async annotation on methods in one class and @Cacheable annotation at method level in a different class.
Then invoke @Async method from a service or any different layer.
It worked for me, both Redis cache and Async, which improved the performance drastically.
Solution 4:[4]
In theory, it would work as long as
the implementation of CacheManager behind the
@Cacheableis not serializing the cached objects (like a cache backed by Hazelcast)Since the
CompletableFutureholds a state, which can be modified by calling e.g. thecancel()method, it's important that all the users of the API won't mess around with the cached object. Otherwise, there might be the risk that the cached object inside theFuturecould not be retrieved anymore, and a cache eviction would be necessaryIt's worth to verify in which order the proxies behind the annotations are called. i.e. is the
@Cacheableproxy called always before the@Asyncone? Or the other way around? Or it depends? For example, if the@Asyncis called before, it will fire aCallableinside aForkJoinPool, just to then retrieve the other object from the cache.
Solution 5:[5]
I tried the below approach and it seems to work.
- create a method with
@Cachablewhich does the actual business logic - create a method with
@Asyncwhich calls the above@Cachablemethod and returns aCompletableFuture - call the method with
@Asyncin your main execution flow
Example:
public class Main {
public void cachedAsyncData() {
try {
asyncFetcher.getData().get();
} catch(Exception e){}
}
}
public class AsyncFetcher {
@Async
public CompletableFuture<String> getData() {
return CompletableFuture.completedFuture(cacheFetcher.getData());
}
}
public class CacheFetcher {
@Cacheable
public String getData() {
return "DATA";
}
}
Solution 6:[6]
Please add annotation @EnableAsync at @Component or @Serice class levele. Exp:
@Service
@Slf4j
@EnableAsync //Add it to here
public class CachingServiceImpl implements CachingService {
Hope to help you!
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | riccardo.cardin |
| Solution 2 | |
| Solution 3 | Didier L |
| Solution 4 | |
| Solution 5 | ColdSpike |
| Solution 6 | phancuongviet |
