SpringCache是SpringFramework3.1引入的新特性,提供了基于注解的缓存配置方法。它本质上不是一个具体的缓存实现方案(例如EHCache),而是一个对缓存使用的抽象,通过在已有代码中打上几个预定义的注释,就可以实现我们希望达到的缓存效果。SpringCache支持跟第三方缓存例如EHCache集成;另外也提供了开箱即用的默认实现,可以直接拿来使用。
SpringCache支持使用SpEL(Spring Expression Language)来定义缓存的key和各种condition,因此具备相当的灵活性,并可以支持非常复杂的语义。
下面先给出一个使用案例,然后通过源码分析其实现原理及核心业务逻辑。
第一部分:使用样例
1 一个简单例子
1.1 配置CacheManager与Cache并启动
由于是springframework的内置功能,使用springcache并不需要额外引入jar包。因此只需要简单的配置就可以启用开箱即用的默认缓存实现。
创建Configuration类,在其中配置CacheManager Bean,并为其创建两个cache(注意cache的名称,在下面需要缓存的方法上打注释配置时需要指定)。
@Configuration
@EnableCaching(proxyTargetClass = true)
public class Configuration{
@Bean(name="simpleCacheManager")
public CacheManager simpleCacheManager(){
SimpleCacheManager cacheManager = new SimpleCacheManager();
List<Cache> caches = new ArrayList<Cache>();
ConcurrentMapCache cache1 = new ConcurrentMapCache("mycache");
ConcurrentMapCache cache2 = new ConcurrentMapCache("mycache2");
caches.add(cache1);
caches.add(cache2);
cacheManager.setCaches(caches);
return cacheManager;
}
}
Configuration类上的@EnableCaching(proxyTargetClass = true)注释就表示启动SpringCache功能。其中proxyTargetClass=true表示:当需要代理的类是一个接口或者是一个动态生成的代理类时使用JdkProxy代理;而当要代理的类是一个具体类时,使用cglib来代理。假如不设置该属性,则默认使用JdkProxy代理,而JdkProxy能够代理的类必须实现接口,因此如果想要一个没实现接口的类被代理,就必须设置proxyTargetClass = true来使用cglib完成代理。
另外@EnableCaching还有一个属性AdviceMode mode,取值有两个AdviceMode.PROXY和AdviceMode.ASPECTJ,意思是Spring AOP使用代理模式实现还是使用原生AspectJ模式实现,默认是代理模式。在此我们只介绍代理模式。
SimpleCacheManager与ConcurrentMapCache都是SpringCache提供的默认实现。而当我们使用SpringBoot时,由于其spring-boot-autoconfigure模块里对SpringCache做了默认的自动配置,因此我们甚至连CacheManager都不需要配置。仅仅在Configuration类上打上@EnableCaching(proxyTargetClass = true)注释便可以启动springcache了(具体源码分析将在后面章节)。
1.2 在需要缓存的方法上添加对应的注释
光是启用SpringCache并没有用,我们还需要指明在哪些类的哪些方法上需要缓存,以及需要什么样的缓存行为。
SpringCache提供了@Cacheable、@CachePut、@CacheEvict等注释,并支持使用SpEL(Spring Expression Language)来定义缓存的key和各种condition,因此具备相当的灵活性,并可以支持非常复杂的语义。关于SpringCache使用语法的详细说明请参照《Spring Cache抽象详解》。
接下来我们创建一个Service,并在其方法上打上SpringCache标签以定义其缓存行为:
@Service
public class UserService {
@CacheEvict(value={"mycache", "mycache2"}, allEntries = true)
public void clearCache(){
}
@CachePut(value = "mycache", key = "#user.id")
public User save(User user) {
return user;
}
@CacheEvict(value = "mycache", key = "#user.id") //移除指定key的数据
public User delete(User user) {
return user;
}
@Cacheable(value = "mycache", key = "#id")
public User findById(final Long id) {
System.out.println("cache miss, invoke find by id, id:" + id);
Random random = new Random();
User user = new User(id,
"wangd_"+random.nextInt(10000)+"_"+System.currentTimeMillis(),
"wd@123.com"+random.nextInt(10000)+"_"+System.currentTimeMillis());
return user;
}
@Cacheable(value="mycache2", key = "#username.concat(#email)", condition = "#username eq 'wangd'")
public User findByUsernameAndEmail(String username, String email){
Random random = new Random();
System.out.println("cache2 miss, invoke find by name and email, name:" + username +
", email:"+email);
User user = new User(System.currentTimeMillis()+random.nextInt(10000),
username,
email);
return user;
}
@Cacheable(value = "mycache2", key = "#username")
public User findByUsername(String username){
Random random = new Random();
System.out.println("cache miss, invoke find by name, name:" + username);
User user = new User(System.currentTimeMillis()+random.nextInt(10000),
username,
"mytestemail@123.com_"+System.currentTimeMillis());
return user;
}
}
至此,SpringCache已经可以使用了,下面我们编写测试代码来验证。
1.3 测试代码与执行结果
使用springboottest可以方便的对容器内的服务进行测试,首先在pom.xml中加入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
然后编写测试代码:
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = Configuration.class)
public class SpringCacheTest {
Logger logger = LoggerFactory.getLogger(SpringCacheTest.class);
@Autowired
UserService userService;
@Test
public void test() throws IOException {
logger.info("invoke userService.clearCache()");
userService.clearCache();
logger.info("invoke userService.findById(1L)");
User user = userService.findById(1L);
//从缓存读数据
Assert.assertNotNull(user);
logger.info("invoke userService.findById(1L)");
User user2 = userService.findById(1L);
Assert.assertEquals(user.getEmail(), user2.getEmail());
User uu = new User(user.getId(), user.getName(), user.getEmail());
uu.setEmail("new_email_addr");
logger.info("invoke userService.save(uu)");
userService.save(uu);
logger.info("invoke userService.findById(1L)");
User user3 = userService.findById(1L);
Assert.assertEquals(uu.getEmail(), user3.getEmail());
Assert.assertNotEquals(user2.getEmail(), user3.getEmail());
}
@Test
public void test2() throws Exception{
logger.info("invoke userService.clearCache()");
userService.clearCache();
logger.info("invoke userService.findByUsernameAndEmail(\"wangd\", \"wd@123.com\")");
User user = userService.findByUsernameAndEmail("wangd", "wd@123.com");
logger.info("invoke userService.findByUsernameAndEmail(\"wangd\", \"wd@123.com\")");
User user2 = userService.findByUsernameAndEmail("wangd", "wd@123.com");
Assert.assertEquals(user.getId(), user2.getId());
logger.info("invoke userService.findByUsernameAndEmail(\"lm\", \"lm@123.com\")");
User user3 = userService.findByUsernameAndEmail("lm", "lm@123.com");
logger.info("invoke userService.findByUsernameAndEmail(\"lm\", \"lm@123.com\")");
User user4 = userService.findByUsernameAndEmail("lm", "lm@123.com");
Assert.assertNotEquals(user3.getId(), user4.getId());
}
@Test
public void test3() throws Exception{
logger.info("invoke userService.clearCache()");
userService.clearCache();
logger.info("invoke userService.findByUsername(\"wangd123\")");
User user = userService.findByUsername("wangd123");
logger.info("invoke userService.findByUsername(\"wangd123\")");
User user2 = userService.findByUsername(user.getName());
logger.info("invoke userService.clearCache()");
userService.clearCache();
logger.info("invoke userService.findByUsername(\"wangd123\")");
User user3 = userService.findByUsername(user.getName());
Assert.assertEquals(user.getId(), user2.getId());
Assert.assertNotEquals(user.getId(), user3.getId());
}
}
Run As Junit Test运行测试程序,测试通过并输出结果如下:
springcache.test.SpringCacheTest : invoke userService.clearCache()
springcache.test.SpringCacheTest : invoke userService.findById(1L)
springcache.service.UserService : cache miss, invoke find by id, id:1
springcache.test.SpringCacheTest : invoke userService.findById(1L)
springcache.test.SpringCacheTest : invoke userService.save(uu)
springcache.test.SpringCacheTest : invoke userService.findById(1L)
springcache.test.SpringCacheTest : invoke userService.clearCache()
springcache.test.SpringCacheTest : invoke userService.findByUsernameAndEmail("wangd", "wd@123.com")
springcache.service.UserService : cache2 miss, invoke find by name and email, name:wangd, email:wd@123.com
springcache.test.SpringCacheTest : invoke userService.findByUsernameAndEmail("wangd", "wd@123.com")
springcache.test.SpringCacheTest : invoke userService.findByUsernameAndEmail("lm", "lm@123.com")
springcache.service.UserService : cache2 miss, invoke find by name and email, name:lm, email:lm@123.com
springcache.test.SpringCacheTest : invoke userService.findByUsernameAndEmail("lm", "lm@123.com")
springcache.service.UserService : cache2 miss, invoke find by name and email, name:lm, email:lm@123.com
springcache.test.SpringCacheTest : invoke userService.clearCache()
springcache.test.SpringCacheTest : invoke userService.findByUsername("wangd123")
springcache.service.UserService : cache miss, invoke find by name, name:wangd123
springcache.test.SpringCacheTest : invoke userService.findByUsername("wangd123")
springcache.test.SpringCacheTest : invoke userService.clearCache()
springcache.test.SpringCacheTest : invoke userService.findByUsername("wangd123")
springcache.service.UserService : cache miss, invoke find by name, name:wangd123
根据打印的日志,对cache使用的效果一目了然,当cache没有命中时会打出一句cacheX miss, invoke find by.......这样的日志。findById、findByUsernameAndEmail、和findByUsername等方法在使用相同参数调用时,都只会在第一次cache没命中时实际执行方法,后面的调用都是直接使用缓存了。除了findByUsernameAndEmail("lm", "lm@123.com"),因为我们对其方法的注释中是这样定义的:
@Cacheable(value="mycache2", key = "#username.concat(#email)", condition = "#username eq 'wangd'")
public User findByUsernameAndEmail(String username, String email)
意思是,只有当username的值是'wangd'时才会将结果缓存。
2 与ehcache集成
上面我们使用的是SpringCache自带的开箱即用的实现,内部使用了ConcurrentHashMap来实现。在生产环境中我们可能需要更给力的缓存实现方案,接下来我们将集成EHCache。
当使用EHCache时我们需要引入ehcache-core的类库并需要spring-context-support类库的支持,在pom.xml中添加如下依赖:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.6.6</version>
</dependency>
然后在Configuration中使用EhCacheCacheManager来定义CacheManager Bean:
@Bean
public CacheManager cacheManager() {
try {
net.sf.ehcache.CacheManager ehcacheCacheManager
= new net.sf.ehcache.CacheManager(new ClassPathResource("ehcache.xml").getInputStream());
EhCacheCacheManager cacheCacheManager = new EhCacheCacheManager(ehcacheCacheManager);
return cacheCacheManager;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
其中使用到了ehcache.xml文件来对ehcache做一些配置,在src/main/resources下面创建ehcache.xml,内容如下:
<ehcache updateCheck="false">
<diskStore path="/home/wangd/data/ehcache2" />
<defaultCache maxElementsInMemory="10000" eternal="false"
timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="true" />
<cache name="mycache"
maxEntriesLocalHeap="10000"
overflowToDisk="false"
eternal="false"
diskPersistent="false"
timeToLiveSeconds="0"
timeToIdleSeconds="0"
statistics="true"/>
<cache name="mycache2"
maxEntriesLocalHeap="10000"
overflowToDisk="false"
eternal="false"
diskPersistent="false"
timeToLiveSeconds="0"
timeToIdleSeconds="0"
statistics="true"/>
</ehcache>
其中指定了持久化到磁盘上的位置"/home/wangd/data/ehcache2",并定义了两个Cache分别命名为mycache和mycache2,与在UserService各方法中指定的cache名相对应。
再次Run As Junit Test运行测试程序,发现测试结果与上面是一致的。
3 自己定制CacheManager和Cache
SpringCache本质上是一个对缓存使用的抽象,将存储的具体实现方案从缓存执行动作及流程中提取出来。缓存流程中面向的两个抽象接口是CacheManager、Cache。其中Cache提供了缓存操作的读取/写入/移除等方法,本着面向抽象编程的原则,内部将缓存对象统一封装成ValueWrapper。Cache接口代码如下:
public interface Cache {
String getName(); //缓存的名字
Object getNativeCache(); //得到底层使用的缓存,如Ehcache
ValueWrapper get(Object key); //根据key得到一个ValueWrapper,然后调用其get方法获取值
<T> T get(Object key, Class<T> type);//根据key,和value的类型直接获取value
void put(Object key, Object value);//往缓存放数据
void evict(Object key);//从缓存中移除key对应的缓存
void clear(); //清空缓存
interface ValueWrapper { //缓存值的Wrapper
Object get(); //得到真实的value
}
}
由于在应用中可能定义多个Cache,因此提供了CacheManager抽象,用于缓存的管理,接口代码如下:
import java.util.Collection;
public interface CacheManager {
Cache getCache(String name); //根据Cache名字获取Cache
Collection<String> getCacheNames(); //得到所有Cache的名字
}
任何实现了这两个接口的缓存方案都可以直接配置进SpringCache使用。其自带的SimpleCacheManager、ConcurrentMapCache是如此;使用ehcache作为存储实现的EhCacheCacheManager、EhCacheCache也是如此。我们可以自己实现CacheManager与Cache并将其集成进来。
为了方便展示,我们自定义缓存实现方案只实现最简单的功能,cache内部使用ConcurrentHashMap做为存储方案,使用默认实现SimpleValueWrapper,MyCache代码如下:
public class MyCache implements Cache {
final static Logger logger = LoggerFactory.getLogger(MyCache.class);
String name;
Map<Object, Object> store = new ConcurrentHashMap<Object, Object>();
public MyCache() {
}
public MyCache(String name) {
this.name = name;
}
@Override
public String getName() {
return this.name;
}
public void setName(String name){
this.name = name;
}
@Override
public Object getNativeCache() {
return store;
}
@Override
public ValueWrapper get(Object key) {
ValueWrapper result = null;
Object thevalue = store.get(key);
if(thevalue!=null) {
logger.info("["+name+"]got cache, key:"+key);
result = new SimpleValueWrapper(thevalue);
}else{
logger.info("["+name+"]missing cache, key:"+key);
}
return result;
}
@SuppressWarnings("unchecked")
@Override
public <T> T get(Object key, Class<T> type) {
ValueWrapper vw = get(key);
if(vw==null){
return null;
}
return (T)vw.get();
}
@SuppressWarnings("unchecked")
@Override
public <T> T get(Object key, Callable<T> valueLoader) {
ValueWrapper vw = get(key);
if(vw==null){
return null;
}
return (T)vw.get();
}
@Override
public void put(Object key, Object value) {
store.put(key, value);
}
@Override
public ValueWrapper putIfAbsent(Object key, Object value) {
Object existing = this.store.putIfAbsent(key, value);
return (existing != null ? new SimpleValueWrapper(existing) : null);
}
@Override
public void evict(Object key) {
store.remove(key);
}
@Override
public void clear() {
store.clear();
}
}
然后是MyCacheManager:
public class MyCacheManager extends AbstractCacheManager {
private Collection<? extends MyCache> caches;
/**
* Specify the collection of Cache instances to use for this CacheManager.
*/
public void setCaches(Collection<? extends MyCache> caches) {
this.caches = caches;
}
@Override
protected Collection<? extends MyCache> loadCaches() {
return this.caches;
}
}
实现完毕,接下来在Configuration中配置我们自己定制的Cache实现方案:
@Bean(name="myCacheManager")
public CacheManager myCacheManager(){
MyCacheManager myCacheManager = new MyCacheManager();
List<MyCache> caches = new ArrayList<MyCache>();
MyCache mycache = new MyCache("mycache");
MyCache mycache2 = new MyCache("mycache2");
caches.add(mycache);
caches.add(mycache2);
myCacheManager.setCaches(caches);
return myCacheManager;
}
完成以上步骤后再次Run As Junit Test运行测试程序,发现测试结果与上面两次都是一致的。
小结: 至此,对SpringCache的配置使用做了一个介绍。其中,我们首先使用了SpringCache自带的开箱即用的存储实现方案、然后集成了EHCache的存储实现方案、最后定制并集成了自己的存储实现方案。由此可见SpringCache本质上是一个对缓存使用的抽象。它并不会要求你使用什么具体的存储实现方案,而是提供了非常便利的方式允许各种存储方案轻松集成进来。下面的章节我们将分析SpringCache的源码以了解其实现原理及核心处理逻辑。