springboot2 - cache接口get函数无效问题

需要有自定义 cache 的经验

问题描述

对比 JSON 和 Cache 两个接口,容易陷入一个误区:

将 JSON.parseObject(String text, Type type),写到了 Cache 的 get(Object key, @Nullable Class type) 函数中。


我期望的效果是:根据 key 值,从缓存中取出 json,然后根据 class 的值,将 json 转换成对应的对象。

实际效果却不尽如人意:从缓存中取数据的时候,走的是 get(Object key) 函数,根本不触发 get(Object key, @Nullable Class type)。

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 type) 函数,实际的功能是:

如果缓存无法转换成指定的类型,就抛出异常,class 参数起到校验的功能,而不是作为类型转换的依据。


从 Cache 的注释上,也能看出问题,get(Object key, Class type) 在 spring4 之后才出现,

如果是为了做类型转换,不可能等这个版本才设计这个接口,很可能是为了某些新特性才增加的。

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 type) 函数。

缓存的使用方式
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 type)的作用,并不是我们所期待的那样。


jdk 中自带的序列化技术,不需要 class 作为参数的,spring 的 cache 更像是为此设计的。

传统的缓存(像是 ehcache),具备天然的优势,能很轻松就能完成接口对接。


仍然存在未知的点:如何触发 get(Object key, @Nullable Class type)?

在后续开发中,我也放弃了自定义 cache,接触答案的机会就更少了;

而且,我的代码本身是偏保守的,很多很新的特性都没碰过。

(看看什么时候,还有大把的时间摸鱼,再来研究研究吧)

posted on 2019-12-25 01:10  疯狂的妞妞  阅读(1252)  评论(0编辑  收藏  举报

导航