Play Framework中Java实现AsyncCacheApi测试缓存的问题
Problem Description
I'm trying to implement the play.api.cache.AsyncCacheApi interface in Java, and I'm running into significant difficulties with the generic parameters when implementing the get and getOrElseUpdate methods. Here's my current implementation code:
package common; import akka.Done; import net.sf.ehcache.Element; import play.api.cache.AsyncCacheApi; import play.api.cache.SyncCacheApi; import scala.Function0; import scala.Option; import scala.concurrent.Future; import scala.concurrent.duration.Duration; import scala.reflect.ClassTag; import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; import static scala.compat.java8.FutureConverters.toScala; public class InMemoryCacheJava implements AsyncCacheApi { final Map<String, Element> cache = new HashMap<>(); @Override public SyncCacheApi sync() { return null; } @Override public Future<Done> set(String key, Object value, Duration expiration) { return toScala(CompletableFuture.supplyAsync( () -> { final Element element = new Element(key, value); if (expiration == Duration.Zero()) { element.setEternal(true); } element.setTimeToLive(Math.toIntExact(expiration.toSeconds())); cache.put(key, element); return Done.getInstance(); } )); } @Override public Future<Done> remove(String key) { return toScala(CompletableFuture.supplyAsync( () -> { cache.remove(key); return Done.getInstance(); } )); } @Override public <T> Future<Option<T>> get(String key, ClassTag<T> evidence$2) { return null; } @Override public Future<Done> removeAll() { return toScala(CompletableFuture.supplyAsync( () -> { cache.clear(); return Done.getInstance(); } )); } @Override public <A> Future<A> getOrElseUpdate(String key, Duration expiration, Function0<Future<A>> orElse, ClassTag<A> evidence$1) { return null; } }
Is this implementation approach correct? Since my production code uses the DefaultAsyncCacheApi implementation, I want to use this Map-based in-memory cache for test code. What am I missing here?
Solution
Let's break down the gaps in your current code and fix the core implementations:
1. Don't return null for sync()!
The AsyncCacheApi contract requires a valid SyncCacheApi instance from the sync() method. We'll add an inner class that implements SyncCacheApi and reuses the same underlying cache map to keep behavior consistent.
2. Implement get() with safe generic handling
We need to:
- Fetch the cached
Element - Validate if it's expired (your current code sets TTL but doesn't check expiration)
- Use
ClassTagto safely cast values to the target generic type - Wrap results in Scala's
Optionand convert to a ScalaFuture
3. Build out getOrElseUpdate() properly
This method needs to:
- Check the cache first for existing values
- Return the cached value if present
- Run the
orElsesupplier to fetch the value if missing - Cache the new value once the supplier's future completes
- Return the final result
Corrected Full Implementation
package common; import akka.Done; import net.sf.ehcache.Element; import play.api.cache.AsyncCacheApi; import play.api.cache.SyncCacheApi; import scala.Function0; import scala.Option; import scala.concurrent.Future; import scala.concurrent.duration.Duration; import scala.reflect.ClassTag; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CompletableFuture; import static scala.compat.java8.FutureConverters.toScala; import static scala.compat.java8.OptionConverters.toScala; public class InMemoryCacheJava implements AsyncCacheApi { final Map<String, Element> cache = new HashMap<>(); private final SyncCacheApi syncCache = new InMemorySyncCache(); @Override public SyncCacheApi sync() { return syncCache; } @Override public Future<Done> set(String key, Object value, Duration expiration) { return toScala(CompletableFuture.supplyAsync( () -> { final Element element = new Element(key, value); if (expiration.equals(Duration.Zero())) { element.setEternal(true); } else { element.setTimeToLive(Math.toIntExact(expiration.toSeconds())); } cache.put(key, element); return Done.getInstance(); } )); } @Override public Future<Done> remove(String key) { return toScala(CompletableFuture.supplyAsync( () -> { cache.remove(key); return Done.getInstance(); } )); } @Override public <T> Future<Option<T>> get(String key, ClassTag<T> classTag) { return toScala(CompletableFuture.supplyAsync(() -> { Element element = cache.get(key); // Clean up expired entries and return empty if invalid if (element == null || element.isExpired()) { if (element != null) { cache.remove(key); } return Option.empty(); } Object value = element.getObjectValue(); // Use ClassTag to safely cast to the target type if (classTag.runtimeClass().isInstance(value)) { return toScala(java.util.Optional.of(classTag.runtimeClass().cast(value))); } return Option.empty(); })); } @Override public Future<Done> removeAll() { return toScala(CompletableFuture.supplyAsync( () -> { cache.clear(); return Done.getInstance(); } )); } @Override public <A> Future<A> getOrElseUpdate(String key, Duration expiration, Function0<Future<A>> orElse, ClassTag<A> classTag) { // First attempt to retrieve from cache Future<Option<A>> getFuture = get(key, classTag); // FlatMap to handle cache hit/miss logic return scala.concurrent.Future.flatMap(getFuture, opt -> { if (opt.isDefined()) { return scala.concurrent.Future.successful(opt.get()); } else { Future<A> futureValue = orElse.apply(); // Cache the result once it's available futureValue.onSuccess(result -> set(key, result, expiration), scala.concurrent.ExecutionContext.global()); return futureValue; } }, scala.concurrent.ExecutionContext.global()); } // Inner class for synchronous cache implementation private class InMemorySyncCache implements SyncCacheApi { @Override public <T> Option<T> get(String key, ClassTag<T> classTag) { Element element = cache.get(key); if (element == null || element.isExpired()) { if (element != null) { cache.remove(key); } return Option.empty(); } Object value = element.getObjectValue(); if (classTag.runtimeClass().isInstance(value)) { return toScala(java.util.Optional.of(classTag.runtimeClass().cast(value))); } return Option.empty(); } @Override public <T> T getOrElseUpdate(String key, Function0<T> orElse, Duration expiration, ClassTag<T> classTag) { Option<T> opt = get(key, classTag); if (opt.isDefined()) { return opt.get(); } else { T value = orElse.apply(); set(key, value, expiration); return value; } } @Override public void set(String key, Object value, Duration expiration) { Element element = new Element(key, value); if (expiration.equals(Duration.Zero())) { element.setEternal(true); } else { element.setTimeToLive(Math.toIntExact(expiration.toSeconds())); } cache.put(key, element); } @Override public void remove(String key) { cache.remove(key); } @Override public void removeAll() { cache.clear(); } } }
Key Fixes & Missing Pieces Addressed
- Valid Sync Cache: The inner
InMemorySyncCacheclass properly implementsSyncCacheApiinstead of returning null, adhering to the interface contract. - Expiration Validation: Added checks for expired elements in all get methods, automatically cleaning up stale entries.
- Safe Generic Casting: Uses
ClassTagto validate and cast values without runtime type errors. - Seamless Scala/Java Interop: Leverages
scala.compat.java8converters to switch between Java and Scala's future/option types smoothly. - Complete Lazy Loading:
getOrElseUpdatefully implements the expected behavior: cache check first, supplier invocation on miss, caching the result, and returning the value.
This implementation will work reliably for your test scenarios, mirroring DefaultAsyncCacheApi behavior while using an in-memory Map for storage.
内容的提问来源于stack exchange,提问作者Arthur Clerc-Gherardi

