Spring3.1新增了一种全新的缓存机制,这种缓存机制与Spring容器无缝地整合在一起,可以对容器中的任意Bean或Bean的方法增加缓存。Spring的缓存机制非常灵活,它可以对容器中的任意Bean或Bean的任意方法进行缓存,因此这种缓存机制可以在JavaEE应用的任何层次上进行缓存。

与Hibernate SessionFactory级别的二级缓存相比,Spring缓存的级别更高,Spring缓存可以在控制器组件或业务逻辑组件级别进行缓存,这样应用完全无须重复调用底层的DAO组件的方法。

Spring缓存同样不是一种具体的缓存实现方案,它底层同样需要依赖EhCache、Guava等具体的缓存工具。但这也正是Spring缓存机制的优势,应用程序只要面向Spring缓存API编程,应用底层的缓存实现可以在不同的缓存之间自由切换,应用程序无须任何改变,只要对配置文件略作修改即可。

1,启用Spring缓存

Spring配置文件专门为缓存提供了一个cache:命名空间,为了启用Spring缓存,需要在配置文件中导入cache:命名空间。导入cache:命名空间与导入util:、context:命名空间的方式完全一样。

导入cache:命名空间后,启用Spring缓存还要两步:

  • 在Spring配置文件中添加<cache:annotation-driven cache-manager="缓存管理器 ID"/>,该元素指定Spring根据注解来启用Bean级别或方法级别的缓存。
  • 针对不同的缓存实现配置对应的缓存管理器(Spring内置缓存器和EhCache缓存)。

1.1,Spring内置缓存实现的配置

Spring内置缓存实现只是一种内存中的缓存,并非真正的缓存实现,因此通常只能用于简单的测试环境,不建议在实际项目中使用Spring内置的缓存实现。

Spring内置的缓存实现使用SimpleCacheManager作为缓存管理器,使用SimpleCacheManager配置缓存非常简单,直接在Spring容器中配置该Bean,然后通过<property.../>驱动该缓存管理器执行setCaches()方法来设置缓存区即可。

SimpleCacheManager是一种内存中的缓冲区,底层直接使用了JDK的ConcurrentMap来实现缓存,SimpleCacheManager使用了ConcurrentMapCacheMapCacheFactoryBean作为缓存区,每个ConcurrentMapCacheFactoryBean配置一个缓冲区。

<?xml version="1.0" encoding="GBK"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns="http://www.springframework.org/schema/beans"
       xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
     http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
    <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="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name = "users"/>
            </set>
        </property>
    </bean>
</beans>

上面配置文件使用了SimpleCacheManager配置了Spring内置的缓存器,并为该缓存管理器配置了两个缓冲区:default和users——这些缓存区的名字很重要,因为后面使用注解驱动缓存时需要根据缓存区的名称来将缓存数据放入指定缓存区内。在实际开发中,开发者可以根据自己的需要,配置更多的缓存区,一般来说应用有多少个组件需要缓存,就应该配置多少个缓存区。

1.2,EhCache缓存实现的配置

在配置EhCache缓存实现之前,首先需要将EhCache缓存的jar包添加到项目的加载路径中。包括:缓存工具的日志工具(slf4j-api-1.7.30.jar)、EhCache核心架包(ehcache-core-2.6.11.jar)

下载地址:https://mvnrepository.com/

为了使用EhCache,同样需要在应用的类加载路径下添加一个ehcache.xml配置文件。

<?xml version="1.0" encoding="GBK" ?>
<ehcache>
    <diskStore path="java.io.tmpdir"/>
    <defaultCache
            maxElementsInMemory="10000"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            maxElementsOnDisk="10000000"
            diskExpiryThreadIntervalSeconds="120"
            memoryStoreEvictionPolicy="LRU"/>
    <cache name="users"
           maxElementsInMemory="10000"
           eternal="false"
           overflowToDisk="true"
           timeToIdleSeconds="300"
           timeToLiveSeconds="600"/>
</ehcache>

配置文件配置了两个缓冲区,其中第一个是用于配置匿名的、默认的缓存区,第二个才是配置了名为users的缓存区。如果需要,可以完全多复制几个cache,用于配置多个缓冲区。

SpringEhCacheCacheManager作为EhCache缓存实现的缓存管理器,因此只要该对象配置在Spring容器中,它就可作为缓存管理器使用,但EhCacheCacheMananger底层需要依赖一个net.sf.ehcache.CacheManager作为实际的缓存管理器。

为了将net.sf.ehcache.CacheManager纳入Spring容器的管理之下,Spring提供了EhCacheMananger、FactoryBean工厂Bean,该工厂Bean实现了FactoryBean<CacheMananger>接口。当程序把EhCacheManagerFactoryBean部署在Spring容器中,并通过Spring容器请求获取该工厂Bean时,实际返回的是它的产品——CacheMananger对象。

因此,为了在Spring配置文件中配置基于EhCache的缓存管理器,只要增加如下代码即可:

<?xml version="1.0" encoding="GBK"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns="http://www.springframework.org/schema/beans"
       xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context"
       xmlns:cache="http://www.springframework.org/schema/cache"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
     http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd">
    <!--配置EhCache的CacheManager通过configLocation指定ehcache.xml文件位置-->
    <bean id="ehCacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"
          p:configLocation="classpath:ehcache.xml" p:shared="false"/>
    <!--配置基于EhCache的缓存管理器并将EhCache的CacheManager注入该缓存管理器Bean-->
    <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager"
          p:cacheManager-ref="ehCacheManager"/>
    <!--启动缓存-->
    <cache:annotation-driven/>
    <context:component-scan base-package="Service"></context:component-scan>
</beans>

上面配置文件中配置的第一个Bean是工厂Bean,它用于配置EhCache的CacheManager;第二个Bean才是为Spring缓存配置的基于EhCache的缓存管理器,该缓存管理器需要依赖于CacheManager,因此程序讲第一个Bean注入到第二个Bean中。

2,使用@Cacheable执行缓存

@Cacheable可用于修饰类或修饰方法,当使用@Cacheable修饰类时,用于告诉Spring在类级别上进行缓存——程序调用该类的实例的任何方法时都需要缓存,而且共享同一缓存区;当使用@Cacheable修饰方法时,用于告诉Spring在方法级别上进行缓存——只有当程序调用该方法时才需要缓存。

2.1,类级别缓存器

使用@Cacheable修饰类时,就可以控制Spring在类级别进行缓存,这样当程序调用该类的任意方法时,只要传入的参数相同,Spring就会使用缓存。

package Service;

import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service("userService")
@Cacheable(value = "users")
public class UserServiceImpl {
    public User getUser1(String name,int age){
        return new User(name,age);
    }
    public User getUser2(String name,int age){
        return new User(name,age);
    }
}

上面@Cacheable对UserServiceImpl进行类级别的缓存,这样程序调用该类的任意方法时,只要传入的参数相同,Spring就会使用缓存。这里的缓存是指:当程序第一次调用该类的实例的某个方法时,Spring缓存机制会将该方法返回的数据放入指定缓存区——就是@Cacheable注解的value属性值所指定的缓存区。以后程序调用该类的实例的任何方法时,只要传入的参数相同,Spring将不会真正执行该方法,而是直接利用缓存区中的数据。 

package Service;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserServiceImpl us = applicationContext.getBean("userService",UserServiceImpl.class);
        User user1 = us.getUser1("燕双嘤",30);
        User user2 = us.getUser2("燕双嘤",30);
        System.out.println(user1 == user2);
    }
}
========================================
true

程序调用了UserServiceImpl的两个不同方法,但由于程序传入的方法参数相同,因此Spring不会真正执行第二次调用的方法,而是直接复用缓存区中的数据。

从结果可以看出,类级别的缓存器默认以所有方法参数作为key来缓存方法返回的数据——同一个类不管调用那个方法,只要调用方法时传入的参数相同,Spring都会直接利用缓存区中的数据。

@Cacheable时可以指定如下属性:

  •  value:必须属性。该属性可制定多个缓存区名字,用于指定将方法返回值放入指定的缓存区内。
  • key:通过SpEL表达式显示指定缓存key。
  • condition:该属性指定一个返回boolean值的SpEL表达式,只有当该表达式返回true时,Spring才会缓存方法返回值。
  • unless:该属性指定一个返回boolean值的SpEL表达式,当该表达式返回true时,Spring就不缓存方法的返回值。
@Service("userService")
@Cacheable(value = "users",key = "#name")
public class UserServiceImpl {
    public User getUser1(String name,int age){
        return new User(name,age);
    }
    public User getUser2(String name,int age){
        return new User(name,age);
    }
}

显式指定以name参数作为缓存的key,这样只要调用的方法具有相同的name参数,Spring缓存机制就会生效。

ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
UserServiceImpl us = applicationContext.getBean("userService",UserServiceImpl.class);
User user1 = us.getUser1("燕双嘤",30);
User user2 = us.getUser2("燕双嘤",40);
System.out.println(user1 == user2);
=====================================
true

上面程序两次调用方法时传入的参数并不完全相同,只有name参数相同,但由于前面使用@Cacheable注解时显式指定了key="#name",这就意味着使用name参数作为缓存的key,因此两次调用方法实际上只执行第一次调用,第二次调用将直接使用缓存的数据,不会真正执行该方法。

condition属性unless属性的功能基本相似,但规则恰好相反:当condtion指定的条件为true时,Spring缓存机制才会执行缓存;当unless指定的条件为true时,Spring缓存机制就不执行缓存。

@Cacheable(value = "users",condition = "#age<100")

2.2,方法级别的缓存

使用@Cacheable修饰方法时,就可以控制Spring在方法级别进行缓存,这样当程序调用该方法时,只要传入的参数相同,Spring就会使用缓存。

@Service("userService")
public class UserServiceImpl {
    @Cacheable(value = "users1")
    public User getUser1(String name,int age){
        return new User(name,age);
    }
    @Cacheable(value = "users2")
    public User getUser2(String name,int age){
        return new User(name,age);
    }
}ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
UserServiceImpl us = applicationContext.getBean("userService",UserServiceImpl.class);
User user1 = us.getUser1("燕双嘤",30);
User user2 = us.getUser2("燕双嘤",30);
User user3 = us.getUser2("燕双嘤",30);
System.out.println(user1 == user2);
System.out.println(user3 == user2);
================================================
false
true

3,使用@CacheEvict清除缓存

@CacheEvict注解修饰的方法可用于清除缓存,使用@CacheEvict注解时可指定如下属性:

  • value:必须属性。用于指定该方法用于清除那个缓存区的数据。
  • allEntries:该属性指定是否清空整个缓存区。
  • beforeInvocation:该属性指定是否在执行方法之前清除缓存。默认是在方法完成之后才清除缓存。
  • condition:该属性指定一个SpEL表达式,只有当该表达式为true时才清除缓存。
  • key:通过SpEL表达式显示指定缓存的key。
@Service("userService")
public class UserServiceImpl {
    @Cacheable(value = "users1")
    public User getUser1(String name, int age) {
        return new User(name, age);
    }

    @Cacheable(value = "users2")
    public User getUser2(String name, int age) {
        return new User(name, age);
    }

    @CacheEvict(value = "users")
    public void evictUser(String name, int arg) {
        System.out.println("--正在清空" + name + "," + arg + "对应的缓存--");
    }

    @CacheEvict(value = "users", allEntries = true)
    public void evictAll() {
        System.out.println("--正在清空整个缓存--");
    }
}

上面程序中第一个@CacheEvict注解只是指定了value="users",这表明该注解用于清除users缓存区中的数据,程序将会根据传入的name、age参数清除对应的数据。第二个@CacheEvict注解则指定了allEntries=true,这表明该方法将会清空整个users缓存区。

public class Main {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserServiceImpl us = applicationContext.getBean("userService", UserServiceImpl.class);
        User user1 = us.getUser1("燕双嘤", 30);
        User user2 = us.getUser2("燕双嘤", 30);
        System.out.println(user1 == user2);
        us.evictUser("燕双嘤", 30);
        User user3 = us.getUser2("燕双嘤", 30);
        System.out.println(user2 == user3);
        User user4 = us.getUser2("燕双嘤", 30);
        us.evictAll();
        System.out.println(user1 == user4);
    }
}
==============================================
true
--正在清空燕双嘤,30对应的缓存--
false
--正在清空整个缓存--
false