Why Spring internally wraps values in ValueWrapper
Użycie ValueWrapper w Spring Cache Abstraction zamiast bezpośredniego zwracania wartości ma kluczowe znaczenie dla jednoznacznego rozróżnienia dwóch sytuacji:
- Brak wartości w cache dla danego klucza.
- Wartość
nullzostała jawnie zapisana w cache dla danego klucza.
Rozważmy, co by się stało, gdyby metoda pobierająca z cache (Cache.get(Object key)) zwracała bezpośrednio Object:
// Hipotetyczny interfejs cache zwracający bezpośrednio Object
interface HypotheticalCache {
@Nullable
Object get(Object key);
}
HypotheticalCache cache = ...;
Object value = cache.get("myKey");
if (value == null) {
// Co to oznacza?
// 1. Klucz "myKey" nie istnieje w cache?
// 2. Klucz "myKey" istnieje, a jego wartość to faktycznie null?
// Nie da się tego rozróżnić!
}
Taka implementacja prowadzi do niejednoznaczności. Nie wiadomo, czy null oznacza brak wpisu, czy też to, że dla danego klucza faktycznie przechowujemy wartość null.
Jak ValueWrapper rozwiązuje ten problem?
Interfejs Cache w Springu definiuje metodę get w następujący sposób:
public interface Cache {
// ... inne metody
@Nullable
ValueWrapper get(Object key);
// ... inne metody
}
A ValueWrapper wygląda tak, jak podałeś:
@FunctionalInterface
interface ValueWrapper {
@Nullable
Object get();
}
Teraz przepływ wygląda następująco:
- Wołasz
cache.get("myKey"). - Jeśli klucz "myKey" nie istnieje w cache, metoda
cache.get("myKey")zwrócinull. - Jeśli klucz "myKey" istnieje w cache (nawet jeśli jego przechowywana wartość to
null), metodacache.get("myKey")zwróci instancjęValueWrapper. - Dopiero na tej instancji
ValueWrapperwołasz metodęvalueWrapper.get(), aby uzyskać faktyczną przechowywaną wartość. Ta metoda może zwrócićnull, jeśli taka wartość została zapisana.
Przykład:
Cache cache = ...; // Implementacja cache (np. z EhCache, Caffeine, Redis)
// Scenariusz 1: Klucz nie istnieje w cache
ValueWrapper wrapper1 = cache.get("nieistniejacyKlucz");
if (wrapper1 == null) {
System.out.println("Klucz 'nieistniejacyKlucz' nie został znaleziony w cache.");
}
// Scenariusz 2: Klucz istnieje, a jego wartość to "jakaśWartość"
cache.put("istniejacyKlucz", "jakaśWartość");
ValueWrapper wrapper2 = cache.get("istniejacyKlucz");
if (wrapper2 != null) {
Object wartosc = wrapper2.get(); // wartosc = "jakaśWartość"
System.out.println("Znaleziono wartość: " + wartosc);
}
// Scenariusz 3: Klucz istnieje, a jego wartość to null
cache.put("kluczZNullem", null);
ValueWrapper wrapper3 = cache.get("kluczZNullem");
if (wrapper3 != null) {
Object wartoscNull = wrapper3.get(); // wartoscNull = null
System.out.println("Znaleziono wartość (która jest nullem): " + wartoscNull);
if (wartoscNull == null) {
System.out.println("Potwierdzone: wartość dla 'kluczZNullem' to null.");
}
}
Podsumowując
ValueWrapper działa jako kontener lub opakowanie na wartość z cache. Jego głównym celem jest umożliwienie mechanizmowi cache'owania sygnalizowania, czy dany klucz w ogóle istnieje w cache, niezależnie od tego, czy przechowywana dla niego wartość jest null czy nie.
Fakt, że jest to @FunctionalInterface wynika po prostu z tego, że ma jedną metodę abstrakcyjną (get()). To pozwala na jego ewentualne tworzenie przy użyciu wyrażeń lambda, chociaż w kontekście pobierania z cache, głównie konsumujemy już istniejące instancje ValueWrapper zwrócone przez implementację cache.