springboot2 - cache接口get函数无效问题
需要有自定义 cache 的经验
问题描述
对比 JSON 和 Cache 两个接口,容易陷入一个误区:
将 JSON.parseObject(String text, Type type),写到了 Cache 的 get(Object key, @Nullable Class
我期望的效果是:根据 key 值,从缓存中取出 json,然后根据 class 的值,将 json 转换成对应的对象。
实际效果却不尽如人意:从缓存中取数据的时候,走的是 get(Object key) 函数,根本不触发 get(Object key, @Nullable Class
JSON工具
public interface JSON {
//
static Object parse(String text);
//
<T> T parseObject(String text, Type type);
}
spring 缓存的接口
JSON 工具的两个函数,咋看之下,完美适配 Cache 接口。
public interface Cache {
/**
* Return the value to which this cache maps the specified key.
* <p>Returns {@code null} if the cache contains no mapping for this key;
* otherwise, the cached value (which may be {@code null} itself) will
* be returned in a {@link ValueWrapper}.
* @param key the key whose associated value is to be returned
* @return the value to which this cache maps the specified key,
* contained within a {@link ValueWrapper} which may also hold
* a cached {@code null} value. A straight {@code null} being
* returned means that the cache contains no mapping for this key.
* @see #get(Object, Class)
* @see #get(Object, Callable)
*/
@Nullable
ValueWrapper get(Object key);
/**
* Return the value to which this cache maps the specified key,
* generically specifying a type that return value will be cast to.
* <p>Note: This variant of {@code get} does not allow for differentiating
* between a cached {@code null} value and no cache entry found at all.
* Use the standard {@link #get(Object)} variant for that purpose instead.
* @param key the key whose associated value is to be returned
* @param type the required type of the returned value (may be
* {@code null} to bypass a type check; in case of a {@code null}
* value found in the cache, the specified type is irrelevant)
* @return the value to which this cache maps the specified key
* (which may be {@code null} itself), or also {@code null} if
* the cache contains no mapping for this key
* @throws IllegalStateException if a cache entry has been found
* but failed to match the specified type
* @since 4.0
* @see #get(Object)
*/
@Nullable
<T> T get(Object key, @Nullable Class<T> type);
}
答案
参考开源代码,就会发现,get(Object key, @Nullable Class
如果缓存无法转换成指定的类型,就抛出异常,class 参数起到校验的功能,而不是作为类型转换的依据。
从 Cache 的注释上,也能看出问题,get(Object key, Class
如果是为了做类型转换,不可能等这个版本才设计这个接口,很可能是为了某些新特性才增加的。
ehcache 源码:
public class EhCacheCache implements Cache {
@Override
@Nullable
public <T> T get(Object key, @Nullable Class<T> type) {
Element element = this.cache.get(key);
Object value = (element != null ? element.getObjectValue() : null);
if (value != null && type != null && !type.isInstance(value)) {
throw new IllegalStateException(
"Cached value is not of required type [" + type.getName() + "]: " + value);
}
return (T) value;
}
}
redis-cache 源码:
public class RedisCache extends AbstractValueAdaptingCache {
@Override
@SuppressWarnings("unchecked")
@Nullable
public <T> T get(Object key, @Nullable Class<T> type) {
Object value = fromStoreValue(lookup(key));
if (value != null && type != null && !type.isInstance(value)) {
throw new IllegalStateException(
"Cached value is not of required type [" + type.getName() + "]: " + value);
}
return (T) value;
}
}
源码分析
会发现这个问题,自然是因为我这么做过,从源码上分析一波,看看为什么不触发 get(Object key, Class
缓存的使用方式
class Test {
@Cacheable(cacheNames = "user", key = "'test' + #id")
public User getUserById(Long id) {
return dao.getUserById(id);
}
}
调用缓存方法栈
通过 get() 函数,手动抛出一个异常,看一下方法栈,代码执行顺序如下:
cn.seaboot.admin.core.ControllerExceptionHandler.exceptionHandler(javax.servlet.http.HttpServletRequest,javax.servlet.http.
HttpServletResponse,cn.seaboot.common.exception.BizException) throws java.io.IOException
cn.seaboot.common.exception.BizException: test
at cn.seaboot.plugin.redis.RedisCache.get(RedisCache.java:43)
at org.springframework.cache.interceptor.AbstractCacheInvoker.doGet(AbstractCacheInvoker.java:73)
at org.springframework.cache.interceptor.CacheAspectSupport.findInCaches(CacheAspectSupport.java:389)
at org.springframework.cache.interceptor.CacheAspectSupport.findCachedItem(CacheAspectSupport.java:350)
at org.springframework.cache.interceptor.CacheAspectSupport.execute(CacheAspectSupport.java:239)
at org.springframework.cache.interceptor.CacheAspectSupport.execute(CacheAspectSupport.java:205)
at org.springframework.cache.interceptor.CacheInterceptor.invoke(CacheInterceptor.java:61)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:688)
at cn.seaboot.admin.service.core.DebugService$$EnhancerBySpringCGLIB$$e478a24.testCache(<generated>)
at cn.seaboot.admin.ctrl.page.DebugCtrl.data(DebugCtrl.java:97)
1、无关紧要的部分:
DebugCtrl -> Service代理 -> CglibAopProxy -> ReflectiveMethodInvocation
DebugCtrl、Service:是自己写的代码,问题不在这里。
CglibAopProxy、ReflectiveMethodInvocation:属于Aop包,归属于更加底层源码,缓存拦截只属于其中一部分。
2、因此,问题必定位于这些代码中,后面我们逐个分析:
- CacheInterceptor 缓存拦截
- CacheAspectSupport 缓存切面
- AbstractCacheInvoker Cache处理对象,负责调用Cache实现类
- RedisCache Cache接口实现类
CacheInterceptor
只是负责调用起CacheAspectSupport,本身代码较少,无分析价值
public class CacheInterceptor extends CacheAspectSupport implements MethodInterceptor, Serializable {
public CacheInterceptor() {
}
@Nullable
public Object invoke(MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
CacheOperationInvoker aopAllianceInvoker = () -> {
try {
return invocation.proceed();
} catch (Throwable var2) {
throw new ThrowableWrapper(var2);
}
};
try {
//只是负责调用起CacheAspectSupport,本身代码较少
return this.execute(aopAllianceInvoker, invocation.getThis(), method, invocation.getArguments());
} catch (ThrowableWrapper var5) {
throw var5.getOriginal();
}
}
}
AbstractCacheInvoker
Cache 处理器,对 Cache 包裹了一层代码,负责调用起 Cache 的相关函数,从这里可以看出一部分问题了,代码自始至终没调用过 get 函数
public abstract class AbstractCacheInvoker {
protected SingletonSupplier<CacheErrorHandler> errorHandler;
protected AbstractCacheInvoker() {
this.errorHandler = SingletonSupplier.of(SimpleCacheErrorHandler::new);
}
protected AbstractCacheInvoker(CacheErrorHandler errorHandler) {
this.errorHandler = SingletonSupplier.of(errorHandler);
}
public void setErrorHandler(CacheErrorHandler errorHandler) {
this.errorHandler = SingletonSupplier.of(errorHandler);
}
public CacheErrorHandler getErrorHandler() {
return (CacheErrorHandler)this.errorHandler.obtain();
}
@Nullable
protected ValueWrapper doGet(Cache cache, Object key) {
try {
return cache.get(key);
} catch (RuntimeException var4) {
this.getErrorHandler().handleCacheGetError(var4, cache, key);
return null;
}
}
protected void doPut(Cache cache, Object key, @Nullable Object result) {
try {
cache.put(key, result);
} catch (RuntimeException var5) {
this.getErrorHandler().handleCachePutError(var5, cache, key, result);
}
}
protected void doEvict(Cache cache, Object key) {
try {
cache.evict(key);
} catch (RuntimeException var4) {
this.getErrorHandler().handleCacheEvictError(var4, cache, key);
}
}
protected void doClear(Cache cache) {
try {
cache.clear();
} catch (RuntimeException var3) {
this.getErrorHandler().handleCacheClearError(var3, cache);
}
}
}
CacheAspectSupport
代码有一千多行,重点看execute,很明显,get 函数确实没被调用过。
class CacheAspectSupport{
@Nullable
private Object execute(CacheOperationInvoker invoker, Method method, CacheAspectSupport.CacheOperationContexts contexts) {
if (contexts.isSynchronized()) {
CacheAspectSupport.CacheOperationContext context = (CacheAspectSupport.CacheOperationContext)contexts.get(CacheableOperation.class).iterator().next();
if (this.isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {
Object key = this.generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
Cache cache = (Cache)context.getCaches().iterator().next();
try {
//同步调用,调用Cache有Callback的get函数
return this.wrapCacheValue(method, cache.get(key, () -> {
return this.unwrapReturnValue(this.invokeOperation(invoker));
}));
} catch (ValueRetrievalException var10) {
throw (ThrowableWrapper)var10.getCause();
}
} else {
return this.invokeOperation(invoker);
}
} else {
this.processCacheEvicts(contexts.get(CacheEvictOperation.class), true, CacheOperationExpressionEvaluator.NO_RESULT);
//异步调用,调用get函数,返回ValueWrapper
ValueWrapper cacheHit = this.findCachedItem(contexts.get(CacheableOperation.class));
List<CacheAspectSupport.CachePutRequest> cachePutRequests = new LinkedList();
if (cacheHit == null) {
this.collectPutRequests(contexts.get(CacheableOperation.class), CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
}
Object cacheValue;
Object returnValue;
if (cacheHit != null && !this.hasCachePut(contexts)) {
//从缓存命中到Value
cacheValue = cacheHit.get();
//Value格式化
returnValue = this.wrapCacheValue(method, cacheValue);
} else {
//从缓存未命中到Value
returnValue = this.invokeOperation(invoker);
cacheValue = this.unwrapReturnValue(returnValue);
}
//put函数调用
this.collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);
Iterator var8 = cachePutRequests.iterator();
while(var8.hasNext()) {
//put函数调用
CacheAspectSupport.CachePutRequest cachePutRequest = (CacheAspectSupport.CachePutRequest)var8.next();
cachePutRequest.apply(cacheValue);
}
//evict函数调用
this.processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);
return returnValue;
}
}
}
分析
很明显,get(Object key, @Nullable Class
jdk 中自带的序列化技术,不需要 class 作为参数的,spring 的 cache 更像是为此设计的。
传统的缓存(像是 ehcache),具备天然的优势,能很轻松就能完成接口对接。
仍然存在未知的点:如何触发 get(Object key, @Nullable Class
在后续开发中,我也放弃了自定义 cache,接触答案的机会就更少了;
而且,我的代码本身是偏保守的,很多很新的特性都没碰过。
(看看什么时候,还有大把的时间摸鱼,再来研究研究吧)