JSR-107 JCache compatibility adapter for Caffeine caching library
—
The Integration Features system provides support for cache loading, writing, copying, and entry processing to integrate with external data sources and custom business logic. These features enable seamless integration between the cache and external systems like databases, web services, and custom data processing pipelines.
Integration with external data sources through cache loaders for read-through functionality.
/**
* Adapts JCache CacheLoader to Caffeine CacheLoader for integration
*/
public final class JCacheLoaderAdapter<K, V> implements CacheLoader<K, Expirable<V>> {
/**
* Load a single entry from external data source
* @param key the key to load
* @return wrapped value with expiration information
*/
public @Nullable Expirable<V> load(K key);
/**
* Load multiple entries from external data source
* @param keys the keys to load
* @return map of loaded key-value pairs
*/
public Map<K, Expirable<V>> loadAll(Set<? extends K> keys);
}Usage Examples:
// Implement custom cache loader
public class DatabaseUserLoader implements CacheLoader<String, User> {
private final UserRepository userRepository;
public DatabaseUserLoader(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public User load(String userId) throws CacheLoaderException {
try {
User user = userRepository.findById(userId);
if (user == null) {
throw new CacheLoaderException("User not found: " + userId);
}
return user;
} catch (Exception e) {
throw new CacheLoaderException("Failed to load user: " + userId, e);
}
}
@Override
public Map<String, User> loadAll(Iterable<? extends String> userIds) throws CacheLoaderException {
try {
Map<String, User> users = new HashMap<>();
for (String userId : userIds) {
User user = userRepository.findById(userId);
if (user != null) {
users.put(userId, user);
}
}
return users;
} catch (Exception e) {
throw new CacheLoaderException("Failed to load users", e);
}
}
}
// Configure cache with loader
CaffeineConfiguration<String, User> config = new CaffeineConfiguration<String, User>()
.setTypes(String.class, User.class)
.setCacheLoaderFactory(() -> new DatabaseUserLoader(userRepository))
.setReadThrough(true);
Cache<String, User> userCache = cacheManager.createCache("users", config);
// Automatic loading on cache miss
User user = userCache.get("user123"); // Loads from database if not cachedIntegration with external data sources through cache writers for write-through functionality.
/**
* Disabled cache writer implementation for caches without write-through
*/
public enum DisabledCacheWriter implements CacheWriter<Object, Object> {
INSTANCE;
/**
* Get singleton instance of disabled cache writer
* @return the singleton instance
*/
public static CacheWriter<Object, Object> get();
/**
* No-op write operation
* @param entry the entry to write (ignored)
*/
public void write(Cache.Entry<? extends Object, ? extends Object> entry);
/**
* No-op write all operation
* @param entries the entries to write (ignored)
*/
public void writeAll(Collection<Cache.Entry<? extends Object, ? extends Object>> entries);
/**
* No-op delete operation
* @param key the key to delete (ignored)
*/
public void delete(Object key);
/**
* No-op delete all operation
* @param keys the keys to delete (ignored)
*/
public void deleteAll(Collection<?> keys);
}Usage Examples:
// Implement custom cache writer
public class DatabaseUserWriter implements CacheWriter<String, User> {
private final UserRepository userRepository;
public DatabaseUserWriter(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public void write(Cache.Entry<? extends String, ? extends User> entry) throws CacheWriterException {
try {
userRepository.save(entry.getValue());
} catch (Exception e) {
throw new CacheWriterException("Failed to write user: " + entry.getKey(), e);
}
}
@Override
public void writeAll(Collection<Cache.Entry<? extends String, ? extends User>> entries)
throws CacheWriterException {
try {
List<User> users = entries.stream()
.map(Cache.Entry::getValue)
.collect(Collectors.toList());
userRepository.saveAll(users);
} catch (Exception e) {
throw new CacheWriterException("Failed to write users", e);
}
}
@Override
public void delete(Object key) throws CacheWriterException {
try {
userRepository.deleteById((String) key);
} catch (Exception e) {
throw new CacheWriterException("Failed to delete user: " + key, e);
}
}
@Override
public void deleteAll(Collection<?> keys) throws CacheWriterException {
try {
List<String> userIds = keys.stream()
.map(String.class::cast)
.collect(Collectors.toList());
userRepository.deleteAllById(userIds);
} catch (Exception e) {
throw new CacheWriterException("Failed to delete users", e);
}
}
}
// Configure cache with writer
CaffeineConfiguration<String, User> config = new CaffeineConfiguration<String, User>()
.setTypes(String.class, User.class)
.setCacheWriterFactory(() -> new DatabaseUserWriter(userRepository))
.setWriteThrough(true);
Cache<String, User> userCache = cacheManager.createCache("users", config);
// Automatic writing to database
userCache.put("user123", new User("John Doe")); // Also saves to database
userCache.remove("user123"); // Also deletes from databaseSupport for store-by-value semantics through configurable copying strategies.
/**
* Interface for copying objects for store-by-value semantics
*/
public interface Copier {
/**
* Copy an object using the specified ClassLoader
* @param object the object to copy
* @param classLoader the ClassLoader for deserialization
* @return copied object
*/
public <T> T copy(T object, ClassLoader classLoader);
/**
* Get identity copier that returns objects unchanged
* @return identity copier instance
*/
public static Copier identity();
}Default implementation using Java serialization for copying objects.
/**
* Copier implementation using Java serialization
*/
public class JavaSerializationCopier extends AbstractCopier<byte[]> {
/**
* Create new Java serialization copier
*/
public JavaSerializationCopier();
/**
* Serialize object to byte array
* @param object the object to serialize
* @param classLoader the ClassLoader for serialization
* @return serialized byte array
*/
protected byte[] serialize(Object object, ClassLoader classLoader);
/**
* Deserialize byte array to object
* @param data the serialized data
* @param classLoader the ClassLoader for deserialization
* @return deserialized object
*/
protected Object deserialize(byte[] data, ClassLoader classLoader);
}Usage Examples:
// Custom copier implementation using JSON
public class JsonCopier implements Copier {
private final ObjectMapper objectMapper;
public JsonCopier() {
this.objectMapper = new ObjectMapper();
}
@Override
public <T> T copy(T object, ClassLoader classLoader) {
if (object == null) {
return null;
}
try {
// Serialize to JSON and deserialize back
String json = objectMapper.writeValueAsString(object);
@SuppressWarnings("unchecked")
Class<T> clazz = (Class<T>) object.getClass();
return objectMapper.readValue(json, clazz);
} catch (Exception e) {
throw new RuntimeException("Failed to copy object", e);
}
}
}
// Configure cache with custom copier
CaffeineConfiguration<String, User> config = new CaffeineConfiguration<String, User>()
.setTypes(String.class, User.class)
.setStoreByValue(true)
.setCopierFactory(() -> new JsonCopier());
Cache<String, User> userCache = cacheManager.createCache("users", config);
// Objects are copied on store and retrieve
User original = new User("John Doe");
userCache.put("user1", original);
User retrieved = userCache.get("user1"); // Different instance, same data
assert original != retrieved; // Different object references
assert original.equals(retrieved); // Same dataSupport for atomic entry processing operations with mutable entry interface.
/**
* Mutable entry implementation for entry processors
*/
public final class EntryProcessorEntry<K, V> implements MutableEntry<K, V> {
/**
* Get the entry key
* @return the key of this entry
*/
public K getKey();
/**
* Get the entry value
* @return the value of this entry, or null if not present
*/
public V getValue();
/**
* Check if the entry exists in the cache
* @return true if the entry exists
*/
public boolean exists();
/**
* Remove this entry from the cache
*/
public void remove();
/**
* Set the value of this entry
* @param value the new value to set
*/
public void setValue(V value);
/**
* Unwrap this entry to a specific type
* @param clazz the class to unwrap to
* @return unwrapped instance
*/
public <T> T unwrap(Class<T> clazz);
}
/**
* Entry processor action enumeration
*/
public enum Action {
NONE, // No action performed
READ, // Entry was read
CREATED, // Entry was created
LOADED, // Entry was loaded from external source
UPDATED, // Entry was updated
DELETED // Entry was deleted
}Usage Examples:
// Counter increment entry processor
EntryProcessor<String, AtomicInteger, Integer> incrementProcessor =
(entry, arguments) -> {
AtomicInteger counter = entry.getValue();
if (counter == null) {
counter = new AtomicInteger(0);
}
int newValue = counter.incrementAndGet();
entry.setValue(counter);
return newValue;
};
// Conditional update entry processor
EntryProcessor<String, User, Boolean> updateEmailProcessor =
(entry, arguments) -> {
User user = entry.getValue();
if (user == null) {
return false;
}
String newEmail = (String) arguments[0];
if (!isValidEmail(newEmail)) {
return false;
}
user.setEmail(newEmail);
entry.setValue(user);
return true;
};
// Complex business logic processor
EntryProcessor<String, Account, TransactionResult> transferProcessor =
(entry, arguments) -> {
Account account = entry.getValue();
BigDecimal amount = (BigDecimal) arguments[0];
String transactionId = (String) arguments[1];
if (account == null) {
return new TransactionResult(false, "Account not found");
}
if (account.getBalance().compareTo(amount) < 0) {
return new TransactionResult(false, "Insufficient funds");
}
// Perform transfer
account.setBalance(account.getBalance().subtract(amount));
account.addTransaction(new Transaction(transactionId, amount));
entry.setValue(account);
return new TransactionResult(true, "Transfer completed");
};
// Use entry processors
Integer newCount = cache.invoke("counter1", incrementProcessor);
Boolean emailUpdated = cache.invoke("user123", updateEmailProcessor, "new@example.com");
TransactionResult result = cache.invoke("account456", transferProcessor,
new BigDecimal("100.00"), "txn-789");Common integration patterns for external systems.
// Database integration pattern
public class DatabaseIntegratedCache<K, V> {
private final Cache<K, V> cache;
private final Repository<K, V> repository;
public DatabaseIntegratedCache(Cache<K, V> cache, Repository<K, V> repository) {
this.cache = cache;
this.repository = repository;
}
// Write-behind pattern
public void putAsync(K key, V value) {
cache.put(key, value);
CompletableFuture.runAsync(() -> {
try {
repository.save(key, value);
} catch (Exception e) {
// Handle async write failure
handleWriteFailure(key, value, e);
}
});
}
// Cache-aside pattern
public V getWithFallback(K key) {
V value = cache.get(key);
if (value == null) {
value = repository.findById(key);
if (value != null) {
cache.put(key, value);
}
}
return value;
}
private void handleWriteFailure(K key, V value, Exception e) {
// Implement failure handling strategy
}
}
// Web service integration pattern
public class WebServiceCacheLoader implements CacheLoader<String, ApiResponse> {
private final WebClient webClient;
private final CircuitBreaker circuitBreaker;
@Override
public ApiResponse load(String endpoint) throws CacheLoaderException {
return circuitBreaker.executeSupplier(() -> {
try {
return webClient.get()
.uri(endpoint)
.retrieve()
.bodyToMono(ApiResponse.class)
.block(Duration.ofSeconds(10));
} catch (Exception e) {
throw new CacheLoaderException("Failed to load from web service", e);
}
});
}
}
// Event-driven invalidation pattern
public class EventDrivenCacheInvalidation {
private final Cache<String, Object> cache;
private final EventBus eventBus;
public EventDrivenCacheInvalidation(Cache<String, Object> cache, EventBus eventBus) {
this.cache = cache;
this.eventBus = eventBus;
// Register for invalidation events
eventBus.register(this);
}
@Subscribe
public void handleInvalidationEvent(CacheInvalidationEvent event) {
if (event.isInvalidateAll()) {
cache.removeAll();
} else {
cache.removeAll(event.getKeysToInvalidate());
}
}
}Efficient bulk operations for external system integration.
public class BulkOperationHelper {
// Bulk load with batching
public static <K, V> void bulkLoadWithBatching(
Cache<K, V> cache,
Set<K> keys,
CacheLoader<K, V> loader,
int batchSize) {
List<K> keyList = new ArrayList<>(keys);
for (int i = 0; i < keyList.size(); i += batchSize) {
int endIndex = Math.min(i + batchSize, keyList.size());
List<K> batch = keyList.subList(i, endIndex);
try {
Map<K, V> loaded = loader.loadAll(batch);
cache.putAll(loaded);
} catch (Exception e) {
// Handle batch failure - could retry individual items
handleBatchFailure(cache, batch, loader, e);
}
}
}
// Bulk write with error handling
public static <K, V> void bulkWriteWithErrorHandling(
Cache<K, V> cache,
Map<K, V> entries,
CacheWriter<K, V> writer) {
try {
List<Cache.Entry<K, V>> entryList = entries.entrySet().stream()
.map(e -> new SimpleEntry<>(e.getKey(), e.getValue()))
.collect(Collectors.toList());
writer.writeAll(entryList);
cache.putAll(entries);
} catch (CacheWriterException e) {
// Fall back to individual writes
for (Map.Entry<K, V> entry : entries.entrySet()) {
try {
writer.write(new SimpleEntry<>(entry.getKey(), entry.getValue()));
cache.put(entry.getKey(), entry.getValue());
} catch (Exception individualError) {
// Log individual failure but continue
handleIndividualWriteFailure(entry.getKey(), entry.getValue(), individualError);
}
}
}
}
private static <K, V> void handleBatchFailure(Cache<K, V> cache, List<K> batch,
CacheLoader<K, V> loader, Exception e) {
// Implementation depends on failure handling strategy
}
private static <K, V> void handleIndividualWriteFailure(K key, V value, Exception e) {
// Implementation depends on failure handling strategy
}
}Install with Tessl CLI
npx tessl i tessl/maven-com-github-ben-manes-caffeine--jcache