一、引言

    利用缓存来提高应用程序的性能已经是非常常用的手段了,随处可见。

    起初我们通常自己再应用内部实现自定义的缓存功能,虽然订制成都比较高,不过开发量会大不少。后来,就使用一些譬如EhCache(EhCache 是一个纯Java的进程内缓存框架,具有快速、精干等特点,是Hibernate中默认的CacheProvider)的缓存框架。再到后来,就出现了缓存中间件,比如Redis(其实功能远不止用作缓存这么简单)。

    本文介绍的是Spring自带的缓存支持。

二、简介

    Spring缓存是一种轻量级的缓存使用的抽象,就像Spring Data Jpa一样,还是需要选择一款具体的实现框架。

    不过基于Spring的注解(从3.1开始可以基于注解实现缓存)等支持,让我们的开发变得更加得心应手。

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

    主要有以下特点:

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

    个人感觉Spring的东西都非常简单、易学且易用,本文会通过实例一步步介绍。

    本文基于Spring boot为基础编写代码,并使用Maven管理依赖,缓存实现框架选用EhCache,为了能够快速演示,数据库也使用H2内存数据库。当然代码也可以直接搬到其他Spring工程中运行。

    另外,本文例子使用了Spring Data JPA,简单并同时支持HQL和SQL以及命名查询等,并通过扩展JpaSpecificationExecutor接口实现了非常简单易用的动态查询(如有兴趣可参考:Spring Data JPA以及Spring Data JPA中的动态查询)。可以极大的减轻了我们的开发代码量。

    本文所涉及的所有代码都会在文末给出连接供下载。

三、自定义实现缓存

    3.1、实例

    这里以一个简单的数据查询为例。

    3.1.1、查询数据首先就需要定义一个数据类(实体):

package com.anxpp.demo.cache.custom.entity;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Data {
    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;
    private String name;
    public Data(){}
    public Data(String name){
        this.name = name;
    }
    public final Long getId() {
        return id;
    }
    public final void setId(Long id) {
        this.id = id;
    }
    public final String getName() {
        return name;
    }
    public final void setName(String name) {
        this.name = name;
    }
}

    3.1.2、其次是实现我们的缓存管理器:

package com.anxpp.demo.cache.custom.utils;
import java.util.HashMap;
import java.util.Map;
/**
 * 自定义缓存管理器
 * 实现增删改,Key Value均支持泛型
 * @author anxpp.com
 * 2017年1月3日 下午10:20:53
 */
public class CacheManager<K,V> {
    private Map<K, V> cache = new HashMap<>();
    public V get(K key){
        return cache.get(key);
    }
    public void merge(K key,V value){
        cache.put(key, value);
    }
    public void remove(K key){
        cache.remove(key);
    }
    public void clear(){
        cache.clear();
    }
}

    3.1.3、然后是数据的查询dao:

package com.anxpp.demo.cache.custom.repo;
import org.springframework.data.jpa.repository.JpaRepository;
import com.anxpp.demo.cache.custom.entity.Data;
public interface DataRepo extends JpaRepository<Data, Long> {}

    3.1.4、最后需要一个数据查询的服务:

    接口:

package com.anxpp.demo.cache.custom.service;
import com.anxpp.demo.cache.custom.entity.Data;
public interface DataService {
    void save(Data data);
    Data findById(Long id);
    void clearCache();
}

    3.1.5、实现:

package com.anxpp.demo.cache.custom.service.impl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.anxpp.demo.cache.custom.entity.Data;
import com.anxpp.demo.cache.custom.repo.DataRepo;
import com.anxpp.demo.cache.custom.service.DataService;
import com.anxpp.demo.cache.custom.utils.CacheManager;
import com.anxpp.demo.cache.utils.TinyUtil;
/**
 * 数据查询服务
 * @author anxpp.com
 * 2017年1月3日 下午10:30:25
 */
@Service
public class DataServiceImpl implements DataService {
    private static CacheManager<Long,Data> cache = new CacheManager<Long, Data>();
    private static Logger log = LoggerFactory.getLogger(DataServiceImpl.class);
    @Autowired
    DataRepo dataRepo;
    @Override
    public void save(Data data) {
        dataRepo.save(data);
    }
    @Override
    public Data findById(Long id) {
        Data data = cache.get(id);
        if(TinyUtil.isNotEmpty(data)){
            log.info("从缓存获取数据...");
            return data;
        }
        log.info("从数据库获取数据...");
        data = dataRepo.findOne(id);
        if(TinyUtil.isNotEmpty(data)){
            log.info("添加缓存...");
            cache.merge(id, data);
        }
        return data;
    }
    @Override
    public void clearCache() {
        cache.clear();
    }
}

    3.1.6、编写单元测试:

package com.anxpp.demo.cache.custom;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import com.anxpp.demo.cache.custom.entity.Data;
import com.anxpp.demo.cache.custom.service.DataService;
/**
 * 自定义缓存单元测试
 * @author anxpp.com
 * 2017年1月3日 下午10:59:52
 */
@RunWith(SpringRunner.class)
@SpringBootTest
public class CustomCacheApplicationTests {
    private static Logger log = LoggerFactory.getLogger(CustomCacheApplicationTests.class);
    @Autowired
    DataService dataService;
    /**
     * 因为测试比较简单,就不添加断言而是直接查看log输出了
     */
    @Test
    public void simpleTest(){
        Data data = new Data("anxpp.com");
        dataService.save(data);
        dataService.findById(data.getId());
        dataService.findById(data.getId());
        log.info("清理缓存");
        dataService.clearCache();
        dataService.findById(data.getId());
        dataService.findById(data.getId());
    }
}

    控制台可以看到如下输出:

--- [main] c.a.d.c.c.CustomCacheApplicationTests    : Started CustomCacheApplicationTests in 2.366 seconds (JVM running for 2.944)
--- [main] c.a.d.c.c.service.impl.DataServiceImpl   : 从数据库获取数据...
--- [main] c.a.d.c.c.service.impl.DataServiceImpl   : 添加缓存...
--- [main] c.a.d.c.c.service.impl.DataServiceImpl   : 从缓存获取数据...
--- [main] c.a.d.c.c.service.impl.DataServiceImpl   : 从数据库获取数据...
--- [main] c.a.d.c.c.service.impl.DataServiceImpl   : 添加缓存...
--- [main] c.a.d.c.c.service.impl.DataServiceImpl   : 从缓存获取数据...

    这确实是我们想要的结果,那么这个缓存也就实现了。

    3.2、总结:

    虽然缓存实现起来并不难,但是如果我们的应用中有很多地方都需要实现各种各样的缓存,代码量其实也并不小,很容易就能看出其中的不足:

  •     缓存代码与业务代码混在一起,是高耦合的,增加了维护和修改的复杂度。
  •     比较呆板,不支持按条件缓存。对于更加复杂的缓存,编写缓存代码可能会话费不少的时间。
  •     缓存实现写死,如果作修改可能会影响很多地方,缓存管理也不易使用。

    下面就介绍Spring的缓存支持,你会发现写缓存是如此的容易!

四、Spring Cache实例及介绍

    这里使用sping的抽象完成我们的缓存功能。

    4.1、示例

    这里需要修改数据获取服务实现改用Spring Cache,首先看下jar包依赖:

  <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
        <!-- 内存数据库  -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>net.sf.ehcache</groupId>
            <artifactId>ehcache</artifactId>
        </dependency>
    </dependencies>

    在resources下添加配置文件ehcache.xml:

<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="ehcache.xsd">
    <cache name="dataCache"
           maxEntriesLocalHeap="200"
           timeToLiveSeconds="600">
    </cache>
</ehcache>

    在程序启动类上开启缓存:

package com.anxpp.demo.cache;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching
public class CacheApplication {
    public static void main(String[] args) {
        SpringApplication.run(CacheApplication.class, args);
    }
}

    为了方便查看查询是否经过数据库,我们在application.properties中配置下sql的打印:

spring.jpa.show-sql = true

    避免与前一个service产生冲突,此处自定义bean名称(更好的方法是避免出现两个相同类名的bean)

   Service调整如下:

package com.anxpp.demo.cache.core.service.spring.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import com.anxpp.demo.cache.core.entity.Data;
import com.anxpp.demo.cache.core.repo.DataRepo;
import com.anxpp.demo.cache.core.service.spring.DataService;
/**
 * 数据查询服务
 * @author anxpp.com
 * 2017年1月3日 下午10:30:25
 */
@Service("SpringDataService")
public class DataServiceImpl implements DataService {
//  private static Logger log = LoggerFactory.getLogger(DataServiceImpl.class);
    @Autowired
    DataRepo dataRepo;
    @Override
    public void save(Data data) {
        dataRepo.save(data);
    }
    @Override
    @Cacheable(value="dataCache")
    public Data findById(Long id) {
        return dataRepo.findOne(id);
    }
    @Override
    public void clearCache() {
    }
}

    可以看到在findById方法中没有任何与缓存相关的代码,仅仅是简单的加了个注解:

@Cacheable(value="dataCache")

    再看看单元测试:

package com.anxpp.demo.cache.custom;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import com.anxpp.demo.cache.core.entity.Data;
import com.anxpp.demo.cache.core.service.spring.DataService;
/**
 * Spring Cache单元测试
 * @author anxpp.com
 * 2017年1月3日 下午10:59:52
 */
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringCacheApplicationTests {
    private static Logger log = LoggerFactory.getLogger(SpringCacheApplicationTests.class);
    @Autowired
    @Qualifier("SpringDataService")
    DataService dataService;
    /**
     * 因为测试比较简单,就不添加断言而是直接查看log输出了
     */
    @Test
    public void simpleTest(){
        Data data = new Data("anxpp.com");
        dataService.save(data);
        long time1st = System.currentTimeMillis();
        dataService.findById(data.getId());
        long time2nd = System.currentTimeMillis();
        log.info("第一次用时:"+(time2nd-time1st));
        dataService.findById(data.getId());
        long time3rd = System.currentTimeMillis();
        log.info("第二次用时:"+(time3rd-time2nd));
    }
}

    单元测试也很简单,通过查看控制台打印的sql语句即可判断是否经过了缓存:

Hibernate: insert into data (id, name) values (null, ?)
Hibernate: select data0_.id as id1_0_0_, data0_.name as name2_0_0_ from data data0_ where data0_.id=?
[main] c.a.d.c.c.SpringCacheApplicationTests    : 第一次用时:18
[main] c.a.d.c.c.SpringCacheApplicationTests    : 第二次用时:1

    很明显只访问了一次数据库,第二次是经过了缓存的。

    4.2、缓存相关注解

    @CacheConfig:主要用于配置该类中会用到的一些共用的缓存配置。

    比如@CacheConfig(cacheNames="cacheData"):配置了该数据访问对象中返回的内容将存储于名为cacheData的缓存对象中,我们也可以不使用该注解,直接通过@Cacheable自己配置缓存集的名字来定义。

    @Cacheable:配置当前方法的返回值将被加入缓存。同时在查询时,会先从缓存中获取,若不存在才再发起对数据库的访问。

    该注解主要有下面几个参数:

  •     value、cacheNames:两个等同的参数(cacheNames为Spring 4新增,作为value的别名),用于指定缓存存储的集合名。由于Spring 4中新增了@CacheConfig,因此在Spring 3中原本必须有的value属性,也成为非必需项了
  •     key:缓存对象存储在Map集合中的key值,非必需,缺省按照函数的所有参数组合作为key值,若自己配置需使用SpEL表达式,比如:@Cacheable(key = "#p0"):使用函数第一个参数作为缓存的key值,更多关于SpEL表达式的详细内容可参考官方文档
  •     condition:缓存对象的条件,非必需,也需使用SpEL表达式,只有满足表达式条件的内容才会被缓存,比如:@Cacheable(key = "#p0", condition = "#p0.length() < 3"),表示只有当第一个参数的长度小于3的时候才会被缓存。
  •     unless:另外一个缓存条件参数,非必需,需使用SpEL表达式。它不同于condition参数的地方在于它的判断时机,该条件是在函数被调用之后才做判断的,所以它可以通过对result进行判断。
  •     keyGenerator:用于指定key生成器,非必需。若需要指定一个自定义的key生成器,我们需要去实现org.springframework.cache.interceptor.KeyGenerator接口,并使用该参数来指定,该参数与key是互斥的!
  •     cacheManager:用于指定使用哪个缓存管理器,非必需。只有当有多个时才需要使用。
  •     cacheResolver:用于指定使用那个缓存解析器,非必需。需通过org.springframework.cache.interceptor.CacheResolver接口来实现自己的缓存解析器,并用该参数指定。

    @CachePut:配置于函数上,能够根据参数定义条件来进行缓存,它与@Cacheable不同的是,它每次都会真实调用函数,所以主要用于数据新增和修改操作上。它的参数与@Cacheable类似,具体功能可参考上面对@Cacheable参数的解析。

    @CacheEvict:配置于函数上,通常用在删除方法上,用来从缓存中移除相应数据。

    除了同@Cacheable一样的参数之外,它还有下面两个参数:

  •     allEntries:非必需,默认为false。当为true时,会移除所有数据。
  •     beforeInvocation:非必需,默认为false,会在调用方法之后移除数据。当为true时,会在调用方法之前移除数据。

    4.3、配置缓存

    在Spring Boot中通过@EnableCaching注解自动化配置合适的缓存管理器(CacheManager),Spring Boot根据下面的顺序去侦测缓存提供者: Generic、JCache (JSR-107)、EhCache 2.x、Hazelcast、Infinispan、Redis、Guava、Simple。

    除了按顺序侦测外,我们也可以通过配置属性spring.cache.type来强制指定。

    使用EhCache

    在Spring Boot中开启EhCache非常简单,只需要在工程中加入ehcache.xml配置文件并在pom.xml中增加ehcache依赖,框架只要发现该文件,就会创建EhCache的缓存管理器。

    ehcache.xml文件的添加和maven依赖如上例所示,更多的配置请参考EhCache相关文档。

    对于EhCache的配置文件也可以通过application.properties文件中使用spring.cache.ehcache.config属性来指定,比如:

spring.cache.ehcache.config=classpath:cache/mycacheconfig.xml

五、扩展示例

    5.1、清理缓存

    上面已经介绍了CacheEvict注解,此处实战。

    前面的例子,如果某条数据发生了改变,就需要更新对应某个缓存,此外在某些操作前,我们还希望情况所有缓存: