Search code examples
springspring-bootspring-mvccachingspring-cache

How to access cache values in Spring


Scenario: I need to access the values of a cache created as part of one method in another method. How do I do it?

public class Invoice {
  private String invoiced;
  private BigDecimal amount;
  //Getters and Setters
}

Method 1: Gets invoked when a customer wants to get list of invoices from UI

@Cacheable(value="invoices")
public List<Invoice> getAllInvoices(String customerId){
...
//Get all invoices from Database and return 
...
return invoices
}

Method 2: Gets invoked when a customer clicks on download on UI

public File downloadInvoice(String invoiceId) {
 //TODO: 
 //Validate if invoiceId is present in the cache. This is a validation step
 //How can I access the cache "invoices" here.
 ...
 //if InvoiceId is present in cache then download from db else throw Exception
 return file;
}

Note: I am not using any caching libraries


Solution

  • As the documentation (and here) on Spring's Cache Abstraction explains, you must enable caching (i.e. using the @EnableCaching annotation with annotation config, or using the <cache:annotation-driven> element with XML config) and declare a bean of type CacheManager to plugin the caching provider of your choice (e.g. Redis).

    Of course, when you are using Spring Boot, both of these concerns are "auto-configured" for you, providing you have declared an appropriate caching provider (i.e. a caching provider implementing Spring's Cache Abstraction) on your Spring Boot application's classpath.

    For complete list of caching providers supported (i.e. "auto-configured") by Spring Boot, see here.

    This (here) is Spring Boot's caching auto-configuration for Redis, when Redis is on the application's classpath. Notice the declaration of the CacheManager bean, which must be precisely named "cacheManager". This setup is the same for all caching providers.

    Now that you know you a "cacheManager" bean of type CacheManager exists in the Spring ApplicationContext, you can use the CacheManager to access the individual caches, which are represented by the Cache (Adapter) interface.

    You could then...

    @Service
    class InvoiceService {
    
      private CacheManager cacheManager;
    
      public InvoiceService(CacheManager cacheManager) {
        this.cacheManager = cacheManager;
      }
    
    
      public File downloadInvoice(String invoiceId) {
    
        // Note: name of cache here (i.e. "invoices") must match the name
        // in the `@Cacheable` annotation on getAllInvoices(..).
        Cache invoices = this.cacheManager.getCache("invoices");
    
        // Now use the "invoices" `Cache` to find a reference to a `Invoice` with "invoiceId".
        // Read/load from database if `Invoice` is "cached"...
        // Do what must be done, etc ...
    
      }
    }
    

    However, you have problem.

    Your @Cacheable, getAllInvoices(..) method caches (i.e. maps) "customerId" (key) to a List<Invoice> object (value) in the "invoices" cache.

    You might be thinking the returned List of Invoices from your @Cacheable, getAllInvoices(..) service method are cached individually, by "invoiceId". However, I assure you, that is NOT the case!

    It is actually...

    customerId -> List<Invoice>

    That is, a List of Invoice objects mapped by the "customerId" in the "invoices" cache.

    There are ways to alter the default behavior (e.g. to cache individual Invoice objects from the List by "invoiceId" in the "invoices" cache if desired, which I have explained in other SO posts on caching Collections), but that is not how your application logic is currently setup and will function!

    Therefore, you will need to translate the "invoiceId" into a "customerId" to access the List of Invoice objects from the "invoices" Cache by "customerId", and then iterate the List to find the (possible) cached Invoice by "invoiceId".

    Or, you can change your caching logic around (recommended).

    Finally...

    No caching provider is the same under-the-hood. There may be a way to access individual caches, independently, depending on provider However, in general, you should keep in mind that Spring's Cache representation for the individual caches identified in Spring's caching annotations (e.g. @Cacheable) or the equivalent JSR-107, JCache API annotation equivalents, are not actual "beans" in the Spring Container (unlike the CacheManager).

    In Redis, caches are Redis HASHES (I believe).

    In GemFire/Geode (which I am most familiar with), caches (i.e. Cache) is a GemFire/Geode Region, which does happen to be a Spring bean in the ApplicationContext.

    Also, some caching providers also wrap the underlying data structure (e.g. GemFire/Geode Region) backing the Cache with an appropriate template (e.g. GemfireTemplate, which is by Region).

    I am not certain if (Sprig Data) Redis creates a RedisTemplate for each HASH backing a Cache. However, this is the case with GemFire/Geode. So you could do something like this as well...

    @Service
    class InvoiceService {
    
      @Resource(name = "invoices")
      private Region<String, Invoice> invoices;
    
      // ...
    
    }
    

    Or, alternatively (recommended)...

    @Service
    class InvoiceService {
    
      @Autowired
      @Qualifier("invoices")
      GemfireTemplate invoicesTemplate;
    
      // ... 
    
    }
    

    Again, this varies by caching provider and is very provider specific.

    The Cache Adapter interface is a generic way to reference the backing caching implementation, and useful if you expect to switch caching providers between environments, or for other reasons.

    Again, to access the individual caches (e.g. "invoices"), you inject the CacheManager since not all caching providers create beans for the individual Cache instances.

    Remember, you are going to have to change your caching design a bit, I think.

    Hope this helps.