zoukankan      html  css  js  c++  java
  • Spring缓存注解@Cacheable、@CacheEvict、@CachePut使用

    从3.1开始,Spring引入了对Cache的支持。其使用方法和原理都类似于Spring对事务管理的支持。Spring Cache是作用在方法上的,其核心思想是这样的:当我们在调用一个缓存方法时会把该方法参数和返回结果作为一个键值对存放在缓存中,等到下次利用同样的参数来调用该方法时将不再执行该方法,而是直接从缓存中获取结果进行返回。所以在使用Spring Cache的时候我们要保证我们缓存的方法对于相同的方法参数要有相同的返回结果。

           使用Spring Cache需要我们做两方面的事:

    n  声明某些方法使用缓存

    n  配置Spring对Cache的支持

           和Spring对事务管理的支持一样,Spring对Cache的支持也有基于注解和基于XML配置两种方式。下面我们先来看看基于注解的方式。

    1       基于注解的支持

           Spring为我们提供了几个注解来支持Spring Cache。其核心主要是@Cacheable和@CacheEvict。使用@Cacheable标记的方法在执行后Spring Cache将缓存其返回结果,而使用@CacheEvict标记的方法会在方法执行前或者执行后移除Spring Cache中的某些元素。下面我们将来详细介绍一下Spring基于注解对Cache的支持所提供的几个注解。

    1.1    @Cacheable

           @Cacheable可以标记在一个方法上,也可以标记在一个类上。当标记在一个方法上时表示该方法是支持缓存的,当标记在一个类上时则表示该类所有的方法都是支持缓存的。对于一个支持缓存的方法,Spring会在其被调用后将其返回值缓存起来,以保证下次利用同样的参数来执行该方法时可以直接从缓存中获取结果,而不需要再次执行该方法。Spring在缓存方法的返回值时是以键值对进行缓存的,值就是方法的返回结果,至于键的话,Spring又支持两种策略,默认策略和自定义策略,这个稍后会进行说明。需要注意的是当一个支持缓存的方法在对象内部被调用时是不会触发缓存功能的。@Cacheable可以指定三个属性,value、key和condition。

    1.1.1  value属性指定Cache名称

           value属性是必须指定的,其表示当前方法的返回值是会被缓存在哪个Cache上的,对应Cache的名称。其可以是一个Cache也可以是多个Cache,当需要指定多个Cache时其是一个数组。

       @Cacheable("cache1")//Cache是发生在cache1上的

       public User find(Integer id) {

          returnnull;

       }

       @Cacheable({"cache1", "cache2"})//Cache是发生在cache1和cache2上的

       public User find(Integer id) {

          returnnull;

       }

    1.1.2  使用key属性自定义key

           key属性是用来指定Spring缓存方法的返回结果时对应的key的。该属性支持SpringEL表达式。当我们没有指定该属性时,Spring将使用默认策略生成key。我们这里先来看看自定义策略,至于默认策略会在后文单独介绍。

           自定义策略是指我们可以通过Spring的EL表达式来指定我们的key。这里的EL表达式可以使用方法参数及它们对应的属性。使用方法参数时我们可以直接使用“#参数名”或者“#p参数index”。下面是几个使用参数作为key的示例。

       @Cacheable(value="users", key="#id")

       public User find(Integer id) {

          returnnull;

       }

       @Cacheable(value="users", key="#p0")

       public User find(Integer id) {

          returnnull;

       }

       @Cacheable(value="users", key="#user.id")

       public User find(User user) {

          returnnull;

       }

       @Cacheable(value="users", key="#p0.id")

       public User find(User user) {

          returnnull;

       }

           除了上述使用方法参数作为key之外,Spring还为我们提供了一个root对象可以用来生成key。通过该root对象我们可以获取到以下信息。

    属性名称

    描述

    示例

    methodName

    当前方法名

    #root.methodName

    method

    当前方法

    #root.method.name

    target

    当前被调用的对象

    #root.target

    targetClass

    当前被调用的对象的class

    #root.targetClass

    args

    当前方法参数组成的数组

    #root.args[0]

    caches

    当前被调用的方法使用的Cache

    #root.caches[0].name

           当我们要使用root对象的属性作为key时我们也可以将“#root”省略,因为Spring默认使用的就是root对象的属性。如:

       @Cacheable(value={"users", "xxx"}, key="caches[1].name")

       public User find(User user) {

          returnnull;

       }

    1.1.3  condition属性指定发生的条件

           有的时候我们可能并不希望缓存一个方法所有的返回结果。通过condition属性可以实现这一功能。condition属性默认为空,表示将缓存所有的调用情形。其值是通过SpringEL表达式来指定的,当为true时表示进行缓存处理;当为false时表示不进行缓存处理,即每次调用该方法时该方法都会执行一次。如下示例表示只有当user的id为偶数时才会进行缓存。

       @Cacheable(value={"users"}, key="#user.id", condition="#user.id%2==0")

       public User find(User user) {

          System.out.println("find user by user " + user);

          return user;

       }

    1.2     @CachePut

           在支持Spring Cache的环境下,对于使用@Cacheable标注的方法,Spring在每次执行前都会检查Cache中是否存在相同key的缓存元素,如果存在就不再执行该方法,而是直接从缓存中获取结果进行返回,否则才会执行并将返回结果存入指定的缓存中。@CachePut也可以声明一个方法支持缓存功能。与@Cacheable不同的是使用@CachePut标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。

           @CachePut也可以标注在类上和方法上。使用@CachePut时我们可以指定的属性跟@Cacheable是一样的。

       @CachePut("users")//每次都会执行方法,并将结果存入指定的缓存中

       public User find(Integer id) {

          returnnull;

       }

    1.3     @CacheEvict

           @CacheEvict是用来标注在需要清除缓存元素的方法或类上的。当标记在一个类上时表示其中所有的方法的执行都会触发缓存的清除操作。@CacheEvict可以指定的属性有value、key、condition、allEntries和beforeInvocation。其中value、key和condition的语义与@Cacheable对应的属性类似。即value表示清除操作是发生在哪些Cache上的(对应Cache的名称);key表示需要清除的是哪个key,如未指定则会使用默认策略生成的key;condition表示清除操作发生的条件。下面我们来介绍一下新出现的两个属性allEntries和beforeInvocation。

    1.3.1  allEntries属性

           allEntries是boolean类型,表示是否需要清除缓存中的所有元素。默认为false,表示不需要。当指定了allEntries为true时,Spring Cache将忽略指定的key。有的时候我们需要Cache一下清除所有的元素,这比一个一个清除元素更有效率。

       @CacheEvict(value="users", allEntries=true)

       public void delete(Integer id) {

          System.out.println("delete user by id: " + id);

       }

    1.3.2  beforeInvocation属性

           清除操作默认是在对应方法成功执行之后触发的,即方法如果因为抛出异常而未能成功返回时也不会触发清除操作。使用beforeInvocation可以改变触发清除操作的时间,当我们指定该属性值为true时,Spring会在调用该方法之前清除缓存中的指定元素。

       @CacheEvict(value="users", beforeInvocation=true)

       public void delete(Integer id) {

          System.out.println("delete user by id: " + id);

       }

           其实除了使用@CacheEvict清除缓存元素外,当我们使用Ehcache作为实现时,我们也可以配置Ehcache自身的驱除策略,其是通过Ehcache的配置文件来指定的。由于Ehcache不是本文描述的重点,这里就不多赘述了,想了解更多关于Ehcache的信息,请查看我关于Ehcache的专栏。

    1.4     @Caching

           @Caching注解可以让我们在一个方法或者类上同时指定多个Spring Cache相关的注解。其拥有三个属性:cacheable、put和evict,分别用于指定@Cacheable、@CachePut和@CacheEvict。

       @Caching(cacheable = @Cacheable("users"), evict = { @CacheEvict("cache2"),

             @CacheEvict(value = "cache3", allEntries = true) })

       public User find(Integer id) {

          returnnull;

       }

    1.5     使用自定义注解

           Spring允许我们在配置可缓存的方法时使用自定义的注解,前提是自定义的注解上必须使用对应的注解进行标注。如我们有如下这么一个使用@Cacheable进行标注的自定义注解。

    @Target({ElementType.TYPE, ElementType.METHOD})

    @Retention(RetentionPolicy.RUNTIME)

    @Cacheable(value="users")

    public @interface MyCacheable {

    }

           那么在我们需要缓存的方法上使用@MyCacheable进行标注也可以达到同样的效果。

       @MyCacheable

       public User findById(Integer id) {

          System.out.println("find user by id: " + id);

          User user = new User();

          user.setId(id);

          user.setName("Name" + id);

          return user;

       }

    2       配置Spring对Cache的支持

    2.1     声明对Cache的支持

    2.1.1  基于注解

           配置Spring对基于注解的Cache的支持,首先我们需要在Spring的配置文件中引入cache命名空间,其次通过<cache:annotation-driven />就可以启用Spring对基于注解的Cache的支持。

    <?xml version="1.0" encoding="UTF-8"?>

    <beans xmlns="http://www.springframework.org/schema/beans"

       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

       xmlns:cache="http://www.springframework.org/schema/cache"

       xsi:schemaLocation="http://www.springframework.org/schema/beans

         http://www.springframework.org/schema/beans/spring-beans-3.0.xsd

         http://www.springframework.org/schema/cache

         http://www.springframework.org/schema/cache/spring-cache.xsd">

       <cache:annotation-driven/>

    </beans>

           <cache:annotation-driven/>有一个cache-manager属性用来指定当前所使用的CacheManager对应的bean的名称,默认是cacheManager,所以当我们的CacheManager的id为cacheManager时我们可以不指定该参数,否则就需要我们指定了。

           <cache:annotation-driven/>还可以指定一个mode属性,可选值有proxy和aspectj。默认是使用proxy。当mode为proxy时,只有缓存方法在外部被调用的时候Spring Cache才会发生作用,这也就意味着如果一个缓存方法在其声明对象内部被调用时Spring Cache是不会发生作用的。而mode为aspectj时就不会有这种问题。另外使用proxy时,只有public方法上的@Cacheable等标注才会起作用,如果需要非public方法上的方法也可以使用Spring Cache时把mode设置为aspectj。

           此外,<cache:annotation-driven/>还可以指定一个proxy-target-class属性,表示是否要代理class,默认为false。我们前面提到的@Cacheable、@cacheEvict等也可以标注在接口上,这对于基于接口的代理来说是没有什么问题的,但是需要注意的是当我们设置proxy-target-class为true或者mode为aspectj时,是直接基于class进行操作的,定义在接口上的@Cacheable等Cache注解不会被识别到,那对应的Spring Cache也不会起作用了。

           需要注意的是<cache:annotation-driven/>只会去寻找定义在同一个ApplicationContext下的@Cacheable等缓存注解。

    2.1.2  基于XML配置

           除了使用注解来声明对Cache的支持外,Spring还支持使用XML来声明对Cache的支持。这主要是通过类似于aop:advice的cache:advice来进行的。在cache命名空间下定义了一个cache:advice元素用来定义一个对于Cache的advice。其需要指定一个cache-manager属性,默认为cacheManager。cache:advice下面可以指定多个cache:caching元素,其有点类似于使用注解时的@Caching注解。cache:caching元素下又可以指定cache:cacheable、cache:cache-put和cache:cache-evict元素,它们类似于使用注解时的@Cacheable、@CachePut和@CacheEvict。下面来看一个示例:

       <cache:advice id="cacheAdvice" cache-manager="cacheManager">

          <cache:caching cache="users">

             <cache:cacheable method="findById" key="#p0"/>

             <cache:cacheable method="find" key="#user.id"/>

             <cache:cache-evict method="deleteAll" all-entries="true"/>

          </cache:caching>

       </cache:advice>

           上面配置定义了一个名为cacheAdvice的cache:advice,其中指定了将缓存findById方法和find方法到名为users的缓存中。这里的方法还可以使用通配符“*”,比如“find*”表示任何以“find”开始的方法。

           有了cache:advice之后,我们还需要引入aop命名空间,然后通过aop:config指定定义好的cacheAdvice要应用在哪些pointcut上。如:

       <aop:config proxy-target-class="false">

          <aop:advisor advice-ref="cacheAdvice" pointcut="execution(* com.xxx.UserService.*(..))"/>

       </aop:config>

           上面的配置表示在调用com.xxx.UserService中任意公共方法时将使用cacheAdvice对应的cache:advice来进行Spring Cache处理。更多关于Spring Aop的内容不在本文讨论范畴内。

    2.2     配置CacheManager

           CacheManager是Spring定义的一个用来管理Cache的接口。Spring自身已经为我们提供了两种CacheManager的实现,一种是基于Java API的ConcurrentMap,另一种是基于第三方Cache实现——Ehcache,如果我们需要使用其它类型的缓存时,我们可以自己来实现Spring的CacheManager接口或AbstractCacheManager抽象类。下面分别来看看Spring已经为我们实现好了的两种CacheManager的配置示例。

    2.2.1  基于ConcurrentMap的配置

       <bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">

          <property name="caches">

             <set>

                <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="xxx"/>

             </set>

          </property>

       </bean>

           上面的配置使用的是一个SimpleCacheManager,其中包含一个名为“xxx”的ConcurrentMapCache。

    2.2.2  基于Ehcache的配置

       <!-- Ehcache实现 -->

       <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager" p:cache-manager-ref="ehcacheManager"/>

       <bean id="ehcacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" p:config-location="ehcache-spring.xml"/>

           上面的配置使用了一个Spring提供的EhCacheCacheManager来生成一个Spring的CacheManager,其接收一个Ehcache的CacheManager,因为真正用来存入缓存数据的还是Ehcache。Ehcache的CacheManager是通过Spring提供的EhCacheManagerFactoryBean来生成的,其可以通过指定ehcache的配置文件位置来生成一个Ehcache的CacheManager。若未指定则将按照Ehcache的默认规则取classpath根路径下的ehcache.xml文件,若该文件也不存在,则获取Ehcache对应jar包中的ehcache-failsafe.xml文件作为配置文件。更多关于Ehcache的内容这里就不多说了,它不属于本文讨论的内容,欲了解更多关于Ehcache的内容可以参考我之前发布的Ehcache系列文章,也可以参考官方文档等。

    3       键的生成策略

           键的生成策略有两种,一种是默认策略,一种是自定义策略。

    3.1     默认策略

           默认的key生成策略是通过KeyGenerator生成的,其默认策略如下:

    n  如果方法没有参数,则使用0作为key。

    n  如果只有一个参数的话则使用该参数作为key。

    n  如果参数多余一个的话则使用所有参数的hashCode作为key。

           如果我们需要指定自己的默认策略的话,那么我们可以实现自己的KeyGenerator,然后指定我们的Spring Cache使用的KeyGenerator为我们自己定义的KeyGenerator。

           使用基于注解的配置时是通过cache:annotation-driven指定的.

       <cache:annotation-driven key-generator="userKeyGenerator"/>  

       <bean id="userKeyGenerator" class="com.xxx.cache.UserKeyGenerator"/>

           而使用基于XML配置时是通过cache:advice来指定的。

       <cache:advice id="cacheAdvice" cache-manager="cacheManager" key-generator="userKeyGenerator">

       </cache:advice>

           需要注意的是此时我们所有的Cache使用的Key的默认生成策略都是同一个KeyGenerator。

    3.2     自定义策略

           自定义策略是指我们可以通过Spring的EL表达式来指定我们的key。这里的EL表达式可以使用方法参数及它们对应的属性。使用方法参数时我们可以直接使用“#参数名”或者“#p参数index”。下面是几个使用参数作为key的示例。

       @Cacheable(value="users", key="#id")

       public User find(Integer id) {

          returnnull;

       }

       @Cacheable(value="users", key="#p0")

       public User find(Integer id) {

          returnnull;

       }

       @Cacheable(value="users", key="#user.id")

       public User find(User user) {

          returnnull;

       }

       @Cacheable(value="users", key="#p0.id")

       public User find(User user) {

          returnnull;

       }

           除了上述使用方法参数作为key之外,Spring还为我们提供了一个root对象可以用来生成key。通过该root对象我们可以获取到以下信息。

    属性名称

    描述

    示例

    methodName

    当前方法名

    #root.methodName

    method

    当前方法

    #root.method.name

    target

    当前被调用的对象

    #root.target

    targetClass

    当前被调用的对象的class

    #root.targetClass

    args

    当前方法参数组成的数组

    #root.args[0]

    caches

    当前被调用的方法使用的Cache

    #root.caches[0].name

           当我们要使用root对象的属性作为key时我们也可以将“#root”省略,因为Spring默认使用的就是root对象的属性。如:

       @Cacheable(value={"users", "xxx"}, key="caches[1].name")

       public User find(User user) {

          returnnull;

       }

    4       Spring单独使用Ehcache

           前面介绍的内容是Spring内置的对Cache的支持,其实我们也可以通过Spring自己单独的使用Ehcache的CacheManager或Ehcache对象。通过在Application Context中配置EhCacheManagerFactoryBean和EhCacheFactoryBean,我们就可以把对应的EhCache的CacheManager和Ehcache对象注入到其它的Spring bean对象中进行使用。

    4.1     EhCacheManagerFactoryBean

         EhCacheManagerFactoryBean是Spring内置的一个可以产生Ehcache的CacheManager对象的FactoryBean。其可以通过属性configLocation指定用于创建CacheManager的Ehcache配置文件的路径,通常是ehcache.xml文件的路径。如果没有指定configLocation,则将使用默认位置的配置文件创建CacheManager,这是属于Ehcache自身的逻辑,即如果在classpath根路径下存在ehcache.xml文件,则直接使用该文件作为Ehcache的配置文件,否则将使用ehcache-xxx.jar中的ehcache-failsafe.xml文件作为配置文件来创建Ehcache的CacheManager。此外,如果不希望创建的CacheManager使用默认的名称(在ehcache.xml文件中定义的,或者是由CacheManager内部定义的),则可以通过cacheManagerName属性进行指定。下面是一个配置EhCacheManagerFactoryBean的示例。

       <!-- 定义CacheManager -->

       <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">

          <!-- 指定配置文件的位置 -->

          <property name="configLocation" value="/WEB-INF/config/ehcache.xml"/>

          <!-- 指定新建的CacheManager的名称 -->

          <property name="cacheManagerName" value="cacheManagerName"/>

       </bean>

    4.2     EhCacheFactoryBean

           EhCacheFactoryBean是用来产生Ehcache的Ehcache对象的FactoryBean。定义EhcacheFactoryBean时有两个很重要的属性我们可以来指定。一个是cacheManager属性,其可以指定将用来获取或创建Ehcache的CacheManager对象,若未指定则将通过CacheManager.create()获取或创建默认的CacheManager。另一个重要属性是cacheName,其表示当前EhCacheFactoryBean对应的是CacheManager中的哪一个Ehcache对象,若未指定默认使用beanName作为cacheName。若CacheManager中不存在对应cacheName的Ehcache对象,则将使用CacheManager创建一个名为cacheName的Cache对象。此外我们还可以通过EhCacheFactoryBean的timeToIdle、timeToLive等属性指定要创建的Cache的对应属性,注意这些属性只对CacheManager中不存在对应Cache时新建的Cache才起作用,对已经存在的Cache将不起作用,更多属性设置请参考Spring的API文档。此外还有几个属性是对不管是已经存在还是新创建的Cache都起作用的属性:statisticsEnabled、sampledStatisticsEnabled、disabled、blocking和cacheEventListeners,其中前四个默认都是false,最后一个表示为当前Cache指定CacheEventListener。下面是一个定义EhCacheFactoryBean的示例。

       <!-- 定义CacheManager -->

       <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">

          <!-- 指定配置文件的位置 -->

          <property name="configLocation" value="/WEB-INF/config/ehcache.xml"/>

          <!-- 指定新建的CacheManager的名称 -->

          <property name="cacheManagerName" value="cacheManagerName"/>

       </bean>

      

       <!-- 定义一个Ehcache -->

       <bean id="userCache" class="org.springframework.cache.ehcache.EhCacheFactoryBean">

          <property name="cacheName" value="user"/>

          <property name="cacheManager" ref="cacheManager"/>

       </bean>

    转自:https://blog.csdn.net/wjacketcn/article/details/50945887

    Spring Cache抽象详解

    大部分的redis缓存就只是交给spring实例化,没有实现spring的缓存接口,看了这个文章更加让人理解

    spring cahe 

    缓存简介

    缓存,我的理解是:让数据更接近于使用者;工作机制是:先从缓存中读取数据,如果没有再从慢速设备上读取实际数据(数据也会存入缓存);缓存什么:那些经常读取且不经常修改的数据/那些昂贵(CPU/IO)的且对于相同的请求有相同的计算结果的数据。如CPU--L1/L2--内存--磁盘就是一个典型的例子,CPU需要数据时先从L1/L2中读取,如果没有到内存中找,如果还没有会到磁盘上找。还有如用过Maven的朋友都应该知道,我们找依赖的时候,先从本机仓库找,再从本地服务器仓库找,最后到远程仓库服务器找;还有如京东的物流为什么那么快?他们在各个地都有分仓库,如果该仓库有货物那么送货的速度是非常快的。

    缓存命中率

    即从缓存中读取数据的次数 与 总读取次数的比率,命中率越高越好:

    命中率 = 从缓存中读取次数 / (总读取次数[从缓存中读取次数 + 从慢速设备上读取的次数])

    Miss率 = 没有从缓存中读取的次数 / (总读取次数[从缓存中读取次数 + 从慢速设备上读取的次数])

    这是一个非常重要的监控指标,如果做缓存一定要健康这个指标来看缓存是否工作良好;

    缓存策略

    Eviction policy

    移除策略,即如果缓存满了,从缓存中移除数据的策略;常见的有LFU、LRU、FIFO:

    FIFO(First In First Out):先进先出算法,即先放入缓存的先被移除;

    LRU(Least Recently Used):最久未使用算法,使用时间距离现在最久的那个被移除;

    LFU(Least Frequently Used):最近最少使用算法,一定时间段内使用次数(频率)最少的那个被移除;

    TTL(Time To Live )

    存活期,即从缓存中创建时间点开始直到它到期的一个时间段(不管在这个时间段内有没有访问都将过期)

     

    TTI(Time To Idle)

    空闲期,即一个数据多久没被访问将从缓存中移除的时间。

    到此,基本了解了缓存的知识,在Java中,我们一般对调用方法进行缓存控制,比如我调用"findUserById(Long id)",那么我应该在调用这个方法之前先从缓存中查找有没有,如果没有再掉该方法如从数据库加载用户,然后添加到缓存中,下次调用时将会从缓存中获取到数据。

    自Spring 3.1起,提供了类似于@Transactional注解事务的注解Cache支持,且提供了Cache抽象;在此之前一般通过AOP实现;使用Spring Cache的好处:

    提供基本的Cache抽象,方便切换各种底层Cache;

    通过注解Cache可以实现类似于事务一样,缓存逻辑透明的应用到我们的业务代码上,且只需要更少的代码就可以完成;

    提供事务回滚时也自动回滚缓存;

    支持比较复杂的缓存逻辑;

    对于Spring Cache抽象,主要从以下几个方面学习:

    • Cache API及默认提供的实现
    • Cache注解
    • 实现复杂的Cache逻辑

    Cache API及默认提供的实现

    Spring提供的核心Cache接口: 

    Java代码  收藏代码

    1. package org.springframework.cache;  
    2.   
    3. public interface Cache {  
    4.     String getName();  //缓存的名字  
    5.     Object getNativeCache(); //得到底层使用的缓存,如Ehcache  
    6.     ValueWrapper get(Object key); //根据key得到一个ValueWrapper,然后调用其get方法获取值  
    7.     <T> T get(Object key, Class<T> type);//根据key,和value的类型直接获取value  
    8.     void put(Object key, Object value);//往缓存放数据  
    9.     void evict(Object key);//从缓存中移除key对应的缓存  
    10.     void clear(); //清空缓存  
    11.   
    12.     interface ValueWrapper { //缓存值的Wrapper  
    13.         Object get(); //得到真实的value  
    14.         }  
    15. }  

    提供了缓存操作的读取/写入/移除方法;

    默认提供了如下实现:

    ConcurrentMapCache:使用java.util.concurrent.ConcurrentHashMap实现的Cache;

    GuavaCache:对Guava com.google.common.cache.Cache进行的Wrapper,需要Google Guava 12.0或更高版本,@since spring 4;

    EhCacheCache:使用Ehcache实现

    JCacheCache:对javax.cache.Cache进行的wrapper,@since spring 3.2;spring4将此类更新到JCache 0.11版本;

    另外,因为我们在应用中并不是使用一个Cache,而是多个,因此Spring还提供了CacheManager抽象,用于缓存的管理: 

    Java代码  收藏代码

    1. package org.springframework.cache;  
    2. import java.util.Collection;  
    3. public interface CacheManager {  
    4.     Cache getCache(String name); //根据Cache名字获取Cache   
    5.     Collection<String> getCacheNames(); //得到所有Cache的名字  
    6. }  

    默认提供的实现: 

    ConcurrentMapCacheManager/ConcurrentMapCacheFactoryBean:管理ConcurrentMapCache;

    GuavaCacheManager;

    EhCacheCacheManager/EhCacheManagerFactoryBean;

    JCacheCacheManager/JCacheManagerFactoryBean;

    另外还提供了CompositeCacheManager用于组合CacheManager,即可以从多个CacheManager中轮询得到相应的Cache,如

    Java代码  收藏代码

    1. <bean id="cacheManager" class="org.springframework.cache.support.CompositeCacheManager">  
    2.     <property name="cacheManagers">  
    3.         <list>  
    4.             <ref bean="ehcacheManager"/>  
    5.             <ref bean="jcacheManager"/>  
    6.         </list>  
    7.     </property>  
    8.     <property name="fallbackToNoOpCache" value="true"/>  
    9. </bean>  

    当我们调用cacheManager.getCache(cacheName) 时,会先从第一个cacheManager中查找有没有cacheName的cache,如果没有接着查找第二个,如果最后找不到,因为fallbackToNoOpCache=true,那么将返回一个NOP的Cache否则返回null。

    除了GuavaCacheManager之外,其他Cache都支持Spring事务的,即如果事务回滚了,Cache的数据也会移除掉。

    Spring不进行Cache的缓存策略的维护,这些都是由底层Cache自己实现,Spring只是提供了一个Wrapper,提供一套对外一致的API。

    示例

    需要添加Ehcache依赖,具体依赖轻参考pom.xml

    SpringCacheTest.java

    Java代码  收藏代码

    1. @Test  
    2. public void test() throws IOException {  
    3.     //创建底层Cache  
    4.     net.sf.ehcache.CacheManager ehcacheManager  
    5.             = new net.sf.ehcache.CacheManager(new ClassPathResource("ehcache.xml").getInputStream());  
    6.   
    7.     //创建Spring的CacheManager  
    8.     EhCacheCacheManager cacheCacheManager = new EhCacheCacheManager();  
    9.     //设置底层的CacheManager  
    10.     cacheCacheManager.setCacheManager(ehcacheManager);  
    11.   
    12.     Long id = 1L;  
    13.     User user = new User(id, "zhang", "zhang@gmail.com");  
    14.   
    15.     //根据缓存名字获取Cache  
    16.     Cache cache = cacheCacheManager.getCache("user");  
    17.     //往缓存写数据  
    18.     cache.put(id, user);  
    19.     //从缓存读数据  
    20.     Assert.assertNotNull(cache.get(id, User.class));  
    21. }  

    此处直接使用Spring提供的API进行操作;我们也可以通过xml/注解方式配置到spring容器;

    xml风格的(spring-cache.xml):

    Java代码  收藏代码

    1. <bean id="ehcacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">  
    2.     <property name="configLocation" value="classpath:ehcache.xml"/>  
    3. </bean>  
    4.   
    5. <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">  
    6.     <property name="cacheManager" ref="ehcacheManager"/>  
    7.     <property name="transactionAware" value="true"/>  
    8. </bean>  

    spring提供EhCacheManagerFactoryBean来简化ehcache cacheManager的创建,这样注入configLocation,会自动根据路径从classpath下找,比编码方式简单多了,然后就可以从spring容器获取cacheManager进行操作了。此处的transactionAware表示是否事务环绕的,如果true,则如果事务回滚,缓存也回滚,默认false。

    注解风格的(AppConfig.java):

    Java代码  收藏代码

    1. @Bean  
    2. public CacheManager cacheManager() {  
    3.   
    4.     try {  
    5.         net.sf.ehcache.CacheManager ehcacheCacheManager  
    6.                 = new net.sf.ehcache.CacheManager(new ClassPathResource("ehcache.xml").getInputStream());  
    7.   
    8.         EhCacheCacheManager cacheCacheManager = new EhCacheCacheManager(ehcacheCacheManager);  
    9.         return cacheCacheManager;  
    10.     } catch (IOException e) {  
    11.         throw new RuntimeException(e);  
    12.     }  
    13. }  

    和编程方式差不多就不多介绍了。

    另外,除了这些默认的Cache之外,我们可以写自己的Cache实现;而且即使不用之后的Spring Cache注解,我们也尽量使用Spring Cache API进行Cache的操作,如果要替换底层Cache也是非常方便的。到此基本的Cache API就介绍完了,接下来我们来看看使用Spring Cache注解来简化Cache的操作。

    Cache注解

    启用Cache注解

    XML风格的(spring-cache.xml):

    Java代码  收藏代码

    1. <cache:annotation-driven cache-manager="cacheManager" proxy-target-class="true"/>  

    另外还可以指定一个 key-generator,即默认的key生成策略,后边讨论;

    注解风格的(AppConfig.java):

    Java代码  收藏代码

    1. @Configuration  
    2. @ComponentScan(basePackages = "com.sishuok.spring.service")  
    3. @EnableCaching(proxyTargetClass = true)  
    4. public class AppConfig implements CachingConfigurer {  
    5.     @Bean  
    6.     @Override  
    7.     public CacheManager cacheManager() {  
    8.   
    9.         try {  
    10.             net.sf.ehcache.CacheManager ehcacheCacheManager  
    11.                     = new net.sf.ehcache.CacheManager(new ClassPathResource("ehcache.xml").getInputStream());  
    12.   
    13.             EhCacheCacheManager cacheCacheManager = new EhCacheCacheManager(ehcacheCacheManager);  
    14.             return cacheCacheManager;  
    15.         } catch (IOException e) {  
    16.             throw new RuntimeException(e);  
    17.         }  
    18.     }  
    19.   
    20.     @Bean  
    21.     @Override  
    22.     public KeyGenerator keyGenerator() {  
    23.         return new SimpleKeyGenerator();  
    24.     }  
    25. }  

    1、使用@EnableCaching启用Cache注解支持;

    2、实现CachingConfigurer,然后注入需要的cacheManager和keyGenerator;从spring4开始默认的keyGenerator是SimpleKeyGenerator;

    @CachePut 

    应用到写数据的方法上,如新增/修改方法,调用方法时会自动把相应的数据放入缓存: 

    Java代码  收藏代码

    1. @CachePut(value = "user", key = "#user.id")  
    2. public User save(User user) {  
    3.     users.add(user);  
    4.     return user;  
    5. }  

    即调用该方法时,会把user.id作为key,返回值作为value放入缓存;

    @CachePut注解:

    Java代码  收藏代码

    1. public @interface CachePut {  
    2.     String[] value();              //缓存的名字,可以把数据写到多个缓存  
    3.     String key() default "";       //缓存key,如果不指定将使用默认的KeyGenerator生成,后边介绍  
    4.     String condition() default ""; //满足缓存条件的数据才会放入缓存,condition在调用方法之前和之后都会判断  
    5.     String unless() default "";    //用于否决缓存更新的,不像condition,该表达只在方法执行之后判断,此时可以拿到返回值result进行判断了  
    6. }  

    @CacheEvict 

    即应用到移除数据的方法上,如删除方法,调用方法时会从缓存中移除相应的数据:

    Java代码  收藏代码

    1. @CacheEvict(value = "user", key = "#user.id") //移除指定key的数据  
    2. public User delete(User user) {  
    3.     users.remove(user);  
    4.     return user;  
    5. }  
    6. @CacheEvict(value = "user", allEntries = true) //移除所有数据  
    7. public void deleteAll() {  
    8.     users.clear();  
    9. }  

    @CacheEvict注解:

    Java代码  收藏代码

    1. public @interface CacheEvict {  
    2.     String[] value();                        //请参考@CachePut  
    3.     String key() default "";                 //请参考@CachePut  
    4.     String condition() default "";           //请参考@CachePut  
    5.     boolean allEntries() default false;      //是否移除所有数据  
    6.     boolean beforeInvocation() default false;//是调用方法之前移除/还是调用之后移除  

    @Cacheable

    应用到读取数据的方法上,即可缓存的方法,如查找方法:先从缓存中读取,如果没有再调用方法获取数据,然后把数据添加到缓存中:

    Java代码  收藏代码

    1. @Cacheable(value = "user", key = "#id")  
    2.  public User findById(final Long id) {  
    3.      System.out.println("cache miss, invoke find by id, id:" + id);  
    4.      for (User user : users) {  
    5.          if (user.getId().equals(id)) {  
    6.              return user;  
    7.          }  
    8.      }  
    9.      return null;  
    10.  }  

    @Cacheable注解:

    Java代码  收藏代码

    1. public @interface Cacheable {  
    2.     String[] value();             //请参考@CachePut  
    3.     String key() default "";      //请参考@CachePut  
    4.     String condition() default "";//请参考@CachePut  
    5.     String unless() default "";   //请参考@CachePut    

     

    运行流程

    Java代码  收藏代码

    1. 1、首先执行@CacheEvict(如果beforeInvocation=true且condition 通过),如果allEntries=true,则清空所有  
    2. 2、接着收集@Cacheable(如果condition 通过,且key对应的数据不在缓存),放入cachePutRequests(也就是说如果cachePutRequests为空,则数据在缓存中)  
    3. 3、如果cachePutRequests为空且没有@CachePut操作,那么将查找@Cacheable的缓存,否则result=缓存数据(也就是说只要当没有cache put请求时才会查找缓存)  
    4. 4、如果没有找到缓存,那么调用实际的API,把结果放入result  
    5. 5、如果有@CachePut操作(如果condition 通过),那么放入cachePutRequests  
    6. 6、执行cachePutRequests,将数据写入缓存(unless为空或者unless解析结果为false);  
    7. 7、执行@CacheEvict(如果beforeInvocation=false 且 condition 通过),如果allEntries=true,则清空所有  

    流程中需要注意的就是2/3/4步:

    如果有@CachePut操作,即使有@Cacheable也不会从缓存中读取;问题很明显,如果要混合多个注解使用,不能组合使用@CachePut和@Cacheable;官方说应该避免这样使用(解释是如果带条件的注解相互排除的场景);不过个人感觉还是不要考虑这个好,让用户来决定如何使用,否则一会介绍的场景不能满足。

    提供的SpEL上下文数据

    Spring Cache提供了一些供我们使用的SpEL上下文数据,下表直接摘自Spring官方文档:

    名字 位置 描述 示例

    methodName

    root对象

    当前被调用的方法名

    #root.methodName

    method

    root对象

    当前被调用的方法

    #root.method.name

    target

    root对象

    当前被调用的目标对象

    #root.target

    targetClass

    root对象

    当前被调用的目标对象类

    #root.targetClass

    args

    root对象

    当前被调用的方法的参数列表

    #root.args[0]

    caches

    root对象

    当前方法调用使用的缓存列表(如@Cacheable(value={"cache1", "cache2"})),则有两个cache

    #root.caches[0].name

    argument name

    执行上下文

    当前被调用的方法的参数,如findById(Long id),我们可以通过#id拿到参数

    #user.id

    result

    执行上下文

    方法执行后的返回值(仅当方法执行之后的判断有效,如‘unless’,'cache evict'的beforeInvocation=false)

    #result

    通过这些数据我们可能实现比较复杂的缓存逻辑了,后边再来介绍。

    Key生成器

    如果在Cache注解上没有指定key的话@CachePut(value = "user"),会使用KeyGenerator进行生成一个key: 

    Java代码  收藏代码

    1. public interface KeyGenerator {  
    2.     Object generate(Object target, Method method, Object... params);  
    3. }  

    默认提供了DefaultKeyGenerator生成器(Spring 4之后使用SimpleKeyGenerator): 

    Java代码  收藏代码

    1. @Override  
    2. public Object generate(Object target, Method method, Object... params) {  
    3.     if (params.length == 0) {  
    4.         return SimpleKey.EMPTY;  
    5.     }  
    6.     if (params.length == 1 && params[0] != null) {  
    7.         return params[0];  
    8.     }  
    9.     return new SimpleKey(params);  
    10. }  

    即如果只有一个参数,就使用参数作为key,否则使用SimpleKey作为key。

    我们也可以自定义自己的key生成器,然后通过xml风格的<cache:annotation-driven key-generator=""/>或注解风格的CachingConfigurer中指定keyGenerator。 

     

    条件缓存

    根据运行流程,如下@Cacheable将在执行方法之前( #result还拿不到返回值)判断condition,如果返回true,则查缓存; 

    Java代码  收藏代码

    1. @Cacheable(value = "user", key = "#id", condition = "#id lt 10")  
    2. public User conditionFindById(final Long id)  

    根据运行流程,如下@CachePut将在执行完方法后(#result就能拿到返回值了)判断condition,如果返回true,则放入缓存; 

    Java代码  收藏代码

    1. @CachePut(value = "user", key = "#id", condition = "#result.username ne 'zhang'")  
    2. public User conditionSave(final User user)   

      

    根据运行流程,如下@CachePut将在执行完方法后(#result就能拿到返回值了)判断unless,如果返回false,则放入缓存;(即跟condition相反)

    Java代码  收藏代码

    1. @CachePut(value = "user", key = "#user.id", unless = "#result.username eq 'zhang'")  
    2.     public User conditionSave2(final User user)   

    根据运行流程,如下@CacheEvict, beforeInvocation=false表示在方法执行之后调用(#result能拿到返回值了);且判断condition,如果返回true,则移除缓存;

    Java代码  收藏代码

    1. @CacheEvict(value = "user", key = "#user.id", beforeInvocation = false, condition = "#result.username ne 'zhang'")  
    2. public User conditionDelete(final User user)   

    @Caching

    有时候我们可能组合多个Cache注解使用;比如用户新增成功后,我们要添加id-->user;username--->user;email--->user的缓存;此时就需要@Caching组合多个注解标签了。

    如用户新增成功后,添加id-->user;username--->user;email--->user到缓存; 

    Java代码  收藏代码

    1. @Caching(  
    2.         put = {  
    3.                 @CachePut(value = "user", key = "#user.id"),  
    4.                 @CachePut(value = "user", key = "#user.username"),  
    5.                 @CachePut(value = "user", key = "#user.email")  
    6.         }  
    7. )  
    8. public User save(User user) {  

      

    @Caching定义如下: 

    Java代码  收藏代码

    1. public @interface Caching {  
    2.     Cacheable[] cacheable() default {}; //声明多个@Cacheable  
    3.     CachePut[] put() default {};        //声明多个@CachePut  
    4.     CacheEvict[] evict() default {};    //声明多个@CacheEvict  
    5. }  

    自定义缓存注解

    比如之前的那个@Caching组合,会让方法上的注解显得整个代码比较乱,此时可以使用自定义注解把这些注解组合到一个注解中,如: 

    Java代码  收藏代码

    1. @Caching(  
    2.         put = {  
    3.                 @CachePut(value = "user", key = "#user.id"),  
    4.                 @CachePut(value = "user", key = "#user.username"),  
    5.                 @CachePut(value = "user", key = "#user.email")  
    6.         }  
    7. )  
    8. @Target({ElementType.METHOD, ElementType.TYPE})  
    9. @Retention(RetentionPolicy.RUNTIME)  
    10. @Inherited  
    11. public @interface UserSaveCache {  
    12. }  

    这样我们在方法上使用如下代码即可,整个代码显得比较干净。 

    Java代码  收藏代码

    1. @UserSaveCache  
    2. public User save(User user)  

      

    示例

    新增/修改数据时往缓存中写 

    Java代码  收藏代码

    1. @Caching(  
    2.         put = {  
    3.                 @CachePut(value = "user", key = "#user.id"),  
    4.                 @CachePut(value = "user", key = "#user.username"),  
    5.                 @CachePut(value = "user", key = "#user.email")  
    6.         }  
    7. )  
    8. public User save(User user)  

    Java代码  收藏代码

    1. @Caching(  
    2.         put = {  
    3.                 @CachePut(value = "user", key = "#user.id"),  
    4.                 @CachePut(value = "user", key = "#user.username"),  
    5.                 @CachePut(value = "user", key = "#user.email")  
    6.         }  
    7. )  
    8. public User update(User user)   

    删除数据时从缓存中移除

    Java代码  收藏代码

    1. @Caching(  
    2.         evict = {  
    3.                 @CacheEvict(value = "user", key = "#user.id"),  
    4.                 @CacheEvict(value = "user", key = "#user.username"),  
    5.                 @CacheEvict(value = "user", key = "#user.email")  
    6.         }  
    7. )  
    8. public User delete(User user)   

    Java代码  收藏代码

    1. @CacheEvict(value = "user", allEntries = true)  
    2.  public void deleteAll()  

    查找时从缓存中读

    Java代码  收藏代码

    1. @Caching(  
    2.         cacheable = {  
    3.                 @Cacheable(value = "user", key = "#id")  
    4.         }  
    5. )  
    6. public User findById(final Long id)   

    Java代码  收藏代码

    1. @Caching(  
    2.          cacheable = {  
    3.                  @Cacheable(value = "user", key = "#username")  
    4.          }  
    5.  )  
    6.  public User findByUsername(final String username)   

    Java代码  收藏代码

    1. @Caching(  
    2.           cacheable = {  
    3.                   @Cacheable(value = "user", key = "#email")  
    4.           }  
    5.   )  
    6.   public User findByEmail(final String email)  

      

    问题及解决方案

    一、比如findByUsername时,不应该只放username-->user,应该连同id--->user和email--->user一起放入;这样下次如果按照id查找直接从缓存中就命中了;这需要根据之前的运行流程改造CacheAspectSupport: 

    Java代码  收藏代码

    1. // We only attempt to get a cached result if there are no put requests  
    2. if (cachePutRequests.isEmpty() && contexts.get(CachePutOperation.class).isEmpty()) {  
    3.     result = findCachedResult(contexts.get(CacheableOperation.class));  
    4. }  

    改为:

    Java代码  收藏代码

    1. Collection<CacheOperationContext> cacheOperationContexts = contexts.get(CacheableOperation.class);  
    2. if (!cacheOperationContexts.isEmpty()) {  
    3.     result = findCachedResult(cacheOperationContexts);  
    4. }  

    然后就可以通过如下代码完成想要的功能: 

    Java代码  收藏代码

    1. @Caching(  
    2.         cacheable = {  
    3.                 @Cacheable(value = "user", key = "#username")  
    4.         },  
    5.         put = {  
    6.                 @CachePut(value = "user", key = "#result.id", condition = "#result != null"),  
    7.                 @CachePut(value = "user", key = "#result.email", condition = "#result != null")  
    8.         }  
    9. )  
    10. public User findByUsername(final String username) {  
    11.     System.out.println("cache miss, invoke find by username, username:" + username);  
    12.     for (User user : users) {  
    13.         if (user.getUsername().equals(username)) {  
    14.             return user;  
    15.         }  
    16.     }  
    17.     return null;  
    18. }  

      

    二、缓存注解会让代码看上去比较乱;应该使用自定义注解把缓存注解提取出去;

    三、往缓存放数据/移除数据是有条件的,而且条件可能很复杂,考虑使用SpEL表达式:

    Java代码  收藏代码

    1. @CacheEvict(value = "user", key = "#user.id", condition = "#root.target.canCache() and #root.caches[0].get(#user.id).get().username ne #user.username", beforeInvocation = true)  
    2. public void conditionUpdate(User user)  

     或更复杂的直接调用目标对象的方法进行操作(如只有修改了某个数据才从缓存中清除,比如菜单数据的缓存,只有修改了关键数据时才清空菜单对应的权限数据) 

    Java代码  收藏代码

    1. @Caching(  
    2.         evict = {  
    3.                 @CacheEvict(value = "user", key = "#user.id", condition = "#root.target.canEvict(#root.caches[0], #user.id, #user.username)", beforeInvocation = true)  
    4.         }  
    5. )  
    6. public void conditionUpdate(User user)   

    Java代码  收藏代码

    1. public boolean canEvict(Cache userCache, Long id, String username) {  
    2.     User cacheUser = userCache.get(id, User.class);  
    3.     if (cacheUser == null) {  
    4.         return false;  
    5.     }  
    6.     return !cacheUser.getUsername().equals(username);  
    7. }  

    如上方式唯一不太好的就是缓存条件判断方法也需要暴露出去;而且缓存代码和业务代码混合在一起,不优雅;因此把canEvict方法移到一个Helper静态类中就可以解决这个问题了:

    Java代码  收藏代码

    1. @CacheEvict(value = "user", key = "#user.id", condition = "T(com.sishuok.spring.service.UserCacheHelper).canEvict(#root.caches[0], #user.id, #user.username)", beforeInvocation = true)  
    2. public void conditionUpdate(User user)  

    四、其实对于:id--->user;username---->user;email--->user;更好的方式可能是:id--->user;username--->id;email--->id;保证user只存一份;如:

    Java代码  收藏代码

    1. @CachePut(value="cacheName", key="#user.username", cacheValue="#user.username")  
    2. public void save(User user)   

    Java代码  收藏代码

    1. @Cacheable(value="cacheName", ley="#user.username", cacheValue="#caches[0].get(#caches[0].get(#username).get())")  
    2. public User findByUsername(String username)  

    五、使用Spring3.1注解 缓存 模糊匹配Evict的问题 

    缓存都是key-value风格的,模糊匹配本来就不应该是Cache要做的;而是通过自己的缓存代码实现;

    六、spring cache的缺陷:例如有一个缓存存放 list<User>,现在你执行了一个 update(user)的方法,你一定不希望清除整个缓存而想替换掉update的元素

    这个在现有的抽象上没有很好的方案,可以考虑通过condition在之前的Helper方法中解决;当然不是很优雅。

    也就是说Spring Cache注解还不是很完美,我认为可以这样设计:

    @Cacheable(cacheName = "缓存名称",key="缓存key/SpEL", value="缓存值/SpEL/不填默认返回值",  beforeCondition="方法执行之前的条件/SpEL", afterCondition="方法执行后的条件/SpEL", afterCache="缓存之后执行的逻辑/SpEL")

    value也是一个SpEL,这样可以定制要缓存的数据;afterCache定制自己的缓存成功后的其他逻辑。

    当然Spring Cache注解对于大多数场景够用了,如果场景复杂还是考虑使用AOP吧;如果自己实现请考虑使用Spring Cache API进行缓存抽象。

    示例请参考我的github:

    https://github.com/zhangkaitao/spring4-showcase/tree/master/spring-cache

    转自:https://blog.csdn.net/sinat_21184471/article/details/76924260

    正因为当初对未来做了太多的憧憬,所以对现在的自己尤其失望。生命中曾经有过的所有灿烂,终究都需要用寂寞来偿还。
  • 相关阅读:
    ASP.NET 2.0服务器控件开发之基本概念篇
    ASP.NET 2.0的URL映射的实现方法
    Asp.net中防止用户多次登录的方法
    SSRS:使用SQL2008教程学习Reporting Services之数据库AdventureWorks2008问题_学习笔记1
    ASP.NET服务器控件编程之热身运动
    ASP.NET2.0服务器控件开发之实现事件
    datalist 或者repeater分页
    .Net 上传图片加水印
    datalist 全选
    深入理解JavaScript中的函数
  • 原文地址:https://www.cnblogs.com/candlia/p/11920099.html
Copyright © 2011-2022 走看看