I just announced the new Spring Boot 2 material, coming in REST With Spring:

>> CHECK OUT THE COURSE

1. Overview

In this tutorial, we’ll take a look at the Guava Cache implementation – basic usage, eviction policies, refreshing the cache and some interesting bulk operations.

Finally, we will take a look at the using the removal notifications the cache is able to send out.

2. How to Use Guava Cache

Let’s start with a simple example – let’s cache the uppercase form of String instances.

First, we’ll create the CacheLoader – used to compute the value stored in the cache. From this, we’ll use the handy CacheBuilder to build our cache using the given specifications:

@Test
public void whenCacheMiss_thenValueIsComputed() {
    CacheLoader<String, String> loader;
    loader = new CacheLoader<String, String>() {
        @Override
        public String load(String key) {
            return key.toUpperCase();
        }
    };

    LoadingCache<String, String> cache;
    cache = CacheBuilder.newBuilder().build(loader);

    assertEquals(0, cache.size());
    assertEquals("HELLO", cache.getUnchecked("hello"));
    assertEquals(1, cache.size());
}

Notice how there is no value in the cache for our “hello” key – and so the value is computed and cached.

Also note that we’re using the getUnchecked() operation – this computes and loads the value into the cache if it doesn’t already exist.

3. Eviction Policies

Every cache needs to remove values at some point. Let’s discuss the mechanism of evicting values out of the cache – using different criteria.

3.1. Eviction by Size

We can limit the size of our cache using maximumSize(). If the cache reaches the limit, the oldest items will be evicted.

In the following code, we limit the cache size to 3 records:

@Test
public void whenCacheReachMaxSize_thenEviction() {
    CacheLoader<String, String> loader;
    loader = new CacheLoader<String, String>() {
        @Override
        public String load(String key) {
            return key.toUpperCase();
        }
    };
    LoadingCache<String, String> cache;
    cache = CacheBuilder.newBuilder().maximumSize(3).build(loader);

    cache.getUnchecked("first");
    cache.getUnchecked("second");
    cache.getUnchecked("third");
    cache.getUnchecked("forth");
    assertEquals(3, cache.size());
    assertNull(cache.getIfPresent("first"));
    assertEquals("FORTH", cache.getIfPresent("forth"));
}

3.2. Eviction by Weight

We can also limit the cache size using a custom weight function. In the following code, we use the length as our custom weight function:

@Test
public void whenCacheReachMaxWeight_thenEviction() {
    CacheLoader<String, String> loader;
    loader = new CacheLoader<String, String>() {
        @Override
        public String load(String key) {
            return key.toUpperCase();
        }
    };

    Weigher<String, String> weighByLength;
    weighByLength = new Weigher<String, String>() {
        @Override
        public int weigh(String key, String value) {
            return value.length();
        }
    };

    LoadingCache<String, String> cache;
    cache = CacheBuilder.newBuilder()
      .maximumWeight(16)
      .weigher(weighByLength)
      .build(loader);

    cache.getUnchecked("first");
    cache.getUnchecked("second");
    cache.getUnchecked("third");
    cache.getUnchecked("last");
    assertEquals(3, cache.size());
    assertNull(cache.getIfPresent("first"));
    assertEquals("LAST", cache.getIfPresent("last"));
}

Note: The cache may remove more than one record to leave room for a new large one.

3.3. Eviction by Time

Beside using size to evict old records, we can use time. In the following example, we customize our cache to remove records that have been idle for 2ms:

@Test
public void whenEntryIdle_thenEviction()
  throws InterruptedException {
    CacheLoader<String, String> loader;
    loader = new CacheLoader<String, String>() {
        @Override
        public String load(String key) {
            return key.toUpperCase();
        }
    };

    LoadingCache<String, String> cache;
    cache = CacheBuilder.newBuilder()
      .expireAfterAccess(2,TimeUnit.MILLISECONDS)
      .build(loader);

    cache.getUnchecked("hello");
    assertEquals(1, cache.size());

    cache.getUnchecked("hello");
    Thread.sleep(300);

    cache.getUnchecked("test");
    assertEquals(1, cache.size());
    assertNull(cache.getIfPresent("hello"));
}

We can also evict records based on their total live time. In the following example, the cache will remove the records after 2ms of being stored:

@Test
public void whenEntryLiveTimeExpire_thenEviction()
  throws InterruptedException {
    CacheLoader<String, String> loader;
    loader = new CacheLoader<String, String>() {
        @Override
        public String load(String key) {
            return key.toUpperCase();
        }
    };

    LoadingCache<String, String> cache;
    cache = CacheBuilder.newBuilder()
      .expireAfterWrite(2,TimeUnit.MILLISECONDS)
      .build(loader);

    cache.getUnchecked("hello");
    assertEquals(1, cache.size());
    Thread.sleep(300);
    cache.getUnchecked("test");
    assertEquals(1, cache.size());
    assertNull(cache.getIfPresent("hello"));
}

4. Weak Keys

Next, let’s see how to make our cache keys have weak references – allowing the garbage collector to collect cache key that are not referenced elsewhere.

By default, both cache keys and values have strong references but we can make our cache store the keys using weak references using weakKeys() as in the following example:

@Test
public void whenWeakKeyHasNoRef_thenRemoveFromCache() {
    CacheLoader<String, String> loader;
    loader = new CacheLoader<String, String>() {
        @Override
        public String load(String key) {
            return key.toUpperCase();
        }
    };

    LoadingCache<String, String> cache;
    cache = CacheBuilder.newBuilder().weakKeys().build(loader);
}

5. Soft Values

We can allow garbage collector to collect our cached values by using softValues() as in the following example:

@Test
public void whenSoftValue_thenRemoveFromCache() {
    CacheLoader<String, String> loader;
    loader = new CacheLoader<String, String>() {
        @Override
        public String load(String key) {
            return key.toUpperCase();
        }
    };

    LoadingCache<String, String> cache;
    cache = CacheBuilder.newBuilder().softValues().build(loader);
}

Note: Many soft references may affect the system performance – it’s preferred to use maximumSize().

6. Handle null Values

Now, let’s see how to handle cache null values. By default, Guava Cache will throw exceptions if you try to load a null value – as it doesn’t make any sense to cache a null.

But if null value means something in your code, then you can make good use of the Optional class as in the following example:

@Test
public void whenNullValue_thenOptional() {
    CacheLoader<String, Optional<String>> loader;
    loader = new CacheLoader<String, Optional<String>>() {
        @Override
        public Optional<String> load(String key) {
            return Optional.fromNullable(getSuffix(key));
        }
    };

    LoadingCache<String, Optional<String>> cache;
    cache = CacheBuilder.newBuilder().build(loader);

    assertEquals("txt", cache.getUnchecked("text.txt").get());
    assertFalse(cache.getUnchecked("hello").isPresent());
}
private String getSuffix(final String str) {
    int lastIndex = str.lastIndexOf('.');
    if (lastIndex == -1) {
        return null;
    }
    return str.substring(lastIndex + 1);
}

7. Refresh the Cache

Next, let’s see how to refresh our cache values. We can refresh our cache automatically using refreshAfterWrite().

In the following example, the cache is refreshed automatically every 1 minute:

@Test
public void whenLiveTimeEnd_thenRefresh() {
    CacheLoader<String, String> loader;
    loader = new CacheLoader<String, String>() {
        @Override
        public String load(String key) {
            return key.toUpperCase();
        }
    };

    LoadingCache<String, String> cache;
    cache = CacheBuilder.newBuilder()
      .refreshAfterWrite(1,TimeUnit.MINUTES)
      .build(loader);
}

Note: You can refresh specific record manually using refresh(key).

8. Preload the Cache

We can insert multiple records in our cache using putAll() method. In the following example, we add multiple records into our cache using a Map:

@Test
public void whenPreloadCache_thenUsePutAll() {
    CacheLoader<String, String> loader;
    loader = new CacheLoader<String, String>() {
        @Override
        public String load(String key) {
            return key.toUpperCase();
        }
    };

    LoadingCache<String, String> cache;
    cache = CacheBuilder.newBuilder().build(loader);

    Map<String, String> map = new HashMap<String, String>();
    map.put("first", "FIRST");
    map.put("second", "SECOND");
    cache.putAll(map);

    assertEquals(2, cache.size());
}

9. RemovalNotification

Sometimes, you need to take some actions when a record is removed from the cache; so, let’s discuss RemovalNotification.

We can register a RemovalListener to get notifications of a record being removed. We also have access to the cause of the removal – via the getCause() method.

In the following sample, a RemovalNotification is received when the forth element in the cache because of its size:

@Test
public void whenEntryRemovedFromCache_thenNotify() {
    CacheLoader<String, String> loader;
    loader = new CacheLoader<String, String>() {
        @Override
        public String load(final String key) {
            return key.toUpperCase();
        }
    };

    RemovalListener<String, String> listener;
    listener = new RemovalListener<String, String>() {
        @Override
        public void onRemoval(RemovalNotification<String, String> n){
            if (n.wasEvicted()) {
                String cause = n.getCause().name();
                assertEquals(RemovalCause.SIZE.toString(),cause);
            }
        }
    };

    LoadingCache<String, String> cache;
    cache = CacheBuilder.newBuilder()
      .maximumSize(3)
      .removalListener(listener)
      .build(loader);

    cache.getUnchecked("first");
    cache.getUnchecked("second");
    cache.getUnchecked("third");
    cache.getUnchecked("last");
    assertEquals(3, cache.size());
}

10. Notes

Finally, here are a few additional quick notes about the Guava cache implementation:

  • it is thread-safe
  • you can insert values manually into the cache using put(key,value)
  • you can measure your cache performance using CacheStats ( hitRate(), missRate(), ..)

11. Conclusion

We went through a lot of usecases of the Guava Cache in this tutorial – from simple usage to eviction of elements, refresh and preload of the cache and removal notifications.

I just announced the new Spring Boot 2 material, coming in REST With Spring:

>> CHECK OUT THE LESSONS

newest oldest most voted
Notify of
Darrell Burgan
Guest
Darrell Burgan

It’s worth noting that such a cache has no cluster awareness, which means that data inconsistency can build between multiple application servers if they use such a simple cache. So, at the very least, some form of time-based eviction is really necessary to put bounds on this inconsistency.

People looking for a stronger cache system should probably look beyond Guava and look to Infinispan or Ehcache, which provide the same capabilities but add in network cluster awareness, including multicast-based eviction.

Eugen Paraschiv
Guest

Hey Darrell – you’re definitely right – there’s no cluster awareness of affinity. My view is that there’s no need for that – the Guava cache is a simple one-JVM type of solution and dealing with the cluster is way beyond what it’s trying to do. So – as you said – if you’re looking for a cluster based solution, you’re not really looking at this basic Guava implementation, but at a proper and mature caching solution.
Thanks for the feedback. Cheers,
Eugen.

Lalit Jha
Guest

What happens if keys are week but values are not soft? While garbage collector collects keys will it also collect values as side effect?

Eugen Paraschiv
Guest

Using soft references for the values will lead to them being collected if no other references exist to them, yes.

Eugen Paraschiv
Guest

If keys are week, and a key is GCed, the entry (and thus the value) will be GCed as well after a while, as part of the “routine maintenance” described in the class javadoc.

Fernando Aspiazu
Guest
Fernando Aspiazu

Good article 🙂

Bharat Bhagat
Guest
Bharat Bhagat

Hi Eugen, Nice article. In refresh the cache part. How it will refresh itself.

Eugen Paraschiv
Guest

Hey Bharat – I’m glad you like it. I’m not sure I fully follow your question though – can you maybe rephrase that a bit? Cheers,
Eugen.