Sometimes we need to cache only singular object, without associating it with any key. In this case the use of Guava's LoadingCache is not well adapted. But fortunately, Guava provides another objects to deal with this situation - Suppliers.
A virtual conference at the intersection of Data and AI. This is not a conference for the hype. Its real users talking about real experiences.
- 40+ speakers with the likes of Hannes from Duck DB, Sol Rashidi, Joe Reis, Sadie St. Lawrence, Ryan Wolf from nvidia, Rebecca from lidl
- 12th September 2024
- Three simultaneous tracks
- Panels, Lighting Talks, Keynotes, Booth crawls, Roundtables and Entertainment.
- Topics include (ingestion, finops for data, data for inference (feature platforms), data for ML observability
- 100% virtual and 100% free
👉 Register here
The goal is to show another method to generate cache loaders. In the first part of this post we focus on Suppliers API. The second part, still through JUnit tests, shows sample use cases.
Guava Suppliers API
Current Guava's release (19.0 at the time of writing) proposes Suppliers inside com.google.common.base package. Unlike CacheLoader, it's not placed inside cache package because the role of suppliers is more general. In fact, this final class defines several methods that we can use when we need to get an object from a function. However, some additional features make of it a great and simple cache loader.
Basically, to get an object, we need to call a get() method, defined in Supplier interface, implemented inside Suppliers class as objects with different characteristics. Thanks to these objects, we have a possibility to work with:
- composed suppliers (compose(Function, Supplier))- generates a new supplier by combining specified function and already defined supplier.
- suppliers supporting caching (memoize(Supplier), memoizeWithExpiration(Supplier, long, TimeUnit)) - allows to cache the instance of the object supplied by defined Supplier. The expiration of this cache can be specified in the memoizeWithExpiration method.
- identity suppliers (ofInstance(T)) - similar to Java's Function.identity(), it created a Supplier always returning the object passed as the argument of this factory method.
- synchronized suppliers (synchronizedSupplier(Supplier) - as the name indicates, generates thread-safe supplier. The synchronization is made through synchronized block applied on the instance of Supplier passed to the factory method.
- functions suppliers (supplierFunction() - unlike all previously seen methods, this one returns an instance of SupplierFunction which itself extends Guava's Function interface. This object is a kind of wrapper for traditional Supplier since it accepts an instance of Supplier in the apply(T) method and, under-the-hood, invokes Supplier get() to retrieve returned object.
Guava Suppliers example
After discovering different types of Supplier, we can pass to code part:
public class SuppliersTest { @Test public void should_correctly_create_and_use_composed_supplier() { int inputSupplierSalary = 300; Supplier<Integer> composedSupplier = Suppliers.compose(salary -> salary * 2, () -> inputSupplierSalary); assertThat(composedSupplier.get()).isEqualTo(600); } @Test public void should_correctly_cache_value_from_supplier() { Integer cachedValue = new Integer(89); // Simple use case is shown here but cache can avoid to make redundant I/O operations // for the values which don't change but must be stored // somewhere outside JAR (database, exposed web service etc.) Supplier<Integer> cachingSupplier = Suppliers.memoize(() -> cachedValue); assertThat(cachingSupplier.get() == cachedValue).isTrue(); } @Test public void should_correctly_cache_value_from_supplier_with_expiration_time() throws InterruptedException { List<Integer> invocations = new ArrayList<>(); Supplier<Integer> delegateSupplier = () -> { invocations.add(1); return 50; }; // Similarly to the test with memoize(), we've simple use case but // expiration cache can help to reduce I/O requests loading some // objects that not change every second (ex: distinguished news in newspaper's homepage) Supplier<Integer> expiringCacheSupplier = Suppliers.memoizeWithExpiration(delegateSupplier, 1, TimeUnit.SECONDS); Integer cachedValue = expiringCacheSupplier.get(); assertThat(cachedValue).isEqualTo(50); assertThat(invocations).hasSize(1); // wait 1 second, should re-call get() method // instead of returning cached value Thread.sleep(1000L); cachedValue = expiringCacheSupplier.get(); assertThat(cachedValue).isEqualTo(50); assertThat(invocations).hasSize(2); // invoke get() immediately after, cached instance should be returned cachedValue = expiringCacheSupplier.get(); assertThat(cachedValue).isEqualTo(50); assertThat(invocations).hasSize(2); } @Test public void should_always_supply_the_same_object_instance() { Integer age = new Integer(19); Integer age2 = new Integer(19); Supplier<Integer> ageSupplier = Suppliers.ofInstance(age); assertThat(ageSupplier.get() == age).isTrue(); assertThat(ageSupplier.get() == age2).isFalse(); } @Test public void should_correctly_wrap_supplier_to_a_function() { Function<Supplier<Integer>, Integer> supplierFunction = Suppliers.supplierFunction(); List<Supplier<Integer>> suppliers = Lists.newArrayList(() -> 10, () -> 20, () -> 30); List<Integer> transformedFromSuppliers = FluentIterable.from(suppliers) .transform(supplierFunction).toList(); assertThat(transformedFromSuppliers).hasSize(3); assertThat(transformedFromSuppliers).containsOnly(10, 20, 30); } }
This post shows that CacheLoader is not the single solution generating cacheable objects in Guava. A very interesting alternative are Supplier implementations, defined inside Suppliers final class. As we can see, simple caching not needing a key-value pairs, can be easily achieved by creating appropriate Supplier and passing it into one of 2 memoize* methods. In additional, Suppliers provides other interesting Supplier which can either be used in iterables as Function or composed with a Function.