You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Play Framework中Java实现AsyncCacheApi测试缓存的问题

Implementing play.api.cache.AsyncCacheApi in Java: Generic Parameter Issues with get() and getOrElseUpdate()

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 ClassTag to safely cast values to the target generic type
  • Wrap results in Scala's Option and convert to a Scala Future

3. Build out getOrElseUpdate() properly

This method needs to:

  • Check the cache first for existing values
  • Return the cached value if present
  • Run the orElse supplier 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 InMemorySyncCache class properly implements SyncCacheApi instead 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 ClassTag to validate and cast values without runtime type errors.
  • Seamless Scala/Java Interop: Leverages scala.compat.java8 converters to switch between Java and Scala's future/option types smoothly.
  • Complete Lazy Loading: getOrElseUpdate fully 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.29 15:24:10