Java in the Enterprise: Caching in Java EE

Anatole Tresch
Anatole Tresch ist zurück mit seiner Kolumne “Java in the Enterprise”. Nachdem im letzten Beitrag eine kurze Einführung in das Thema „Caching“ und die grundsätzliche Funktionsweise des JSR 107 API gegeben wurde, geht es im zweiten Teil um weitere Anwendungsfälle und Funktionen sowie die Grundlagen des SPI.
Atomare Operationen
Im ersten Teil dieser Einführung in das Java Chaching API habe ich das Map
-ähnliche Grund-API eines Caches gezeigt. Nun definiert JSR 107 zusätzliche atomare Funktionen, welche auf einem Cache
ausgeführt werden können:
void remove(K key, V value)
entfernt einen vorhandenen Eintrag genau dann, wenn der aktuelle Wert mit dem übergegebenen Wert übereinstimmt.V getAndRemove(K key, V value)
entfernt einen vorhandenen Eintrag und gibt den entfernten Wert zurück.boolean replace(K key, V oldValue, V newValue)
ersetzt den aktuellen Wert mit newValue, wenn der aktuelle Wert mit dem übergegebenen oldValue übereinstimmt.boolean replace(K key, V newValue)
ersetzt den aktuellen Wert mit newValue, wenn ein Eintrag zum Schlüssel key vorhanden ist.V getAndReplace(K key, V value)
ersetzt den aktuellen Wert mit newValue, wenn ein Eintrag zum Schlüssel key vorhanden ist, und gibt den zuletzt gültigen Wert zurück.<T> T invoke(K key, EntryProcessor<K,V,T> entryProcessor,
Object… arguments)
throws EntryProcessorException
Diese Methode führt eine EntryProcessor (funktionale Schnittstelle) gegen einen Eintrag mit dem angegebenen Schlüssel aus. Wenn der Eintrag nicht vorhanden ist, wird versucht, mit Hilfe des konfiguriertenCacheLoader
, den Wert zu laden. Ist der Wert immer nochnull
, wird ein SurrogatCache.Entry
mit dem Wertnull
an den Prozessor übergeben. Entry-Prozessoren erlauben, Cache-Einträge atomar zu löschen oder zu aktualisieren.<T> T invokeAll(Set<? extends K> keys,
EntryProcessor<K,V,T> entryProcessor,
Object... arguments)
throws EntryProcessorException
Diese Methode macht das gleiche, wie die Methode davor, jedoch für mehrere Schlüssel gleichzeitig.void loadall(Set<? extends K> keys,
Diese Methode ruft den konfigurierten
boolean replaceExistingValues,
CompletionListener completionListener)CacheLoader
auf, um die angeforderten Schlüssel zu laden. Optional kann hier einCompletionListener
übergeben werden.
Manuelle Konfiguration von Caches
Grundsätzlich können Caches auch mittels Vendor-spezifischen Konfigurationen definiert und mit dem JSR 107 API verwaltet werden. JSR 107 definiert aber auch eine programmatische Schnittstelle, um Caches zu konfigurieren. Nachfolgend werden wir uns die konfigurierbaren Aspekte näher betrachten. Instanzen von Configuration
können dann einem CacheManager
übergeben werden, der daraus einen neuen Cache
erzeugt:
Configuration<K,V> config = …;
CacheManager manager = …;
Cache<K,V> cache = manager.createCache("helloCache", config);
Expiry Policies
Bei der Arbeit mit Caches sprengt die Menge von Daten im Cache womöglich die vorhandenen Ressourcen der aktuellen Laufzeitumgebung. Um Fehler aufgrund von fehlenden Ressourcen zu vermeiden, können gecachte Einträge automatisch wieder entfernt werden. Dies wird mit Hilfe einer ExpiryPolicy
konfiguriert, die definiert, nach welcher Zeitdauer Cache-Einträge in Abhängigkeit von Erstellung, Zugriff und/oder einer Änderung wieder entfernt werden können. Die ExpiryPolicy
kann beim Erstellen eines Caches konfiguriert werden und ist wie folgt definiert:
public interface ExpiryPolicy{ Duration getExpiryForCreation(); // duration after creation until expiry Duration getExpiryForAccess(); // duration after access until expiry Duration getExpiryForUpdate(); // duration after update until expiry }
Mit Duration.ZERO
wird signalisiert, dass der Eintrag sofort entfernt werden kann. Geben die Methoden getExpiryForUpdate
oder getExpiryForAccess
null
zurück, so werden die Standardeinstellungen verwendet (zum Beispiel des verwaltenden CacheManagers
). Das Paket javax.cache.expiry
stellt dazu verschiedene Klassen zur Verfügung:
AccessedExpiryPolicy
definiert eine Policy, bei der ein Cache-Eintrag auf Basis seines letzten Zugriffs entfernt wird.CreatedExpiryPolicy
definiert eine Policy, bei der ein Cache-Eintrag auf Basis seines Erstellungszeitpunktes entfernt wird.EternalExpiryPolicy
definiert eine Policy, bei der ein Cache-Eintrag nie entfernt wird.ModifiedExpiryPolicy
definiert eine Policy, bei der ein Cache-Eintrag auf Basis seines Änderungszeitpunktes entfernt wird.TouchedExpiryPolicy
definiert eine Policy, bei der ein Cache-Eintrag auf Basis seines Zugriffszeitpunktes entfernt wird.
Integration
JSR 107 definiert weitere Interfaces, welche die Integration eines Caches mit externer Funktionalität vereinfachen sollen. Diese Artefakte können konfiguriert werden, wenn eine neue Cache-Instanz erstellt wird (wird später gezeigt):
- Der
CacheLoader
abstrahiert den Mechanismus, wie Cache-Einträge vom Backend geladen werden können. - Der
CacheWriter
abstrahiert den Mechanismus, wie Cache-Einträge, welche geändert oder gelöscht werden, in das Backend repliziert werden.
Read-through and Write-through
Read-through und Write-Through sind weitere Aspekte, die für einen Cache konfiguriert werden können. Read-Through löst dabei jedes Mal einen Aufruf des CacheLoader
aus, wenn ein Eintrag gelesen wird. Write-Through hingegen ruft den CacheWriter
jedes Mal auf, wenn Werte hinzugefügt, geändert oder gelöscht werden.
Service Provider Interface (SPI)
Das wichtigste Interface ist der CachingProvider
. Diese Schnittstelle modelliert eine Factory, über die auf Instanzen von CacheManager
zugegriffen werden, welche mit den folgenden Parametern konfiguriert werden:
- Den zu verwendeten
ClassLoader
- Einen providerabhängigen Cache-
URI
. Als Beispiel kann eine Konfigurationsdatei im Klassenpfad referenziert werden:classpath://META-INF/cacheconfig.xml
. Die unterstützten URIs sind Vendor-abhängig. - Weitere
Properties
sind Vendor-abhängig.
Die CachingProvider
-Schnittstelle definiert auch Methoden, um gewisse Standardwerte zu definieren, die immer dann zum Zug kommen, wenn diese als Parameter nicht explizit übergeben werden:
public interface CachingProvider extends Closeable { CacheManager getCacheManager(URI uri, ClassLoader classLoader, Properties properties); CacheManager getCacheManager(URI uri, ClassLoader classLoader); CacheManager getCacheManager(); ClassLoader getDefaultClassLoader(); URI getDefaultURI(); Properties getDefaultProperties(); void close(); void close(ClassLoader classLoader); void close(URI uri, ClassLoader classLoader); boolean isSupported(OptionalFeature optionalFeature); }
Die Aufzählung OptionalFeature
ist wie folgt definiert:
public enum OptionalFeature { STORE_BY_REFERENCE }
Implementierungen von CachingProvider
werden mit dem java.util.ServiceLoader
registriert. Der zu verwendende Default CachingProvider
kann mit dem System Property javax.cache.CachingProvider
gesetzt werden.
Ein Beispiel
Im folgenden Beispiel benutzen wir einen einfachen Cache, um die Antwort für eine bestimmte Servlet-Anfrage für zehn Sekunden zu cachen. Um unseren Cache entsprechend zu konfigurieren, benötigen wir eine Instanz von Configuration<K,V>
, die sowohl Schlüssel- und Wertetyp, als auch ein Boolean-Flag für die Eigenschaft storeByValue definiert. Für das Erstellen einer Configuration
bedienen wir uns der Klasse MutableConfiguration<K,V>
, welche CompleteConfiguration<K,V>
erweitert und am Ende dem CacheManager
zum Erstellen des Caches übergeben wird.
In der GET-Methode des Servlet schließlich wird geprüft, ob ein gecacheder Wert vorhanden ist. Ist dies der Fall, so wird dieser in die Antwort integriert. Andernfalls wird ein neuer Wert erzeugt und für die nächsten 10 Sekunden im Cache abgelegt. Als Resultat wird das Servlet immer für jeweils 10 Sekunden die Antwort aus dem Cache zurückliefern:
import javax.cache.Cache; import javax.cache.CacheBuilder; import javax.cache.CacheManager; import javax.cache.Caching; ... @WebServlet("/helloDemo") public class HelloServlet extends HttpServlet { private volatile Cache cache; public Cache cache(){ if (cache == null){ synchronized(this){ if (cache == null){ //building a cache CacheManager manager = Caching.getCachingProvider().getCacheManager(); MutableConfiguration<String, String> config = new MutableConfiguration<String, String>(); config.setTypes(String.class, String.class) .setStoreByValue(false) .setStatisticsEnabled(true) .setExpiryPolicyFactory( FactoryBuilder.factoryOf( new AccessedExpiryPolicy(new Duration( TimeUnit.HOURS, 1)))); cache = manager.createCache("helloCache", config); } } } return cache; } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/plain"); response.getWriter().append("\n"); String message = cache().get("message"); if (message == null) { message = "HELLO WORLD at POSIX/ms: " + System.currentTimeMillis(); cache().put("message", message); } response.getWriter().append(message); response.getWriter().append("\n"); } }
Caching Annotations
JSR107 definiert einen Satz von Caching-Annotationen, welche es erlauben, Cache-Operationen auf Methoden-Ebene deskriptiv zu deklarieren. Grundsätzlich bilden diese Annotationen die zugrunde liegenden Cache-Operationen ab. Es sind folgende Annotationen verfügbar:
● @CacheDefaults
– Annotation auf Ebene der Klasse, um Standardeigenschaften zu definieren, welche von den Methodenannotationen genutzt werden. Die Annotation besitzt folgende Eigenschaften:
○ cacheName konfiguriert den benutzenden Cache. Wenn nichts festgelegt wurde, wird ein Default Cache-Name generiert.
○ cacheResolverFactory erlaubt es, eine CacheResolverFactory
zu konfigurieren, um den zu benutzenden Cache dynamisch aufzulösen.
○ cacheKeyGenerator ermöglicht es, einen CacheKeyGenerator
zu registrieren, der es erlaubt, den Schlüssel des zu lesenden/schreibenden Eintrages aufgrund der aktuellen Parameter dynamisch zu evaluieren. Ein möglicher Anwendungsfall wäre zum Beispiel die transparente Berücksichtigung des authentifizierten Benutzers, um einen Cache pro Benutzer zu isolieren.
@CacheResult
– erlaubt es, Methoden zu annotieren, deren Rückgabewert gecached werden soll. Standardmäßig wird dabei ein Schlüssel erzeugt, welcher alle übergebenen Methodenparameter berücksichtigt. Die Annotation besitzt folgende Eigenschaften: cacheName, cacheResolverFactory, cacheKeyGenerator- cachedExceptions und nonCachedExceptions erlauben es zu definieren, ob und welche geworfenen Exceptions gecached werden sollen.
- exceptionCacheName erlaubt es, für den Fehlerfall einen alternativen Cache zu benutzen.
- skipGet überspringt das Lesen des Caches beim Methodeneintritt, das heißt, die Methode schreibt immer nur die Werte in den Cache.
- Mit
@CacheKey
können auch lediglich Teilmengen der Methodenparameter für die Schlüsselgenerierung berücksichtigt werden. @CachePut
– kennzeichnet einen Methodenparameter, der im Cache gespeichert werden soll.- @CacheRemove – markiert eine Methode, die Einträge aus dem Cache entfernt. Die Annotation besitzt die folgenden Eigenschaften: cacheName, cacheResolverFactory, cacheKeyGenerator.
- afterInvocation ermöglicht zu kontrollieren, ob der Eintrag nach Ausführung der Methode entfernt werden soll (default) oder vor der Ausführung.
- evictFor erlaubt es, Ausnahmen zu definieren, welche zu einem Entfernen des Eintrages führen.
- noEvictFor erlaubt es, Ausnahmen zu definieren, welche nicht zu einem Entfernen des Eintrages führen.
@CacheRemoveAll
– Diese Annotation erlaubt es, Methoden zu annotieren, welche den Cache komplett zurücksetzen. Die Annotation ist analog zu@CacheRemove
.
Bei der Implementierung der Interfaces CacheResolverFactory
oder CacheKeyGenerator
wird man auch auf eine Metadaten-Schnittstelle treffen, welche wir hier nicht weiter ansehen werden. Wer sich aber mit Reflection oder CDI etwas näher auskennt, wird sich sofort zurechtfinden.
Nachfolgend ist eine skelettartige Implementierung eines Daten-DAOs wiedergegeben, welche exemplarisch die Benutzung der erwähnten Annotationen zeigt:
@CacheDefaults(cacheName=”domainCache”) public class DomainDao { @CacheResult public Domain getDomain(String domainId, Locale locale){...} @CacheRemove public void deleteDomain(String domainId, Locale locale){...} @CachePut public void updateDomain(String domainId, Locale locale, @CacheValue Domain domain){...} @CacheResult(cacheName=”allDomains”) public List getDomains(Locale locale){...} @RemoveAll public void deleteAllDomains(){...} }
Management
Das Java Caching API definiert im Packet javax.cache.management
auch einige JMX-Management-Funktionen für die Cache-Verwaltung und -Statistiken:
CacheMXBean
erlaubt Zugriff auf folgende Eigenschaften eines Caches:
- den Schlüssel- und Wertetyp,
- die Flags für Read-Through und Write-Through,
- ist Store-by-value aktiv,
- ist das Sammeln von Statistiken aktiviert,
- ist Management aktiviert.
CacheStatisticsMXBean
bietet folgende Attribute und Operationen:
- Anzahl cache hits und Cache hit ratio (in Prozent),
- Anzahl der cache misses und Cache miss ratio (in Prozent),
- Anzahl der cache reads und puts
- Anzahl der cache removals und evictions
- Durchschnittliche Zeit für Zugriff, das Schreiben und das Entfernen von Einträgen
- Zurücksetzen der Statistik
Die Management-Funktionen können mit Hilfe des CacheManager
für jeden Cache
mit den Methoden enableManagement
/enableStatistics
gestartet oder gestoppt werden.
Kritik
Das JSR 107-Team hat sicher alles daran gelegt, ein umfassendes und einfach zu bedienendes API zu definieren, das auch mächtig genug ist, um alle wichtigen Anwendungsfälle abdecken zu können. Dennoch gibt es meiner Ansicht nach einige Punkte, die ich gerne erwähnen möchte.
- Der JSR definiert einige Interfaces, welche es erlauben, ausführbare Funktionalität zu übergeben, also klassische, sogenannte funktionale Interfaces. Leider sind nicht alle Interfaces als functional Interfaces im Sinne von Java 8 umgesetzt, was ich sehr schade finde. Dies trifft zum Beispiel für
CompletionListener
,CacheLoader
undCacheWriter
zu. - Die
ExpiryPolicy
scheint mir ebenfalls für viele reale Anwendungsfälle zu unflexibel modelliert. Statt die Expiry nur an den Ablauf von festen Laufzeiten zu knüpfen, wäre es weitaus flexibler, das Management der Expiry an dieExpiryPolicy
zu delegieren. Das könnte in etwa so aussehen:public interface ExpiryPolicy<K,V> { void entryCreated(Cache.Entry<K,V> entry); void entryAccessed(Cache.Entry<K,V> entry); void entryUpdated(Cache.Entry<K,V> entry); boolean isExpired(Cache.Entry<K,V> entry); }
- Schließlich könnten meiner Meinung nach auch andere Teile des APIs noch schlanker und minimalistischer ausfallen. Hier sei der
CacheManager
erwähnt, der unter anderem die folgenden Methoden deklariert:<K,V> Cache<K,V> getCache(String Cachename,
Im Grunde genommen ist die zweite Methode überflüssig. Ich kann ja der ersten Methode auch
Class<K> keyType, Class<V> valuetype);
<K,V> Cache<K,V> getCache(String cachename);
Object.class
als Schlüssel-/Wertetyp übergeben und würde somit ebenfalls einen (nicht) typ-sicheren Cache erhalten. - Auch aufgefallen ist mir die Art, wie Caches mittels
Configuration<K, V>
beim Erzeugen konfiguriert werden. Es wird ein minimalistisches InterfaceConfiguration<K,V>
definiert, welches nur den Schlüssel-, Wertetyp und die storeByValue Eigenschaft zurückgibt. Alle weiteren Konfigurationseigenschaften sind in einem erweiterten InterfaceCompleteConfiguration<K,V>
definiert. Ich würde dann an dieser Stelle einen ConfigurationBuilder erwarten, welcher sodann Thread-sichere Konfigurationsobjekte (Werttypen) erzeugt. Doch die Expertengruppe hat sich dazu entschieden, eine veränderliche KlasseMutableConfiguration<K,V>
zu definieren, welche sich zwar ähnlich wie ein Builder verhält, aber eben (leider) keiner ist. - Ebenso interessant ist die Methode
boolean Configuration.isStoreByValue()
, welche eigentlich dieselbe Eigenschaft „umgekehrt redundant“ wieOptionalFeature.STORE_BY_REFERENCE
modelliert, die in anderen Teilen des API, zum Beispiel imCacheProvider
, verwendet wird. - Das Handling von
ClassLoaders
ist wichtig, unabhängig davon, ob die Anwendung in einem eigenständigen SE-Kontext (zum Beispiel OSGi) oder in einem Java-EE-Applikationsserver läuft. Dabei den zu verwendendenClassLoader
bei der Erstellung von Caches zu übergeben, ergibt für mich einen Sinn. Aber warum muss ich einen Default-Klassenlader setzen können, der sodann (womöglich) global imCaching
Singleton gemeinsam genutzt wird. Es braucht wenig Phantasie, um sich Szenarien auszudenken, wo das kräftig ins Auge gehen kann. Kommt noch hinzu, dass das Caching Singleton kein SPI anbietet, um diese Funktionalität sinnvoll anzupassen. Vielleicht ein Aspekt, den man im nächsten Maintenance-Release angehen könnte. - Schließlich scheint mir die doppelte Indirektion von
Caching
Singleton in Kombination mitCacheManager
undCacheProvider
ebenfalls etwas unglücklich. Auf demCaching
Singleton existiert gerade mal eine einzige Methode, um auf einenCache
zuzugreifen. Für alle komplexen Anwendungsfälle bin ich gezwungen, die erheblich komplexere SPI-KlasseCacheProvider
zu nutzen. Habe ich mehrere Provider registriert, so bin ich sogar gezwungen, einen voll qualifizierten Implementationsklassennamen zu übergeben. Erst dann erhalte ich Zugriff auf denCacheManager
, mit dem ich programmatisch neueCache
-Instanzen erzeugen kann. Oder aber ich konfiguriere alles, dann aber eben wieder Vendor-abhängig.
Zusammenfassung
Nachdem ich mich durch die Spezifikation des JSR 107 gearbeitet habe, bleibt ein gemischtes Gefühl zurück. Sicher, das Java Caching API ist in weiten Teilen verständlich und meist einfach zu bedienen. Aber es bleibt auch der Nachgeschmack, dass trotz der langen Entwicklungszeit einige Teile noch knackiger, mit weniger Redundanz, einem flexibleren SPI und mit besserem Support für funktionale Aspekte daherkommen könnten. Dennoch ist das API ein großer Vorteil für das Java-Ökosystem, sowohl für Java EE als auch für Java SE basierte Laufzeitumgebungen.
Das API wird aktuell von allen namhaften Herstellern implementiert [1] und erlaubt es in vielen Fällen ohne herstellerspezifische Abhängigkeiten, Caching in eigenen Lösungen zu nutzen. Für erweiterte Funktionen, speziell im verteilten oder geclusterten Umfeld, ist eine Bindung an herstellerspezifische Artefakte jedoch wohl kaum zu umgehen.
Hinterlasse einen Kommentar