Search code examples
springcachingtransactional

Spring cache binded only to the current transaction


I'm trying to convince my company to work with spring 3.2 's cache (I know it's very old).

The application is build on top alfresco 5.x (which is build on top of spring 3.2).

Currently, we have some cache binded to the current transaction :

if (AlfrescoTransactionSupport.getTransactionReadState() == TxnReadState.TXN_READ_ONLY) {
  cache = (Map<String, Boolean>) AlfrescoTransactionSupport.getResource(CACHED_NAME);
  if (cache == null) {
    cache = new HashMap<String, Boolean>();
  }
  AlfrescoTransactionSupport.bindResource(CACHED_NAME, cache);
}

The cache live only for the current read transaction and then, destroyed.

I've tryied

@Cacheable("cache_name") 
@Transactional(readOnly=true)

Annotation, but when a read-write transaction is open, the cache is not destroyed.

Any idea how to do that in spring way ?


Solution

  • @biiyamn was right,

    I had to implement my own cache to do that.

    First of all, i had ti implement the BeanFactory :

    import org.springframework.beans.factory.BeanNameAware;
    import org.springframework.beans.factory.FactoryBean;
    import org.springframework.beans.factory.InitializingBean;
    import org.springframework.util.StringUtils;
    
    public class KReadTransactionCacheFactoryBean implements FactoryBean<KReadTransactionCache>, BeanNameAware,
        InitializingBean {
    
      private String name = "";
    
      private boolean allowNullValues = true;
    
      private KReadTransactionCache cache;
    
      /**
       * Specify the name of the cache.
       * <p>Default is "" (empty String).
       */
      public void setName(String name) {
        this.name = name;
      }
    
      /**
       * Set whether to allow {@code null} values
       * (adapting them to an internal null holder value).
       * <p>Default is "true".
       */
      public void setAllowNullValues(boolean allowNullValues) {
        this.allowNullValues = allowNullValues;
      }
    
      public void setBeanName(String beanName) {
        if (!StringUtils.hasLength(this.name)) {
          setName(beanName);
        }
      }
    
      public void afterPropertiesSet() {
        this.cache = new KReadTransactionCache(this.name, this.allowNullValues);
      }
    
      public KReadTransactionCache getObject() {
        return this.cache;
      }
    
      public Class<?> getObjectType() {
        return KReadTransactionCache.class;
      }
    
      public boolean isSingleton() {
        return false;
      }
    
    }
    

    Then, implement de cache binded to current transaction

    import java.io.Serializable;
    import java.util.HashMap;
    import java.util.Map;
    
    import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
    import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState;
    import org.springframework.cache.Cache;
    import org.springframework.cache.support.SimpleValueWrapper;
    
    public class KReadTransactionCache implements Cache {
    
      private static final Object NULL_HOLDER = new NullHolder();
    
      private final String name;
    
      private final boolean allowNullValues;
    
      /**
       * Create a new ConcurrentMapCache with the specified name.
       * @param name the name of the cache
       */
      public KReadTransactionCache(String name) {
        this(name, true);
      }
    
      protected static Map<Object, Object> getBindedCache(String name) {
        Map<Object, Object> cache = null;
        if (AlfrescoTransactionSupport.getTransactionReadState() == TxnReadState.TXN_READ_ONLY) {
          cache = AlfrescoTransactionSupport.getResource(name);
          if (cache == null) {
            cache = new HashMap<>();
          }
          AlfrescoTransactionSupport.bindResource(name, cache);
        }
        return cache;
      }
    
      /**
       * Create a new Map with the specified name and the
       * given internal ConcurrentMap to use.
       * @param name the name of the cache
       * @param allowNullValues whether to allow {@code null} values
       * (adapting them to an internal null holder value)
       */
      public KReadTransactionCache(String name, boolean allowNullValues) {
        this.name = name;
        this.allowNullValues = allowNullValues;
    
      }
    
      public String getName() {
        return this.name;
      }
    
      public Map getNativeCache() {
        return getBindedCache(name);
      }
    
      public boolean isAllowNullValues() {
        return this.allowNullValues;
      }
    
      public ValueWrapper get(Object key) {
        final Map<Object, Object> bindedCache = getBindedCache(name);
        if (bindedCache == null) {
          return null;
        }
        Object value = bindedCache.get(key);
        return (value != null ? new SimpleValueWrapper(fromStoreValue(value)) : null);
      }
    
      public void put(Object key, Object value) {
        final Map<Object, Object> bindedCache = getBindedCache(name);
        if (bindedCache == null) {
          return;
        }
    
        bindedCache.put(key, toStoreValue(value));
      }
    
      public void evict(Object key) {
        final Map<Object, Object> bindedCache = getBindedCache(name);
        if (bindedCache == null) {
          return;
        }
        bindedCache.remove(key);
      }
    
      public void clear() {
        final Map<Object, Object> bindedCache = getBindedCache(name);
        if (bindedCache == null) {
          return;
        }
        bindedCache.clear();
      }
    
      /**
       * Convert the given value from the internal store to a user value
       * returned from the get method (adapting {@code null}).
       * @param storeValue the store value
       * @return the value to return to the user
       */
      protected Object fromStoreValue(Object storeValue) {
        if (this.allowNullValues && storeValue == NULL_HOLDER) {
          return null;
        }
        return storeValue;
      }
    
      /**
       * Convert the given user value, as passed into the put method,
       * to a value in the internal store (adapting {@code null}).
       * @param userValue the given user value
       * @return the value to store
       */
      protected Object toStoreValue(Object userValue) {
        if (this.allowNullValues && userValue == null) {
          return NULL_HOLDER;
        }
        return userValue;
      }
    
      @SuppressWarnings("serial")
      private static class NullHolder implements Serializable {
      }
    
    }
    

    And the xml configuration :

        <!-- *******************************
           ***** CACHE CONFIGURATION *****
           ******************************* -->
       <!-- simple cache manager -->
       <bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
          <property name="caches">
             <set>
                <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="default" />
                <bean class="path.to.package.KReadTransactionCacheFactoryBean" p:name="cacheNameByAnnotation" />
                <!-- TODO Add other cache instances in here -->
             </set>
          </property>
       </bean>