今天在使用的时候使用GuavaCache的refreshAfterWrite的功能时,发现在少数场景下会报错CacheLoader returned null for key
。但是如果把refreshAfterWrite去掉时,又不会报错。具体错误内容是这样的。
com.google.common.cache.CacheLoader$InvalidCacheLoadException: CacheLoader returned null for key ValueOfKeyIsNull.
at com.google.common.cache.LocalCache$Segment.getAndRecordStats(LocalCache.java:2348)
at com.google.common.cache.LocalCache$Segment.loadSync(LocalCache.java:2318)
at com.google.common.cache.LocalCache$Segment.lockedGetOrLoad(LocalCache.java:2280)
at com.google.common.cache.LocalCache$Segment.get(LocalCache.java:2195)
at com.google.common.cache.LocalCache.get(LocalCache.java:3934)
at com.google.common.cache.LocalCache.getOrLoad(LocalCache.java:3938)
at com.google.common.cache.LocalCache$LocalLoadingCache.get(LocalCache.java:4821)
at com.google.guava.cache.GuavaRefreshWhenCacheIsNullTest.testGuavaRefreshWhenCacheIsNullThrowsException(GuavaRefreshWhenCacheIsNullTest.java:49)
首先为什么如果不用refreshAfterWrite功能时为什么不会有问题?由于好奇,只能去源码里查找答案。基于报错内容,在com.google.common.cache.LocalCache.Segment#getAndRecordStats找到这一段源代码
value = getUninterruptibly(newValue);
if (value == null) {
throw new InvalidCacheLoadException("CacheLoader returned null for key " + key + ".");
}
大致意思是从ListenableFuture newValue这个Future中获取到的值不能为空,如果为空,则直接报一个InvalidCacheLoadException异常。
当我们使用了refreshAfterWrite功能时,必须build一个自己实现的CacheLoader,这时会返回一个com.google.common.cache.LocalCache.LocalLoadingCache的LoadingCache实例。从org.springframework.cache.guava.GuavaCache代码中,发现这么一段代码
@Override
public ValueWrapper get(Object key) {
if (this.cache instanceof LoadingCache) {
try {
Object value = ((LoadingCache<Object, Object>) this.cache).get(key);
return toValueWrapper(value);
}
catch (ExecutionException ex) {
throw new UncheckedExecutionException(ex.getMessage(), ex);
}
}
return super.get(key);
}
当这个cache是LoadingCache时,走的获取key对应的value的方式是不同的。依次会走到com.google.common.cache.LocalCache S e g m e n t . l o a d S y n c , 然 后 到 c o m . g o o g l e . c o m m o n . c a c h e . L o c a l C a c h e Segment.loadSync,然后到com.google.common.cache.LocalCache Segment.loadSync,然后到com.google.common.cache.LocalCacheSegment.getAndRecordStats,最终获取的value如果为null的话,则直接报错,即使你在GuavaCacheManager层面设置了setAllowNullValues(true)也依然会报错。
如果不是LoadingCache的话,那是允许返回null值的,且不会报错。但是使用了refreshAfterWrite功能后,是不允许的。其实仔细想一想也是很合理的,这里我们重写了CacheLoader,CacheLoader的一个重要的工作就是在2次获取同一个key时,且key到了该refresh的时间,就会后台异步刷新,如果刷新这个key得到了新值,就会覆盖key对应的旧值。但是如果得到了null,应该怎么做呢?刷新还是不管?GuavaCache表示自己也很无奈,干脆报错,让业务层自己去理会好了。
不过,个人觉得这种方式还是比较粗暴。就算是使用了refreshAfterWrite,也不敢保证自己的每个key都能对应值。但是从报错位置的代码来看,确实没有可设置的参数给业务来屏蔽这个异常。
有一种最挫最简单的方法,在get的时候catch住异常,异常情况下直接返回null,这种方法简单粗暴又有效
对于null值的处理,java8是提供了一种很好的处理方法,就是Optional类。对value值统一使用Optional封装,业务方拿到Optional时,通过Optional.orElse(null)方法拿到真实值,避免在CacheLoader的load中返回null。关于Optional,更多详细内容可以参考我的另一篇博客Java8新特性学习(二)- Optional类。
下面代码已上传到 github - common-caches
@Test
public void testGuavaRefreshWhenCacheIsNullReturnNull() {
CacheBuilder<Object, Object> cacheBuilder = CacheBuilder.newBuilder()
.refreshAfterWrite(10, TimeUnit.SECONDS)
.expireAfterWrite(20, TimeUnit.SECONDS);
LoadingCache<String, Optional<String>> refreshWarehouseCache = cacheBuilder.build(new CacheLoader<String, Optional<String>>() {
@Override
public Optional<String> load(String key) {
if ("ValueOfKeyIsNull".equals(key)) {
return Optional.empty();
}
return Optional.of("1234567890");
}
@Override
public ListenableFuture<Optional<String>> reload(String key, Optional<String> oldValue) {
System.out.println("testGuavaRefresh reload : key=" + key);
return Futures.immediateFuture(load(key));
}
});
try {
Optional<String> myValue = refreshWarehouseCache.get("myKey");
Assert.assertEquals("1234567890", myValue.orElse(null));
myValue = refreshWarehouseCache.get("ValueOfKeyIsNull");
//get myValue is null
Assert.assertNull(myValue.orElse(null));
} catch (ExecutionException e) {
e.printStackTrace();
}
}
这是找一个特殊的值,且不会在真实环境中不会有和这个特殊值相同。这里以value是String类型为例,当然如果是Object类型的,也是可以判断的,只要XXXObject某些关键字段的值不一样就行,可以使用Objects.equals()来判定是否是特殊值,主要要重写这个XXXObject的equals和hashCode方法就行了。
下面代码已上传到 github - common-caches
@Test
public void testGuavaRefreshWhenCacheIsNullReturnDefaultNullValue() {
CacheBuilder
前面的博客有讲过GuavaCache相关的内容,包括缓存篇(一)- Guava 和 Guava Cache expireAfterWrite 与 refreshAfterWrite区别.
关于GuavaCache,其实有一些设计比较好的方面,但是也存在一些可以完善的方面。在使用的过程中,不断发现设计好的学习过来。你觉得还有哪些设计不好的方面,欢迎一起交流。
我先来一个觉得不好的吧。spring中集成的Guava Cache,一个GuavaCacheManager,只设计了一个CacheLoader,但是cacheName却有多个,这就意味着一个CacheName在后台异步刷新时,需要考虑多个不同的cacheName的情况。而CacheLoader中只能通过Object key来判断当前这个key是属于哪个cacheName的,进而再调用对应的cacheName的刷新方法去刷新,这是比较困难的一件事,如果你的多个cacheName的key是没有什么特别的规则的话,这简直就是一个灾难。