• SpringBoot2(九)Cache接口get函数无效问题


    原本想基于Lettuce,自己写一个Redis的Cache,自定义Cache难度不高,但是在编码过程中,发现get(Object key, Class<T> aClass)函数从未被调用过,导致计划迟迟未完成。

    自定义Cache的基本代码

    package cn.seaboot.plugin.redis;
    
    import cn.seaboot.admin.consts.SystemConst;
    import cn.seaboot.common.core.Converter;
    import cn.seaboot.common.exception.BizException;
    import org.springframework.cache.Cache;
    import org.springframework.cache.support.SimpleValueWrapper;
    
    import java.util.HashMap;
    import java.util.Map;
    import java.util.concurrent.Callable;
    
    /**
     *
     */
    public class RedisCache implements Cache {
    
      Map<Object, Object> map = new HashMap<>();
    
      /**
       * 简单直白,就是获取Cache的名字
       */
      @Override
      public String getName() {
        return SystemConst.CACHE_DEF;
      }
    
      /**
       * 获取底层的缓存实现对象
       */
      @Override
      public Object getNativeCache() {
        return SystemConst.CACHE_DEF;
      }
    
      /**
       * 根据键获取值,把值包装在ValueWrapper里面,如果有必要可以附加额外信息
       */
      @Override
      public ValueWrapper get(Object key) {
        System.out.println("ValueWrapper");
        throw new BizException("test");
    //    return map.containsKey(key)?new SimpleValueWrapper(map.get(key)):null;
      }
    
      /**
       * 代码封装中希望被调用的代码
       */
      @Override
      public <T> T get(Object key, Class<T> aClass) {
        try {
          System.out.println("get(Object o, Class<T> aClass)");
          return map.containsKey(key)?Converter.convert(map.get(key), aClass):null;
        } catch (Exception e) {
          e.printStackTrace();
        }
        return null;
      }
    
      /**
       * 从缓存中获取 key 对应的值,如果缓存没有命中,则添加缓存,
       * 此时可异步地从 valueLoader 中获取对应的值(4.3版本新增)
       * 与缓存标签中的sync属性有关
       */
      @Override
      public <T> T get(Object key, Callable<T> valueLoader) {
        try {
          System.out.println("get");
          return valueLoader.call();
        } catch (Exception e) {
          e.printStackTrace();
        }
        return null;
      }
    
      /**
       * 存放键值对
       */
      @Override
      public void put(Object key, Object value) {
        System.out.println("put(Object key, Object value)");
        map.put(key, value);
      }
    
      /**
       * 如果键对应的值不存在,则添加键值对
       */
      @Override
      public ValueWrapper putIfAbsent(Object key, Object value) {
        System.out.println("putIfAbsent");
        map.put(key, value);
        return new SimpleValueWrapper(value);
      }
    
      /**
       * 移除键对应键值对
       */
      @Override
      public void evict(Object key) {
        System.out.println("evict");
        map.remove(key);
      }
    
      /**
       * 清空缓存
       */
      @Override
      public void clear() {
        System.out.println("clear");
        map.clear();
      }
    }

    问题

    数据缓存需要经过序列化,转为String,或者byte[];在取值的时候,需要数据还原,JSON是我们最常用的,它的序列化依赖Class对象:

    JSON.parseObject(String json, Class<T> clazz)

     因此,Cache接口中,get(Object key, Class<T> aClass)是我们最希望被调用的函数,而实际使用中,此函数却从未触发过,这是为何 ?

    (后面提到的get函数,均指此函数,重点分析此函数不被调用的原因)

      @Override
      public <T> T get(Object key, Class<T> aClass) {
        try {
          System.out.println("get(Object o, Class<T> aClass)");
          return map.containsKey(key)?Converter.convert(map.get(key), aClass):null;
        } catch (Exception e) {
          e.printStackTrace();
        }
        return null;
      }

    分析

    打印异常栈,分析源码,核心异常如下:

    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函数确实没被调用过

      @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;
        }
      }

    其中最可疑的wrapCacheValue,作用是包装缓存值,合理的代码逻辑,会对返回值进行二次封装,然而,此处代码仅仅只是用Optional进行了处理,并未做更有效的处理,应该是Java8下的代码调整。

      @Nullable
      private Object wrapCacheValue(Method method, @Nullable Object cacheValue) {
        return method.getReturnType() != Optional.class || cacheValue != null && cacheValue.getClass() == Optional.class ? cacheValue : Optional.ofNullable(cacheValue);
      }

    推测

    JDK自带的对象序列化技术,在对象转为byte[]之后,内容中已经包含了全类名,byte[]转为对象,可以自动转型,不需要Class作为参数,。
    Spring的Cache十分符合JDK的用法,传统的缓存EhCache,设计上就采用了原生的序列化。

    JSON是Douglas Crockford在2001年开始推广使用的数据格式,在2005年-2006年正式成为主流的数据格式,

    而Spring 框架最开始的部分是由Rod Johnson于2000年为伦敦金融界提供独立咨询业务时写出来的。

    两者产生的时间差异,可能是这一现象的原因:

    Spring重点对JDK原生代码做了支持,因为JDK不需要Class作为参数,随着第三方序列化的产生,尽管接口上已经设计好,但是并未改变原先的做法。

    序列化测试代码:

    import java.io.*;
    
    /**
     * @author Mr.css
     * @date 2019/12/24 14:38
     */
    class A implements Serializable {
    }
    
    public class Test {
      public static void main(String[] args) throws IOException, ClassNotFoundException {
    //    A a = new A();
    //    FileOutputStream is = new FileOutputStream("C:\Users\postm\Desktop\test.txt");
    //    try (ObjectOutputStream oos = new ObjectOutputStream(is)) {
    //      oos.writeObject(a);
    //    }
    
        FileInputStream is = new FileInputStream("C:\Users\postm\Desktop\test.txt");
        ObjectInputStream ois = new ObjectInputStream(is);
        A a = (A)ois.readObject();
      }
    }
  • 相关阅读:
    arcgis使用python,做arctoolbox,渔网裁剪gdb里的要素,四等分
    arcgis计算中心点的质心XY,并根据属性Label进行标注。
    arcpy对要素类每个图斑批量出图。
    python安装模块wheel步骤
    arcgis中shp文件的字符串string型字段转换为日期型Date格式。如“20190426”转为“2019/4/26”
    .tar.gz海量遥感影像解压
    python压缩解压文件(天地图切片批量解压缩)
    python读取多层嵌套文件夹中的文件(zip文件嵌套在不同层级的文件夹中)
    使用Binding时关于数据更新的注意事项
    滑动列表底部自动加载下一页。修改旧代码ing
  • 原文地址:https://www.cnblogs.com/chenss15060100790/p/12094489.html
Copyright © 2020-2023  润新知