Spring 3.1 引入了激动人心的基于注释(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案(例如EHCache 或者 OSCache),而是一个对缓存使用的抽象,通过在既有代码中添加少量它定义的各种 annotation,即能够达到缓存方法的返回对象的效果。

Spring 的缓存技术还具备相当的灵活性,不仅能够使用 SpEL(Spring Expression Language)来定义缓存的 key 和各种 condition,还提供开箱即用的缓存临时存储方案,也支持和主流的专业缓存例如 EHCache 集成。

其特点总结如下:

  • 通过少量的配置 annotation 注释即可使得既有代码支持缓存
  • 支持开箱即用 Out-Of-The-Box,即不用安装和部署额外第三方组件即可使用缓存
  • 支持 Spring Express Language,能使用对象的任何属性或者方法来定义缓存的 key 和 condition
  • 支持 AspectJ,并通过其实现任何方法的缓存支持
  • 支持自定义 key 和自定义缓存管理者,具有相当的灵活性和扩展性

1.EnableCaching

开启spring注解驱动的cache管理能力,类似于spring xml命名空间<cache:*>的支持。将会和org.springframework.context.annotation.Configuration一起使用,示例:

 @Configuration
  @EnableCaching
  public class AppConfig {
    @Bean
      public MyService myService() {
          // configure and return a class having @Cacheable methods
          return new MyService();
      }
 
      @Bean
      public CacheManager cacheManager() {
          // configure and return an implementation of Spring's CacheManager SPI
          SimpleCacheManager cacheManager = new SimpleCacheManager();
          cacheManager.addCaches(Arrays.asList(new ConcurrentMapCache("default")));
          return cacheManager;
      }
  }

相应的xml配置如下:

  <beans>
      <cache:annotation-driven/>
      <bean id="myService" class="com.foo.MyService"/>
      <bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
         <property name="caches">
             <set>
                  <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean">
                      <property name="name" value="default"/>
                  </bean>
              </set>
          </property>
      </bean>
  </beans>

  在上述的场景中,@EnableCaching和<cache:annotation-driven/>负责注册spring所需的管理注解驱动的cache管理组件,如org.springframework.cache.interceptor.CacheInterceptor,当org.springframework.cache.annotation.Cacheable方法触发时基于proxy-或者基于AspectJ的advice 会将interceptor织入调用栈中。

  必须注册org.springframework.cache.CacheManager类型的bean,原因是框架没有默认的可用。不同点在于<cache:annotation-driven>元素假定了一个名称为cacheManager的bean,而@EnableCaching通过类型检索一个cache manage bean。因此cache manager bean方法的命名不是显式的。

  如果希望使用一种@EnableCaching与cache manager bean更直接的方式,spring提供了CachingConfigurer回调接口的实现,注意实现语句和@override注解方法:

 @Configuration
 @EnableCaching
 public class AppConfig extends CachingConfigurerSupport {

     @Bean
     public MyService myService() {
         // configure and return a class having @Cacheable methods
         return new MyService();
     }

     @Bean
     @Override
     public CacheManager cacheManager() {
         // configure and return an implementation of Spring's CacheManager SPI
         SimpleCacheManager cacheManager = new SimpleCacheManager();
         cacheManager.setCaches(Arrays.asList(new ConcurrentMapCache("default")));
         return cacheManager;
     }

     @Bean
     @Override
     public KeyGenerator keyGenerator() {
         // configure and return an implementation of Spring's KeyGenerator SPI
         return new MyKeyGenerator();
     }
 }

 注意,上述示例中的keyGenerator方法,它允许自定义一个cache key产生策略。每个spring的org.springframework.cache.interceptor.KeyGenerator spi,通常将配置spring的org.springframework.cache.interceptor.SimpleKeyGenerator,一个key生成器必须显示指明。如果不需要定制,则直接返回new SimpleKeyGenerator()。

1.1 方法

boolean proxyTargetClass() default false;

表明是创建基于子类代理(cglib)还是基于接口的标准java代理,默认是false。当且仅当mode()设置成Advice#PROXY时其作用。

注意,这个属性设置为true时会影响到所有spring管理bean的代理,而不仅仅是被标识为@Cacheable的接口或者类。例如,spring标注的其它bean如@Transactional将会同时升级到子类代理。这种方式实际上没有负面影响,除非显示期望一种代理类型,例如测试。

1.2 源码

1.2.1 xml源码解析

CacheNamespaceHandler.java

public class CacheNamespaceHandler extends NamespaceHandlerSupport {

    static final String CACHE_MANAGER_ATTRIBUTE = "cache-manager";

    static final String DEFAULT_CACHE_MANAGER_BEAN_NAME = "cacheManager";

    static String extractCacheManager(Element element) {
        return (element.hasAttribute(CacheNamespaceHandler.CACHE_MANAGER_ATTRIBUTE) ? element
                .getAttribute(CacheNamespaceHandler.CACHE_MANAGER_ATTRIBUTE)
                : CacheNamespaceHandler.DEFAULT_CACHE_MANAGER_BEAN_NAME);
    }

    static BeanDefinition parseKeyGenerator(Element element, BeanDefinition def) {
        String name = element.getAttribute("key-generator");
        if (StringUtils.hasText(name)) {
            def.getPropertyValues().add("keyGenerator", new RuntimeBeanReference(name.trim()));
        }
        return def;
    }

    @Override
    public void init() {
        registerBeanDefinitionParser("annotation-driven", new AnnotationDrivenCacheBeanDefinitionParser());
        registerBeanDefinitionParser("advice", new CacheAdviceParser());
    }
}

1.2.1.1 注册了AnnotationDrivenCacheBeanDefinitionParser

    /**
     * Parses the '{@code <cache:annotation-driven>}' tag. Will
     * {@link AopNamespaceUtils#registerAutoProxyCreatorIfNecessary
     * register an AutoProxyCreator} with the container as necessary.
     */
    @Override
    public BeanDefinition parse(Element element, ParserContext parserContext) {
        String mode = element.getAttribute("mode");
        if ("aspectj".equals(mode)) {
            // mode="aspectj"
            registerCacheAspect(element, parserContext);
        }
        else {
            // mode="proxy"
            AopAutoProxyConfigurer.configureAutoProxyCreator(element, parserContext);
        }

        return null;
    }

aspectJ方式

    /**
     * Registers a
     * <pre class="code">
     * <bean id="cacheAspect" class="org.springframework.cache.aspectj.AnnotationCacheAspect" factory-method="aspectOf">
     *   <property name="cacheManager" ref="cacheManager"/>
     *   <property name="keyGenerator" ref="keyGenerator"/>
     * </bean>
     * </pre>
     */
    private void registerCacheAspect(Element element, ParserContext parserContext) {
        if (!parserContext.getRegistry().containsBeanDefinition(AnnotationConfigUtils.CACHE_ASPECT_BEAN_NAME)) {
            RootBeanDefinition def = new RootBeanDefinition();
            def.setBeanClassName(AnnotationConfigUtils.CACHE_ASPECT_CLASS_NAME);
            def.setFactoryMethodName("aspectOf");
            parseCacheManagerProperty(element, def);
            CacheNamespaceHandler.parseKeyGenerator(element, def);
            parserContext.registerBeanComponent(new BeanComponentDefinition(def, AnnotationConfigUtils.CACHE_ASPECT_BEAN_NAME));
        }
    }

aop方式:

/**
     * Inner class to just introduce an AOP framework dependency when actually in proxy mode.
     */
    private static class AopAutoProxyConfigurer {

        public static void configureAutoProxyCreator(Element element, ParserContext parserContext) {
            AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(parserContext, element);

            if (!parserContext.getRegistry().containsBeanDefinition(AnnotationConfigUtils.CACHE_ADVISOR_BEAN_NAME)) {
                Object eleSource = parserContext.extractSource(element);

                // Create the CacheOperationSource definition.
                RootBeanDefinition sourceDef = new RootBeanDefinition("org.springframework.cache.annotation.AnnotationCacheOperationSource");
                sourceDef.setSource(eleSource);
                sourceDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
                String sourceName = parserContext.getReaderContext().registerWithGeneratedName(sourceDef);

                // Create the CacheInterceptor definition.
                RootBeanDefinition interceptorDef = new RootBeanDefinition(CacheInterceptor.class);
                interceptorDef.setSource(eleSource);
                interceptorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
                parseCacheManagerProperty(element, interceptorDef);
                CacheNamespaceHandler.parseKeyGenerator(element, interceptorDef);
                interceptorDef.getPropertyValues().add("cacheOperationSources", new RuntimeBeanReference(sourceName));
                String interceptorName = parserContext.getReaderContext().registerWithGeneratedName(interceptorDef);

                // Create the CacheAdvisor definition.
                RootBeanDefinition advisorDef = new RootBeanDefinition(BeanFactoryCacheOperationSourceAdvisor.class);
                advisorDef.setSource(eleSource);
                advisorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
                advisorDef.getPropertyValues().add("cacheOperationSource", new RuntimeBeanReference(sourceName));
                advisorDef.getPropertyValues().add("adviceBeanName", interceptorName);
                if (element.hasAttribute("order")) {
                    advisorDef.getPropertyValues().add("order", element.getAttribute("order"));
                }
                parserContext.getRegistry().registerBeanDefinition(AnnotationConfigUtils.CACHE_ADVISOR_BEAN_NAME, advisorDef);

                CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(),
                        eleSource);
                compositeDef.addNestedComponent(new BeanComponentDefinition(sourceDef, sourceName));
                compositeDef.addNestedComponent(new BeanComponentDefinition(interceptorDef, interceptorName));
                compositeDef.addNestedComponent(new BeanComponentDefinition(advisorDef, AnnotationConfigUtils.CACHE_ADVISOR_BEAN_NAME));
                parserContext.registerComponent(compositeDef);
            }
        }

1.2.1.2 注册了CacheAdviceParser,处理子注解

    @Override
    protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
        builder.addPropertyReference("cacheManager", CacheNamespaceHandler.extractCacheManager(element));
        CacheNamespaceHandler.parseKeyGenerator(element, builder.getBeanDefinition());

        List<Element> cacheDefs = DomUtils.getChildElementsByTagName(element, DEFS_ELEMENT);
        if (cacheDefs.size() >= 1) {
            // Using attributes source.
            List<RootBeanDefinition> attributeSourceDefinitions = parseDefinitionsSources(cacheDefs, parserContext);
            builder.addPropertyValue("cacheOperationSources", attributeSourceDefinitions);
        }
        else {
            // Assume annotations source.
            builder.addPropertyValue("cacheOperationSources",
                    new RootBeanDefinition("org.springframework.cache.annotation.AnnotationCacheOperationSource"));
        }
    }

 

调用解析方法,包括

cacheable,
cache-evict,
cache-put,
method,
caching
    private static final String CACHEABLE_ELEMENT = "cacheable";

    private static final String CACHE_EVICT_ELEMENT = "cache-evict";

    private static final String CACHE_PUT_ELEMENT = "cache-put";

    private static final String METHOD_ATTRIBUTE = "method";

    private static final String DEFS_ELEMENT = "caching";

private RootBeanDefinition parseDefinitionSource(Element definition, ParserContext parserContext) {
        Props prop = new Props(definition);
        // add cacheable first

        ManagedMap<TypedStringValue, Collection<CacheOperation>> cacheOpMap = new ManagedMap<TypedStringValue, Collection<CacheOperation>>();
        cacheOpMap.setSource(parserContext.extractSource(definition));

        List<Element> cacheableCacheMethods = DomUtils.getChildElementsByTagName(definition, CACHEABLE_ELEMENT);

        for (Element opElement : cacheableCacheMethods) {
            String name = prop.merge(opElement, parserContext.getReaderContext());
            TypedStringValue nameHolder = new TypedStringValue(name);
            nameHolder.setSource(parserContext.extractSource(opElement));
            CacheableOperation op = prop.merge(opElement, parserContext.getReaderContext(), new CacheableOperation());
            op.setUnless(getAttributeValue(opElement, "unless", ""));

            Collection<CacheOperation> col = cacheOpMap.get(nameHolder);
            if (col == null) {
                col = new ArrayList<CacheOperation>(2);
                cacheOpMap.put(nameHolder, col);
            }
            col.add(op);
        }

        List<Element> evictCacheMethods = DomUtils.getChildElementsByTagName(definition, CACHE_EVICT_ELEMENT);

        for (Element opElement : evictCacheMethods) {
            String name = prop.merge(opElement, parserContext.getReaderContext());
            TypedStringValue nameHolder = new TypedStringValue(name);
            nameHolder.setSource(parserContext.extractSource(opElement));
            CacheEvictOperation op = prop.merge(opElement, parserContext.getReaderContext(), new CacheEvictOperation());

            String wide = opElement.getAttribute("all-entries");
            if (StringUtils.hasText(wide)) {
                op.setCacheWide(Boolean.valueOf(wide.trim()));
            }

            String after = opElement.getAttribute("before-invocation");
            if (StringUtils.hasText(after)) {
                op.setBeforeInvocation(Boolean.valueOf(after.trim()));
            }

            Collection<CacheOperation> col = cacheOpMap.get(nameHolder);
            if (col == null) {
                col = new ArrayList<CacheOperation>(2);
                cacheOpMap.put(nameHolder, col);
            }
            col.add(op);
        }

        List<Element> putCacheMethods = DomUtils.getChildElementsByTagName(definition, CACHE_PUT_ELEMENT);

        for (Element opElement : putCacheMethods) {
            String name = prop.merge(opElement, parserContext.getReaderContext());
            TypedStringValue nameHolder = new TypedStringValue(name);
            nameHolder.setSource(parserContext.extractSource(opElement));
            CachePutOperation op = prop.merge(opElement, parserContext.getReaderContext(), new CachePutOperation());
            op.setUnless(getAttributeValue(opElement, "unless", ""));

            Collection<CacheOperation> col = cacheOpMap.get(nameHolder);
            if (col == null) {
                col = new ArrayList<CacheOperation>(2);
                cacheOpMap.put(nameHolder, col);
            }
            col.add(op);
        }

        RootBeanDefinition attributeSourceDefinition = new RootBeanDefinition(NameMatchCacheOperationSource.class);
        attributeSourceDefinition.setSource(parserContext.extractSource(definition));
        attributeSourceDefinition.getPropertyValues().add("nameMap", cacheOpMap);
        return attributeSourceDefinition;
    }

 其中,

    • @Cacheable 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存
    • @CachePut 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用
    • @CachEvict 主要针对方法配置,能够根据一定的条件对缓存进行清空

 

@Cacheable、@CachePut、@CacheEvict 注释介绍

通过上面的源码,我们可以看到 spring cache 主要使用两个注释标签,即 @Cacheable、@CachePut 和 @CacheEvict,我们总结一下其作用和配置方法。

表 1. @Cacheable 作用和配置方法
@Cacheable 的作用主要针对方法配置,能够根据方法的请求参数对其结果进行缓存
@Cacheable 主要的参数
value 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 例如:
@Cacheable(value=”mycache”) 或者 
@Cacheable(value={”cache1”,”cache2”}
key 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 例如:
@Cacheable(value=”testcache”,key=”#userName”)
condition 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 例如:
@Cacheable(value=”testcache”,condition=”#userName.length()>2”)
表 2. @CachePut 作用和配置方法
@CachePut 的作用主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用
@CachePut 主要的参数
value 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 例如:
@Cacheable(value=”mycache”) 或者 
@Cacheable(value={”cache1”,”cache2”}
key 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 例如:
@Cacheable(value=”testcache”,key=”#userName”)
condition 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 例如:
@Cacheable(value=”testcache”,condition=”#userName.length()>2”)
表 3. @CacheEvict 作用和配置方法
@CachEvict 的作用主要针对方法配置,能够根据一定的条件对缓存进行清空
@CacheEvict 主要的参数
value 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 例如:
@CachEvict(value=”mycache”) 或者 
@CachEvict(value={”cache1”,”cache2”}
key 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 例如:
@CachEvict(value=”testcache”,key=”#userName”)
condition 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才清空缓存 例如:
@CachEvict(value=”testcache”,
condition=”#userName.length()>2”)
allEntries 是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存 例如:
@CachEvict(value=”testcache”,allEntries=true)
beforeInvocation 是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存 例如:
@CachEvict(value=”testcache”,beforeInvocation=true)
 

小结

  总之,注释驱动的 spring cache 能够极大的减少我们编写常见缓存的代码量,通过少量的注释标签和配置文件,即可达到使代码具备缓存的能力。且具备很好的灵活性和扩展性。但是我们也应该看到,spring cache 由于基于 spring AOP 技术,尤其是动态的 proxy 技术,导致其不能很好的支持方法的内部调用或者非 public 方法的缓存设置,当然这都是可以解决的问题,通过学习这个技术,我们能够认识到,AOP 技术的应用还是很广泛的,如果有兴趣,我相信你也能基于 AOP 实现自己的缓存方案。

参考文献

【1】https://www.ibm.com/developerworks/cn/opensource/os-cn-spring-cache/

posted on 2016-08-03 08:35  一天不进步,就是退步  阅读(7416)  评论(0编辑  收藏  举报